mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-03 20:38:03 +03:00
Compare commits
55
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b26a91de4f | ||
|
|
1f304916bd | ||
|
|
0086362663 | ||
|
|
e51b3c3621 | ||
|
|
6243d2a26e | ||
|
|
35e616d3b9 | ||
|
|
08cb6e6bca | ||
|
|
48ad0300ea | ||
|
|
0fc379203f | ||
|
|
fc8f8a451d | ||
|
|
e5e85ca9da | ||
|
|
7780db9bbe | ||
|
|
2953d44734 | ||
|
|
7b8ade3ec5 | ||
|
|
5dda894e29 | ||
|
|
47a2c2ffdc | ||
|
|
7a018833ec | ||
|
|
65e853ed84 | ||
|
|
3519dfecbd | ||
|
|
df261e4479 | ||
|
|
61cad5ec8b | ||
|
|
a642a190ed | ||
|
|
60e2a0c502 | ||
|
|
7d3e44fee2 | ||
|
|
9927942aaa | ||
|
|
a308ded2e6 | ||
|
|
7741e9e77e | ||
|
|
d562d8947d | ||
|
|
dbb1ea30ba | ||
|
|
efc9e6da62 | ||
|
|
8267cf953a | ||
|
|
24e6f6d551 | ||
|
|
dcdfc57ccd | ||
|
|
3461c511aa | ||
|
|
c412e77a9b | ||
|
|
ccb69ea5e2 | ||
|
|
52a412d9e2 | ||
|
|
18a1b5042a | ||
|
|
c26d2eda24 | ||
|
|
a1bf968be9 | ||
|
|
c037ccd98d | ||
|
|
37ceb8b4b6 | ||
|
|
fd2ca74822 | ||
|
|
47cfe9994a | ||
|
|
3e2f040cd8 | ||
|
|
c7245c0336 | ||
|
|
eef6e63bc1 | ||
|
|
6ce8dc53e7 | ||
|
|
01a034be53 | ||
|
|
de2caf3cef | ||
|
|
cecc88f43c | ||
|
|
cd4ce973e9 | ||
|
|
fc7b980636 | ||
|
|
8ee131cbbb | ||
|
|
2776ea6d74 |
@@ -67,9 +67,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
cache: false
|
cache: false
|
||||||
- name: Check Format
|
- name: Check Format
|
||||||
run: |
|
run: go run ./infra/vformat/main.go -mode check -pwd ./
|
||||||
go install -v mvdan.cc/gofumpt@latest
|
|
||||||
go run ./infra/vformat/main.go -mode check -pwd ./
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
needs: check-assets
|
needs: check-assets
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func newFakeDNSSniffer(ctx context.Context) (protocolSnifferWithMetadata, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if fakeDNSEngine == nil {
|
if fakeDNSEngine == nil {
|
||||||
errNotInit := errors.New("FakeDNSEngine is not initialized, but such a sniffer is used").AtError()
|
errNotInit := errors.New("FakeDNSEngine is not initialized, but such a sniffer is used")
|
||||||
return protocolSnifferWithMetadata{}, errNotInit
|
return protocolSnifferWithMetadata{}, errNotInit
|
||||||
}
|
}
|
||||||
return protocolSnifferWithMetadata{protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) {
|
return protocolSnifferWithMetadata{protocolSniffer: func(ctx context.Context, bytes []byte) (SniffResult, error) {
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ func toNetIP(addrs []net.Address) ([]net.IP, error) {
|
|||||||
if addr.Family().IsIP() {
|
if addr.Family().IsIP() {
|
||||||
ips = append(ips, addr.IP())
|
ips = append(ips, addr.IP())
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("Failed to convert address", addr, "to Net IP.").AtWarning()
|
return nil, errors.New("Failed to convert address", addr, "to Net IP.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ips, nil
|
return ips, nil
|
||||||
|
|||||||
@@ -212,6 +212,28 @@ func (s *DNS) IsOwnLink(ctx context.Context) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MayUseSystemResolver reports whether any name server configured here could
|
||||||
|
// still resolve through the system resolver. That is what happens when no name
|
||||||
|
// server is configured at all, and it is also what a name server pointed at
|
||||||
|
// "localhost" does. Callers that are about to redirect the system resolver need
|
||||||
|
// to know, because a resolution path that reaches it would then loop back to
|
||||||
|
// them.
|
||||||
|
//
|
||||||
|
// Any such server is enough: name servers can be selected per domain, so a
|
||||||
|
// single local one makes some query reach the system resolver even when
|
||||||
|
// independent upstreams are configured alongside it.
|
||||||
|
func (s *DNS) MayUseSystemResolver() bool {
|
||||||
|
if len(s.clients) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, client := range s.clients {
|
||||||
|
if _, isLocal := client.server.(*LocalNameServer); isLocal {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// LookupIP implements dns.Client.
|
// LookupIP implements dns.Client.
|
||||||
func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, error) {
|
func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, error) {
|
||||||
// Normalize the FQDN form query
|
// Normalize the FQDN form query
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/net"
|
||||||
|
feature_dns "github.com/xtls/xray-core/features/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fakeServer stands in for any name server that is not the system resolver.
|
||||||
|
type fakeServer struct{}
|
||||||
|
|
||||||
|
func (fakeServer) Name() string { return "fake" }
|
||||||
|
func (fakeServer) IsDisableCache() bool { return false }
|
||||||
|
func (fakeServer) QueryIP(context.Context, string, feature_dns.IPOption) ([]net.IP, uint32, error) {
|
||||||
|
return nil, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callers that are about to redirect the system resolver rely on this to tell
|
||||||
|
// whether any resolution path could still reach the system resolver, so the
|
||||||
|
// mixed shape has to be reported as reachable: a domain-specific rule can
|
||||||
|
// select the system resolver even when an independent upstream also exists.
|
||||||
|
func TestMayUseSystemResolver(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
clients []*Client
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no clients at all",
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only the system resolver",
|
||||||
|
clients: []*Client{{server: NewLocalNameServer()}},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "the system resolver alongside an independent name server",
|
||||||
|
clients: []*Client{{server: fakeServer{}}, {server: NewLocalNameServer()}},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only independent name servers",
|
||||||
|
clients: []*Client{{server: fakeServer{}}},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
server := &DNS{clients: tt.clients}
|
||||||
|
if got := server.MayUseSystemResolver(); got != tt.want {
|
||||||
|
t.Errorf("MayUseSystemResolver() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -188,10 +188,10 @@ func parseResponse(payload []byte) (*IPRecord, error) {
|
|||||||
var parser dnsmessage.Parser
|
var parser dnsmessage.Parser
|
||||||
h, err := parser.Start(payload)
|
h, err := parser.Start(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("failed to parse DNS response").Base(err).AtWarning()
|
return nil, errors.New("failed to parse DNS response").Base(err)
|
||||||
}
|
}
|
||||||
if err := parser.SkipAllQuestions(); err != nil {
|
if err := parser.SkipAllQuestions(); err != nil {
|
||||||
return nil, errors.New("failed to skip questions in DNS response").Base(err).AtWarning()
|
return nil, errors.New("failed to skip questions in DNS response").Base(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func NewFakeDNSHolder() (*Holder, error) {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if fkdns, err = NewFakeDNSHolderConfigOnly(nil); err != nil {
|
if fkdns, err = NewFakeDNSHolderConfigOnly(nil); err != nil {
|
||||||
return nil, errors.New("Unable to create Fake Dns Engine").Base(err).AtError()
|
return nil, errors.New("Unable to create Fake Dns Engine").Base(err)
|
||||||
}
|
}
|
||||||
err = fkdns.initialize(dns.FakeIPv4Pool, 65535)
|
err = fkdns.initialize(dns.FakeIPv4Pool, 65535)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -80,13 +80,13 @@ func (fkdns *Holder) initialize(ipPoolCidr string, lruSize int) error {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if _, ipRange, err = net.ParseCIDR(ipPoolCidr); err != nil {
|
if _, ipRange, err = net.ParseCIDR(ipPoolCidr); err != nil {
|
||||||
return errors.New("Unable to parse CIDR for Fake DNS IP assignment").Base(err).AtError()
|
return errors.New("Unable to parse CIDR for Fake DNS IP assignment").Base(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ones, bits := ipRange.Mask.Size()
|
ones, bits := ipRange.Mask.Size()
|
||||||
rooms := bits - ones
|
rooms := bits - ones
|
||||||
if math.Log2(float64(lruSize)) >= float64(rooms) {
|
if math.Log2(float64(lruSize)) >= float64(rooms) {
|
||||||
return errors.New("LRU size is bigger than subnet size").AtError()
|
return errors.New("LRU size is bigger than subnet size")
|
||||||
}
|
}
|
||||||
fkdns.domainToIP = cache.NewLru(lruSize)
|
fkdns.domainToIP = cache.NewLru(lruSize)
|
||||||
fkdns.ipRange = ipRange
|
fkdns.ipRange = ipRange
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dis
|
|||||||
if dest.Network == net.Network_UDP { // UDP classic DNS mode
|
if dest.Network == net.Network_UDP { // UDP classic DNS mode
|
||||||
return NewClassicNameServer(dest, dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP), nil
|
return NewClassicNameServer(dest, dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP), nil
|
||||||
}
|
}
|
||||||
return nil, errors.New("No available name server could be created from ", dest).AtWarning()
|
return nil, errors.New("No available name server could be created from ", dest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient creates a DNS client managing a name server with client IP, domain rules and expected IPs.
|
// NewClient creates a DNS client managing a name server with client IP, domain rules and expected IPs.
|
||||||
@@ -102,7 +102,7 @@ func NewClient(
|
|||||||
// Create a new server for each client for now
|
// Create a new server for each client for now
|
||||||
server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP)
|
server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to create nameserver").Base(err).AtWarning()
|
return errors.New("failed to create nameserver").Base(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, isLocalDNS := server.(*LocalNameServer)
|
_, isLocalDNS := server.(*LocalNameServer)
|
||||||
@@ -113,7 +113,7 @@ func NewClient(
|
|||||||
if len(ns.ExpectedIp) > 0 {
|
if len(ns.ExpectedIp) > 0 {
|
||||||
expectedMatcher, err = geodata.IPReg.BuildIPMatcher(ns.ExpectedIp)
|
expectedMatcher, err = geodata.IPReg.BuildIPMatcher(ns.ExpectedIp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to create expected ip matcher").Base(err).AtWarning()
|
return errors.New("failed to create expected ip matcher").Base(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ func NewClient(
|
|||||||
if len(ns.UnexpectedIp) > 0 {
|
if len(ns.UnexpectedIp) > 0 {
|
||||||
unexpectedMatcher, err = geodata.IPReg.BuildIPMatcher(ns.UnexpectedIp)
|
unexpectedMatcher, err = geodata.IPReg.BuildIPMatcher(ns.UnexpectedIp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to create unexpected ip matcher").Base(err).AtWarning()
|
return errors.New("failed to create unexpected ip matcher").Base(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func (s *FakeDNSServer) IsDisableCache() bool {
|
|||||||
|
|
||||||
func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, opt dns.IPOption) ([]net.IP, uint32, error) {
|
func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, opt dns.IPOption) ([]net.IP, uint32, error) {
|
||||||
if f.fakeDNSEngine == nil {
|
if f.fakeDNSEngine == nil {
|
||||||
return nil, 0, errors.New("Unable to locate a fake DNS Engine").AtError()
|
return nil, 0, errors.New("Unable to locate a fake DNS Engine")
|
||||||
}
|
}
|
||||||
|
|
||||||
var ips []net.Address
|
var ips []net.Address
|
||||||
@@ -39,7 +39,7 @@ func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, opt dns.IPOp
|
|||||||
|
|
||||||
netIP, err := toNetIP(ips)
|
netIP, err := toNetIP(ips)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, errors.New("Unable to convert IP to net ip").Base(err).AtError()
|
return nil, 0, errors.New("Unable to convert IP to net ip").Base(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
errors.LogInfo(ctx, f.Name(), " got answer: ", domain, " -> ", ips)
|
errors.LogInfo(ctx, f.Name(), " got answer: ", domain, " -> ", ips)
|
||||||
|
|||||||
+6
-2
@@ -89,10 +89,10 @@ func (g *Instance) startInternal() error {
|
|||||||
g.active = true
|
g.active = true
|
||||||
|
|
||||||
if err := g.initAccessLogger(); err != nil {
|
if err := g.initAccessLogger(); err != nil {
|
||||||
return errors.New("failed to initialize access logger").Base(err).AtWarning()
|
return errors.New("failed to initialize access logger").Base(err)
|
||||||
}
|
}
|
||||||
if err := g.initErrorLogger(); err != nil {
|
if err := g.initErrorLogger(); err != nil {
|
||||||
return errors.New("failed to initialize error logger").Base(err).AtWarning()
|
return errors.New("failed to initialize error logger").Base(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -141,6 +141,10 @@ func (g *Instance) Handle(msg log.Message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Instance) Severity() log.Severity {
|
||||||
|
return g.config.ErrorLogLevel
|
||||||
|
}
|
||||||
|
|
||||||
// Close implements common.Closable.Close().
|
// Close implements common.Closable.Close().
|
||||||
func (g *Instance) Close() error {
|
func (g *Instance) Close() error {
|
||||||
errors.LogDebug(context.Background(), "Logger closing")
|
errors.LogDebug(context.Background(), "Logger closing")
|
||||||
|
|||||||
+11
-22
@@ -330,7 +330,6 @@ type SenderConfig struct {
|
|||||||
// Send traffic through the given IP. Only IP is allowed.
|
// Send traffic through the given IP. Only IP is allowed.
|
||||||
Via *net.IPOrDomain `protobuf:"bytes,1,opt,name=via,proto3" json:"via,omitempty"`
|
Via *net.IPOrDomain `protobuf:"bytes,1,opt,name=via,proto3" json:"via,omitempty"`
|
||||||
StreamSettings *internet.StreamConfig `protobuf:"bytes,2,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"`
|
StreamSettings *internet.StreamConfig `protobuf:"bytes,2,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"`
|
||||||
ProxySettings *internet.ProxyConfig `protobuf:"bytes,3,opt,name=proxy_settings,json=proxySettings,proto3" json:"proxy_settings,omitempty"`
|
|
||||||
MultiplexSettings *MultiplexingConfig `protobuf:"bytes,4,opt,name=multiplex_settings,json=multiplexSettings,proto3" json:"multiplex_settings,omitempty"`
|
MultiplexSettings *MultiplexingConfig `protobuf:"bytes,4,opt,name=multiplex_settings,json=multiplexSettings,proto3" json:"multiplex_settings,omitempty"`
|
||||||
ViaCidr string `protobuf:"bytes,5,opt,name=via_cidr,json=viaCidr,proto3" json:"via_cidr,omitempty"`
|
ViaCidr string `protobuf:"bytes,5,opt,name=via_cidr,json=viaCidr,proto3" json:"via_cidr,omitempty"`
|
||||||
TargetStrategy internet.DomainStrategy `protobuf:"varint,6,opt,name=target_strategy,json=targetStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"target_strategy,omitempty"`
|
TargetStrategy internet.DomainStrategy `protobuf:"varint,6,opt,name=target_strategy,json=targetStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"target_strategy,omitempty"`
|
||||||
@@ -382,13 +381,6 @@ func (x *SenderConfig) GetStreamSettings() *internet.StreamConfig {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *SenderConfig) GetProxySettings() *internet.ProxyConfig {
|
|
||||||
if x != nil {
|
|
||||||
return x.ProxySettings
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *SenderConfig) GetMultiplexSettings() *MultiplexingConfig {
|
func (x *SenderConfig) GetMultiplexSettings() *MultiplexingConfig {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.MultiplexSettings
|
return x.MultiplexSettings
|
||||||
@@ -506,14 +498,13 @@ const file_app_proxyman_config_proto_rawDesc = "" +
|
|||||||
"\x03tag\x18\x01 \x01(\tR\x03tag\x12M\n" +
|
"\x03tag\x18\x01 \x01(\tR\x03tag\x12M\n" +
|
||||||
"\x11receiver_settings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\x10receiverSettings\x12G\n" +
|
"\x11receiver_settings\x18\x02 \x01(\v2 .xray.common.serial.TypedMessageR\x10receiverSettings\x12G\n" +
|
||||||
"\x0eproxy_settings\x18\x03 \x01(\v2 .xray.common.serial.TypedMessageR\rproxySettings\"\x10\n" +
|
"\x0eproxy_settings\x18\x03 \x01(\v2 .xray.common.serial.TypedMessageR\rproxySettings\"\x10\n" +
|
||||||
"\x0eOutboundConfig\"\x9d\x03\n" +
|
"\x0eOutboundConfig\"\xd6\x02\n" +
|
||||||
"\fSenderConfig\x12-\n" +
|
"\fSenderConfig\x12-\n" +
|
||||||
"\x03via\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x03via\x12N\n" +
|
"\x03via\x18\x01 \x01(\v2\x1b.xray.common.net.IPOrDomainR\x03via\x12N\n" +
|
||||||
"\x0fstream_settings\x18\x02 \x01(\v2%.xray.transport.internet.StreamConfigR\x0estreamSettings\x12K\n" +
|
"\x0fstream_settings\x18\x02 \x01(\v2%.xray.transport.internet.StreamConfigR\x0estreamSettings\x12T\n" +
|
||||||
"\x0eproxy_settings\x18\x03 \x01(\v2$.xray.transport.internet.ProxyConfigR\rproxySettings\x12T\n" +
|
|
||||||
"\x12multiplex_settings\x18\x04 \x01(\v2%.xray.app.proxyman.MultiplexingConfigR\x11multiplexSettings\x12\x19\n" +
|
"\x12multiplex_settings\x18\x04 \x01(\v2%.xray.app.proxyman.MultiplexingConfigR\x11multiplexSettings\x12\x19\n" +
|
||||||
"\bvia_cidr\x18\x05 \x01(\tR\aviaCidr\x12P\n" +
|
"\bvia_cidr\x18\x05 \x01(\tR\aviaCidr\x12P\n" +
|
||||||
"\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategy\"\xa4\x01\n" +
|
"\x0ftarget_strategy\x18\x06 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0etargetStrategyJ\x04\b\x03\x10\x04\"\xa4\x01\n" +
|
||||||
"\x12MultiplexingConfig\x12\x18\n" +
|
"\x12MultiplexingConfig\x12\x18\n" +
|
||||||
"\aenabled\x18\x01 \x01(\bR\aenabled\x12 \n" +
|
"\aenabled\x18\x01 \x01(\bR\aenabled\x12 \n" +
|
||||||
"\vconcurrency\x18\x02 \x01(\x05R\vconcurrency\x12(\n" +
|
"\vconcurrency\x18\x02 \x01(\x05R\vconcurrency\x12(\n" +
|
||||||
@@ -548,8 +539,7 @@ var file_app_proxyman_config_proto_goTypes = []any{
|
|||||||
(*net.IPOrDomain)(nil), // 10: xray.common.net.IPOrDomain
|
(*net.IPOrDomain)(nil), // 10: xray.common.net.IPOrDomain
|
||||||
(*internet.StreamConfig)(nil), // 11: xray.transport.internet.StreamConfig
|
(*internet.StreamConfig)(nil), // 11: xray.transport.internet.StreamConfig
|
||||||
(*serial.TypedMessage)(nil), // 12: xray.common.serial.TypedMessage
|
(*serial.TypedMessage)(nil), // 12: xray.common.serial.TypedMessage
|
||||||
(*internet.ProxyConfig)(nil), // 13: xray.transport.internet.ProxyConfig
|
(internet.DomainStrategy)(0), // 13: xray.transport.internet.DomainStrategy
|
||||||
(internet.DomainStrategy)(0), // 14: xray.transport.internet.DomainStrategy
|
|
||||||
}
|
}
|
||||||
var file_app_proxyman_config_proto_depIdxs = []int32{
|
var file_app_proxyman_config_proto_depIdxs = []int32{
|
||||||
7, // 0: xray.app.proxyman.SniffingConfig.domains_excluded:type_name -> xray.common.geodata.DomainRule
|
7, // 0: xray.app.proxyman.SniffingConfig.domains_excluded:type_name -> xray.common.geodata.DomainRule
|
||||||
@@ -562,14 +552,13 @@ var file_app_proxyman_config_proto_depIdxs = []int32{
|
|||||||
12, // 7: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage
|
12, // 7: xray.app.proxyman.InboundHandlerConfig.proxy_settings:type_name -> xray.common.serial.TypedMessage
|
||||||
10, // 8: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain
|
10, // 8: xray.app.proxyman.SenderConfig.via:type_name -> xray.common.net.IPOrDomain
|
||||||
11, // 9: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig
|
11, // 9: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig
|
||||||
13, // 10: xray.app.proxyman.SenderConfig.proxy_settings:type_name -> xray.transport.internet.ProxyConfig
|
6, // 10: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig
|
||||||
6, // 11: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig
|
13, // 11: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy
|
||||||
14, // 12: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy
|
12, // [12:12] is the sub-list for method output_type
|
||||||
13, // [13:13] is the sub-list for method output_type
|
12, // [12:12] is the sub-list for method input_type
|
||||||
13, // [13:13] is the sub-list for method input_type
|
12, // [12:12] is the sub-list for extension type_name
|
||||||
13, // [13:13] is the sub-list for extension type_name
|
12, // [12:12] is the sub-list for extension extendee
|
||||||
13, // [13:13] is the sub-list for extension extendee
|
0, // [0:12] is the sub-list for field type_name
|
||||||
0, // [0:13] is the sub-list for field type_name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_app_proxyman_config_proto_init() }
|
func init() { file_app_proxyman_config_proto_init() }
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ message SenderConfig {
|
|||||||
// Send traffic through the given IP. Only IP is allowed.
|
// Send traffic through the given IP. Only IP is allowed.
|
||||||
xray.common.net.IPOrDomain via = 1;
|
xray.common.net.IPOrDomain via = 1;
|
||||||
xray.transport.internet.StreamConfig stream_settings = 2;
|
xray.transport.internet.StreamConfig stream_settings = 2;
|
||||||
xray.transport.internet.ProxyConfig proxy_settings = 3;
|
reserved 3;
|
||||||
MultiplexingConfig multiplex_settings = 4;
|
MultiplexingConfig multiplex_settings = 4;
|
||||||
string via_cidr = 5;
|
string via_cidr = 5;
|
||||||
xray.transport.internet.DomainStrategy target_strategy = 6;
|
xray.transport.internet.DomainStrategy target_strategy = 6;
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
|
|||||||
}
|
}
|
||||||
mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings)
|
mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("failed to parse stream config").Base(err).AtWarning()
|
return nil, errors.New("failed to parse stream config").Base(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
newCtx := session.ContextWithInbound(ctx, &session.Inbound{Tag: tag, Source: src})
|
newCtx := session.ContextWithInbound(ctx, &session.Inbound{Tag: tag, Source: src})
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ func NewHandler(ctx context.Context, config *core.InboundHandlerConfig) (inbound
|
|||||||
|
|
||||||
receiverSettings, ok := rawReceiverSettings.(*proxyman.ReceiverConfig)
|
receiverSettings, ok := rawReceiverSettings.(*proxyman.ReceiverConfig)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("not a ReceiverConfig").AtError()
|
return nil, errors.New("not a ReceiverConfig")
|
||||||
}
|
}
|
||||||
|
|
||||||
streamSettings := receiverSettings.StreamSettings
|
streamSettings := receiverSettings.StreamSettings
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ func (w *tcpWorker) Start() error {
|
|||||||
go w.callback(conn)
|
go w.callback(conn)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to listen TCP on ", w.port).AtWarning().Base(err)
|
return errors.New("failed to listen TCP on ", w.port).Base(err)
|
||||||
}
|
}
|
||||||
w.hub = hub
|
w.hub = hub
|
||||||
return nil
|
return nil
|
||||||
@@ -528,7 +528,7 @@ func (w *dsWorker) Start() error {
|
|||||||
go w.callback(conn)
|
go w.callback(conn)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to listen Unix Domain Socket on ", w.address).AtWarning().Base(err)
|
return errors.New("failed to listen Unix Domain Socket on ", w.address).Base(err)
|
||||||
}
|
}
|
||||||
w.hub = hub
|
w.hub = hub
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/mux"
|
"github.com/xtls/xray-core/common/mux"
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/common/net/cnc"
|
|
||||||
"github.com/xtls/xray-core/common/serial"
|
"github.com/xtls/xray-core/common/serial"
|
||||||
"github.com/xtls/xray-core/common/session"
|
"github.com/xtls/xray-core/common/session"
|
||||||
"github.com/xtls/xray-core/core"
|
"github.com/xtls/xray-core/core"
|
||||||
@@ -26,8 +25,6 @@ import (
|
|||||||
"github.com/xtls/xray-core/transport"
|
"github.com/xtls/xray-core/transport"
|
||||||
"github.com/xtls/xray-core/transport/internet"
|
"github.com/xtls/xray-core/transport/internet"
|
||||||
"github.com/xtls/xray-core/transport/internet/stat"
|
"github.com/xtls/xray-core/transport/internet/stat"
|
||||||
"github.com/xtls/xray-core/transport/internet/tls"
|
|
||||||
"github.com/xtls/xray-core/transport/pipe"
|
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,7 +60,6 @@ type Handler struct {
|
|||||||
streamSettings *internet.MemoryStreamConfig
|
streamSettings *internet.MemoryStreamConfig
|
||||||
proxyConfig proto.Message
|
proxyConfig proto.Message
|
||||||
proxy proxy.Outbound
|
proxy proxy.Outbound
|
||||||
outboundManager outbound.Manager
|
|
||||||
mux *mux.ClientManager
|
mux *mux.ClientManager
|
||||||
xudp *mux.ClientManager
|
xudp *mux.ClientManager
|
||||||
udp443 string
|
udp443 string
|
||||||
@@ -77,7 +73,6 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
|
|||||||
uplinkCounter, downlinkCounter := getStatCounter(v, config.Tag)
|
uplinkCounter, downlinkCounter := getStatCounter(v, config.Tag)
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
tag: config.Tag,
|
tag: config.Tag,
|
||||||
outboundManager: v.GetFeature(outbound.ManagerType()).(outbound.Manager),
|
|
||||||
uplinkCounter: uplinkCounter,
|
uplinkCounter: uplinkCounter,
|
||||||
downlinkCounter: downlinkCounter,
|
downlinkCounter: downlinkCounter,
|
||||||
}
|
}
|
||||||
@@ -92,7 +87,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
|
|||||||
h.senderSettings = s
|
h.senderSettings = s
|
||||||
mss, err := internet.ToMemoryStreamConfig(s.StreamSettings)
|
mss, err := internet.ToMemoryStreamConfig(s.StreamSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("failed to parse stream settings").Base(err).AtWarning()
|
return nil, errors.New("failed to parse stream settings").Base(err)
|
||||||
}
|
}
|
||||||
h.streamSettings = mss
|
h.streamSettings = mss
|
||||||
default:
|
default:
|
||||||
@@ -108,9 +103,11 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
|
|||||||
|
|
||||||
ctx = session.ContextWithFullHandler(ctx, h)
|
ctx = session.ContextWithFullHandler(ctx, h)
|
||||||
|
|
||||||
newCtx := session.ContextWithStreamSettings(ctx, h.streamSettings)
|
if h.streamSettings != nil {
|
||||||
|
ctx = session.ContextWithStreamSettings(ctx, h.streamSettings)
|
||||||
|
}
|
||||||
|
|
||||||
rawProxyHandler, err := common.CreateObject(newCtx, proxyConfig)
|
rawProxyHandler, err := common.CreateObject(ctx, proxyConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -197,7 +194,6 @@ func (h *Handler) Dispatch(ctx context.Context, link *transport.Link) {
|
|||||||
common.Interrupt(link.Reader)
|
common.Interrupt(link.Reader)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
unchangedDomain := ob.Target.Address.Domain()
|
unchangedDomain := ob.Target.Address.Domain()
|
||||||
ob.Target.Address = net.IPAddress(ips[dice.Roll(len(ips))])
|
ob.Target.Address = net.IPAddress(ips[dice.Roll(len(ips))])
|
||||||
@@ -221,7 +217,7 @@ func (h *Handler) Dispatch(ctx context.Context, link *transport.Link) {
|
|||||||
if ob.Target.Network == net.Network_UDP && ob.Target.Port == 443 {
|
if ob.Target.Network == net.Network_UDP && ob.Target.Port == 443 {
|
||||||
switch h.udp443 {
|
switch h.udp443 {
|
||||||
case "reject":
|
case "reject":
|
||||||
test(errors.New("XUDP rejected UDP/443 traffic").AtInfo())
|
test(errors.New("XUDP rejected UDP/443 traffic"))
|
||||||
return
|
return
|
||||||
case "skip":
|
case "skip":
|
||||||
goto out
|
goto out
|
||||||
@@ -270,66 +266,26 @@ func (h *Handler) DestIpAddress() net.IP {
|
|||||||
|
|
||||||
// Dial implements internet.Dialer.
|
// Dial implements internet.Dialer.
|
||||||
func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) {
|
func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connection, error) {
|
||||||
if h.senderSettings != nil {
|
if h.senderSettings != nil && h.senderSettings.Via != nil {
|
||||||
|
outbounds := session.OutboundsFromContext(ctx)
|
||||||
if h.senderSettings.ProxySettings.HasTag() {
|
ob := outbounds[len(outbounds)-1]
|
||||||
|
h.SetOutboundGateway(ctx, ob)
|
||||||
tag := h.senderSettings.ProxySettings.Tag
|
|
||||||
handler := h.outboundManager.GetHandler(tag)
|
|
||||||
if handler != nil {
|
|
||||||
errors.LogDebug(ctx, "proxying to ", tag, " for dest ", dest)
|
|
||||||
outbounds := session.OutboundsFromContext(ctx)
|
|
||||||
ctx = session.ContextWithOutbounds(ctx, append(outbounds, &session.Outbound{
|
|
||||||
Target: dest,
|
|
||||||
Tag: tag,
|
|
||||||
})) // add another outbound in session ctx
|
|
||||||
opts := pipe.OptionsFromContext(ctx)
|
|
||||||
uplinkReader, uplinkWriter := pipe.New(opts...)
|
|
||||||
downlinkReader, downlinkWriter := pipe.New(opts...)
|
|
||||||
|
|
||||||
go handler.Dispatch(ctx, &transport.Link{Reader: uplinkReader, Writer: downlinkWriter})
|
|
||||||
conn := cnc.NewConnection(cnc.ConnectionInputMulti(uplinkWriter), cnc.ConnectionOutputMulti(downlinkReader))
|
|
||||||
|
|
||||||
if config := tls.ConfigFromStreamSettings(h.streamSettings); config != nil {
|
|
||||||
tlsConfig := config.GetTLSConfig(tls.WithDestination(dest))
|
|
||||||
conn = tls.Client(conn, tlsConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
return h.getStatCouterConnection(conn), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
errors.LogError(ctx, "failed to get outbound handler with tag: ", tag)
|
|
||||||
return nil, errors.New("failed to get outbound handler with tag: " + tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
if h.senderSettings.Via != nil {
|
|
||||||
outbounds := session.OutboundsFromContext(ctx)
|
|
||||||
ob := outbounds[len(outbounds)-1]
|
|
||||||
h.SetOutboundGateway(ctx, ob)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := internet.Dial(ctx, dest, h.streamSettings)
|
conn, err := internet.Dial(ctx, dest, h.streamSettings)
|
||||||
conn = h.getStatCouterConnection(conn)
|
conn = h.getStatCouterConnection(conn)
|
||||||
outbounds := session.OutboundsFromContext(ctx)
|
|
||||||
if outbounds != nil {
|
|
||||||
ob := outbounds[len(outbounds)-1]
|
|
||||||
ob.Conn = conn
|
|
||||||
} else {
|
|
||||||
// for Vision's pre-connect
|
|
||||||
}
|
|
||||||
return conn, err
|
return conn, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound) {
|
func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound) {
|
||||||
if ob.Gateway == nil && h.senderSettings != nil && h.senderSettings.Via != nil && !h.senderSettings.ProxySettings.HasTag() && (h.streamSettings.SocketSettings == nil || len(h.streamSettings.SocketSettings.DialerProxy) == 0) {
|
if ob.Gateway == nil && h.senderSettings != nil && h.senderSettings.Via != nil &&
|
||||||
|
(h.streamSettings.SocketSettings == nil || len(h.streamSettings.SocketSettings.DialerProxy) == 0) {
|
||||||
var domain string
|
var domain string
|
||||||
addr := h.senderSettings.Via.AsAddress()
|
addr := h.senderSettings.Via.AsAddress()
|
||||||
domain = h.senderSettings.Via.GetDomain()
|
domain = h.senderSettings.Via.GetDomain()
|
||||||
switch {
|
switch {
|
||||||
case h.senderSettings.ViaCidr != "":
|
case h.senderSettings.ViaCidr != "":
|
||||||
ob.Gateway = ParseRandomIP(addr, h.senderSettings.ViaCidr)
|
ob.Gateway = ParseRandomIP(addr, h.senderSettings.ViaCidr)
|
||||||
|
|
||||||
case domain == "origin":
|
case domain == "origin":
|
||||||
if inbound := session.InboundFromContext(ctx); inbound != nil {
|
if inbound := session.InboundFromContext(ctx); inbound != nil {
|
||||||
if inbound.Local.IsValid() && inbound.Local.Address.Family().IsIP() {
|
if inbound.Local.IsValid() && inbound.Local.Address.Family().IsIP() {
|
||||||
@@ -344,11 +300,9 @@ func (h *Handler) SetOutboundGateway(ctx context.Context, ob *session.Outbound)
|
|||||||
errors.LogDebug(ctx, "use inbound source ip as sendthrough: ", inbound.Source.Address.String())
|
errors.LogDebug(ctx, "use inbound source ip as sendthrough: ", inbound.Source.Address.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// case addr.Family().IsDomain():
|
default: // case addr.Family().IsDomain():
|
||||||
default:
|
|
||||||
ob.Gateway = addr
|
ob.Gateway = addr
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,13 +68,13 @@ func (p *Portal) HandleConnection(ctx context.Context, link *transport.Link) err
|
|||||||
outbounds := session.OutboundsFromContext(ctx)
|
outbounds := session.OutboundsFromContext(ctx)
|
||||||
ob := outbounds[len(outbounds)-1]
|
ob := outbounds[len(outbounds)-1]
|
||||||
if ob == nil {
|
if ob == nil {
|
||||||
return errors.New("outbound metadata not found").AtError()
|
return errors.New("outbound metadata not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
if isDomain(ob.Target, p.domain) {
|
if isDomain(ob.Target, p.domain) {
|
||||||
muxClient, err := mux.NewClientWorker(*link, mux.ClientStrategy{})
|
muxClient, err := mux.NewClientWorker(*link, mux.ClientStrategy{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to create mux client worker").Base(err).AtWarning()
|
return errors.New("failed to create mux client worker").Base(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
worker, err := NewPortalWorker(muxClient)
|
worker, err := NewPortalWorker(muxClient)
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if conds.Len() == 0 {
|
if conds.Len() == 0 {
|
||||||
return nil, errors.New("this rule has no effective fields").AtWarning()
|
return nil, errors.New("this rule has no effective fields")
|
||||||
}
|
}
|
||||||
|
|
||||||
return conds, nil
|
return conds, nil
|
||||||
@@ -145,7 +145,7 @@ func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatch
|
|||||||
}
|
}
|
||||||
s, ok := i.(*StrategyLeastLoadConfig)
|
s, ok := i.(*StrategyLeastLoadConfig)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("not a StrategyLeastLoadConfig").AtError()
|
return nil, errors.New("not a StrategyLeastLoadConfig")
|
||||||
}
|
}
|
||||||
leastLoadStrategy := NewLeastLoadStrategy(s)
|
leastLoadStrategy := NewLeastLoadStrategy(s)
|
||||||
return &Balancer{
|
return &Balancer{
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type windowsReader struct {
|
type windowsReader struct {
|
||||||
bufs []syscall.WSABuf
|
bufs []syscall.WSABuf
|
||||||
|
ready bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *windowsReader) Init(bs []*Buffer) {
|
func (r *windowsReader) Init(bs []*Buffer) {
|
||||||
@@ -15,6 +16,7 @@ func (r *windowsReader) Init(bs []*Buffer) {
|
|||||||
for _, b := range bs {
|
for _, b := range bs {
|
||||||
r.bufs = append(r.bufs, syscall.WSABuf{Len: uint32(Size), Buf: &b.v[0]})
|
r.bufs = append(r.bufs, syscall.WSABuf{Len: uint32(Size), Buf: &b.v[0]})
|
||||||
}
|
}
|
||||||
|
r.ready = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *windowsReader) Clear() {
|
func (r *windowsReader) Clear() {
|
||||||
@@ -25,6 +27,14 @@ func (r *windowsReader) Clear() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *windowsReader) Read(fd uintptr) int32 {
|
func (r *windowsReader) Read(fd uintptr) int32 {
|
||||||
|
// On the first invocation, we return -1 to indicate "not ready"
|
||||||
|
// to make rawConn.Read wait for readability using the runtime's own mechanism
|
||||||
|
// because syscall.WSARecv() is a blocking call when used with nil OVERLAPPED
|
||||||
|
if !r.ready {
|
||||||
|
r.ready = true
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
var nBytes uint32
|
var nBytes uint32
|
||||||
var flags uint32
|
var flags uint32
|
||||||
err := syscall.WSARecv(syscall.Handle(fd), &r.bufs[0], uint32(len(r.bufs)), &nBytes, &flags, nil, nil)
|
err := syscall.WSARecv(syscall.Handle(fd), &r.bufs[0], uint32(len(r.bufs)), &nBytes, &flags, nil, nil)
|
||||||
|
|||||||
@@ -118,7 +118,9 @@ func (w *BufferedWriter) Write(b []byte) (int, error) {
|
|||||||
|
|
||||||
nBytes, err := w.buffer.Write(b)
|
nBytes, err := w.buffer.Write(b)
|
||||||
totalBytes += nBytes
|
totalBytes += nBytes
|
||||||
if err != nil {
|
|
||||||
|
// ErrBufferFull means a partial write, so flush below and continue
|
||||||
|
if err != nil && err != ErrBufferFull {
|
||||||
return totalBytes, err
|
return totalBytes, err
|
||||||
}
|
}
|
||||||
if !w.buffered || w.buffer.IsFull() {
|
if !w.buffered || w.buffer.IsFull() {
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import (
|
|||||||
|
|
||||||
// [,)
|
// [,)
|
||||||
func RandBetween(from int64, to int64) int64 {
|
func RandBetween(from int64, to int64) int64 {
|
||||||
if from == to {
|
|
||||||
return from
|
|
||||||
}
|
|
||||||
if from > to {
|
if from > to {
|
||||||
from, to = to, from
|
from, to = to, from
|
||||||
}
|
}
|
||||||
|
if d := to - from; d == 0 || d == 1 {
|
||||||
|
return from
|
||||||
|
}
|
||||||
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
|
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
|
||||||
return from + bigInt.Int64()
|
return from + bigInt.Int64()
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-65
@@ -18,17 +18,12 @@ type hasInnerError interface {
|
|||||||
Unwrap() error
|
Unwrap() error
|
||||||
}
|
}
|
||||||
|
|
||||||
type hasSeverity interface {
|
|
||||||
Severity() log.Severity
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error is an error object with underlying error.
|
// Error is an error object with underlying error.
|
||||||
type Error struct {
|
type Error struct {
|
||||||
prefix []interface{}
|
prefix []interface{}
|
||||||
message []interface{}
|
message []interface{}
|
||||||
caller string
|
caller string
|
||||||
inner error
|
inner error
|
||||||
severity log.Severity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error implements error.Error().
|
// Error implements error.Error().
|
||||||
@@ -69,46 +64,6 @@ func (err *Error) Base(e error) *Error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err *Error) atSeverity(s log.Severity) *Error {
|
|
||||||
err.severity = s
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err *Error) Severity() log.Severity {
|
|
||||||
if err.inner == nil {
|
|
||||||
return err.severity
|
|
||||||
}
|
|
||||||
|
|
||||||
if s, ok := err.inner.(hasSeverity); ok {
|
|
||||||
as := s.Severity()
|
|
||||||
if as < err.severity {
|
|
||||||
return as
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return err.severity
|
|
||||||
}
|
|
||||||
|
|
||||||
// AtDebug sets the severity to debug.
|
|
||||||
func (err *Error) AtDebug() *Error {
|
|
||||||
return err.atSeverity(log.Severity_Debug)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AtInfo sets the severity to info.
|
|
||||||
func (err *Error) AtInfo() *Error {
|
|
||||||
return err.atSeverity(log.Severity_Info)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AtWarning sets the severity to warning.
|
|
||||||
func (err *Error) AtWarning() *Error {
|
|
||||||
return err.atSeverity(log.Severity_Warning)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AtError sets the severity to error.
|
|
||||||
func (err *Error) AtError() *Error {
|
|
||||||
return err.atSeverity(log.Severity_Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the string representation of this error.
|
// String returns the string representation of this error.
|
||||||
func (err *Error) String() string {
|
func (err *Error) String() string {
|
||||||
return err.Error()
|
return err.Error()
|
||||||
@@ -132,9 +87,8 @@ func New(msg ...interface{}) *Error {
|
|||||||
details = details[:i]
|
details = details[:i]
|
||||||
}
|
}
|
||||||
return &Error{
|
return &Error{
|
||||||
message: msg,
|
message: msg,
|
||||||
severity: log.Severity_Info,
|
caller: details,
|
||||||
caller: details,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +125,9 @@ func LogErrorInner(ctx context.Context, inner error, msg ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func doLog(ctx context.Context, inner error, severity log.Severity, msg ...interface{}) {
|
func doLog(ctx context.Context, inner error, severity log.Severity, msg ...interface{}) {
|
||||||
|
if log.GetSeverity() < severity {
|
||||||
|
return
|
||||||
|
}
|
||||||
pc, _, _, _ := runtime.Caller(2)
|
pc, _, _, _ := runtime.Caller(2)
|
||||||
details := runtime.FuncForPC(pc).Name()
|
details := runtime.FuncForPC(pc).Name()
|
||||||
if len(details) >= trim {
|
if len(details) >= trim {
|
||||||
@@ -181,10 +138,9 @@ func doLog(ctx context.Context, inner error, severity log.Severity, msg ...inter
|
|||||||
details = details[:i]
|
details = details[:i]
|
||||||
}
|
}
|
||||||
err := &Error{
|
err := &Error{
|
||||||
message: msg,
|
message: msg,
|
||||||
severity: severity,
|
caller: details,
|
||||||
caller: details,
|
inner: inner,
|
||||||
inner: inner,
|
|
||||||
}
|
}
|
||||||
if ctx != nil && ctx != context.Background() {
|
if ctx != nil && ctx != context.Background() {
|
||||||
id := uint32(c.IDFromContext(ctx))
|
id := uint32(c.IDFromContext(ctx))
|
||||||
@@ -193,7 +149,7 @@ func doLog(ctx context.Context, inner error, severity log.Severity, msg ...inter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Record(&log.GeneralMessage{
|
log.Record(&log.GeneralMessage{
|
||||||
Severity: GetSeverity(err),
|
Severity: severity,
|
||||||
Content: err,
|
Content: err,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -217,11 +173,3 @@ L:
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSeverity returns the actual severity of the error, including inner errors.
|
|
||||||
func GetSeverity(err error) log.Severity {
|
|
||||||
if s, ok := err.(hasSeverity); ok {
|
|
||||||
return s.Severity()
|
|
||||||
}
|
|
||||||
return log.Severity_Info
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,30 +7,21 @@ import (
|
|||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
. "github.com/xtls/xray-core/common/errors"
|
. "github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestError(t *testing.T) {
|
func TestError(t *testing.T) {
|
||||||
err := New("TestError")
|
err := New("TestError")
|
||||||
if v := GetSeverity(err); v != log.Severity_Info {
|
if v := err.Error(); !strings.Contains(v, "TestError") {
|
||||||
t.Error("severity: ", v)
|
t.Error("error: ", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = New("TestError2").Base(io.EOF)
|
err = New("TestError2").Base(io.EOF)
|
||||||
if v := GetSeverity(err); v != log.Severity_Info {
|
if v := err.Error(); !strings.Contains(v, "EOF") {
|
||||||
t.Error("severity: ", v)
|
t.Error("error: ", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = New("TestError3").Base(io.EOF).AtWarning()
|
err = New("TestError3").Base(io.EOF)
|
||||||
if v := GetSeverity(err); v != log.Severity_Warning {
|
err = New("TestError4").Base(err)
|
||||||
t.Error("severity: ", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = New("TestError4").Base(io.EOF).AtWarning()
|
|
||||||
err = New("TestError5").Base(err)
|
|
||||||
if v := GetSeverity(err); v != log.Severity_Warning {
|
|
||||||
t.Error("severity: ", v)
|
|
||||||
}
|
|
||||||
if v := err.Error(); !strings.Contains(v, "EOF") {
|
if v := err.Error(); !strings.Contains(v, "EOF") {
|
||||||
t.Error("error: ", v)
|
t.Error("error: ", v)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,19 +82,10 @@ func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatch
|
|||||||
}
|
}
|
||||||
g.Add(m, uint32(i))
|
g.Add(m, uint32(i))
|
||||||
case *DomainRule_Geosite:
|
case *DomainRule_Geosite:
|
||||||
domains, err := loadSiteWithAttrs(v.Geosite.File, v.Geosite.Code, v.Geosite.Attrs)
|
err := loadSiteMatchers(v.Geosite, func(m strmatcher.Matcher) { g.Add(m, uint32(i)) })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for j, d := range domains {
|
|
||||||
domains[j] = nil // peak mem
|
|
||||||
m, err := parseDomain(d)
|
|
||||||
if err != nil {
|
|
||||||
errors.LogError(context.Background(), "ignore invalid geosite entry in ", v.Geosite.File, ":", v.Geosite.Code, " at index ", j, ", ", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
g.Add(m, uint32(i))
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
panic("unknown domain rule type")
|
panic("unknown domain rule type")
|
||||||
}
|
}
|
||||||
@@ -108,12 +99,12 @@ func (f *MphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatch
|
|||||||
return g, nil
|
return g, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompactDomainMatcherFactory struct {
|
type CompactMphDomainMatcherFactory struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
shared *utils.WeakCacheMap[string, strmatcher.LinearAnyMatcher]
|
shared *utils.WeakCacheMap[string, strmatcher.MphValueMatcher]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmatcher.MatcherSet, error) {
|
func (f *CompactMphDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (*strmatcher.MphValueMatcher, error) {
|
||||||
key := rule.File + ":" + rule.Code + "@" + rule.Attrs
|
key := rule.File + ":" + rule.Code + "@" + rule.Attrs
|
||||||
|
|
||||||
f.Lock()
|
f.Lock()
|
||||||
@@ -125,33 +116,23 @@ func (f *CompactDomainMatcherFactory) getOrCreateFrom(rule *GeoSiteRule) (strmat
|
|||||||
}
|
}
|
||||||
errors.LogDebug(context.Background(), "geodata geosite matcher cache MISS ", key)
|
errors.LogDebug(context.Background(), "geodata geosite matcher cache MISS ", key)
|
||||||
|
|
||||||
s := strmatcher.NewLinearAnyMatcher()
|
s := strmatcher.NewMphValueMatcher()
|
||||||
domains, err := loadSiteWithAttrs(rule.File, rule.Code, rule.Attrs)
|
if err := loadSiteMatchers(rule, func(m strmatcher.Matcher) { s.Add(m, 0) }); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for i, d := range domains {
|
if err := s.Build(); err != nil {
|
||||||
domains[i] = nil // peak mem
|
return nil, err
|
||||||
m, err := parseDomain(d)
|
|
||||||
if err != nil {
|
|
||||||
errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.Add(m)
|
|
||||||
}
|
}
|
||||||
f.shared.Store(key, s)
|
f.shared.Store(key, s)
|
||||||
return s, err
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildMatcher implements DomainMatcherFactory.
|
// BuildMatcher implements DomainMatcherFactory.
|
||||||
func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
|
func (f *CompactMphDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainMatcher, error) {
|
||||||
if len(rules) == 0 {
|
if len(rules) == 0 {
|
||||||
return nil, errors.New("empty domain rule list")
|
return nil, errors.New("empty domain rule list")
|
||||||
}
|
}
|
||||||
compact := &CompactDomainMatcher{
|
compact := new(CompactMphDomainMatcher)
|
||||||
matchers: make([]strmatcher.MatcherSet, 0, len(rules)),
|
|
||||||
values: make([]uint32, 0, len(rules)),
|
|
||||||
}
|
|
||||||
for i, r := range rules {
|
for i, r := range rules {
|
||||||
switch v := r.Value.(type) {
|
switch v := r.Value.(type) {
|
||||||
case *DomainRule_Custom:
|
case *DomainRule_Custom:
|
||||||
@@ -168,8 +149,7 @@ func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainM
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
compact.matchers = append(compact.matchers, m)
|
compact.combiner.Add(m, uint32(i))
|
||||||
compact.values = append(compact.values, uint32(i))
|
|
||||||
default:
|
default:
|
||||||
panic("unknown domain rule type")
|
panic("unknown domain rule type")
|
||||||
}
|
}
|
||||||
@@ -177,37 +157,40 @@ func (f *CompactDomainMatcherFactory) BuildMatcher(rules []*DomainRule) (DomainM
|
|||||||
return compact, nil
|
return compact, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompactDomainMatcher struct {
|
type CompactMphDomainMatcher struct {
|
||||||
custom strmatcher.ValueMatcher
|
custom strmatcher.ValueMatcher
|
||||||
matchers []strmatcher.MatcherSet
|
combiner strmatcher.MphValueMatcherCombiner
|
||||||
values []uint32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match implements DomainMatcher.
|
// Match implements DomainMatcher.
|
||||||
func (c *CompactDomainMatcher) Match(input string) []uint32 {
|
func (c *CompactMphDomainMatcher) Match(input string) []uint32 {
|
||||||
var result []uint32
|
result := c.combiner.Match(input)
|
||||||
if c.custom != nil {
|
if c.custom != nil {
|
||||||
result = append(result, c.custom.Match(input)...)
|
result = append(c.custom.Match(input), result...)
|
||||||
}
|
|
||||||
for i, m := range c.matchers {
|
|
||||||
if m.MatchAny(input) {
|
|
||||||
result = append(result, c.values[i])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchAny implements DomainMatcher.
|
// MatchAny implements DomainMatcher.
|
||||||
func (c *CompactDomainMatcher) MatchAny(input string) bool {
|
func (c *CompactMphDomainMatcher) MatchAny(input string) bool {
|
||||||
if c.custom != nil && c.custom.MatchAny(input) {
|
if c.custom != nil && c.custom.MatchAny(input) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for _, m := range c.matchers {
|
return c.combiner.MatchAny(input)
|
||||||
if m.MatchAny(input) {
|
}
|
||||||
return true
|
|
||||||
|
// loadSiteMatchers calls add with a matcher for every domain of the geosite rule and logs the invalid ones.
|
||||||
|
func loadSiteMatchers(rule *GeoSiteRule, add func(strmatcher.Matcher)) error {
|
||||||
|
i := 0
|
||||||
|
return loadSite(rule.File, rule.Code, rule.Attrs, func(t Domain_Type, value []byte) {
|
||||||
|
m, err := parseDomain(&Domain{Type: t, Value: string(value)})
|
||||||
|
if err != nil {
|
||||||
|
errors.LogError(context.Background(), "ignore invalid geosite entry in ", rule.File, ":", rule.Code, " at index ", i, ", ", err)
|
||||||
|
} else {
|
||||||
|
add(m)
|
||||||
}
|
}
|
||||||
}
|
i++
|
||||||
return false
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseDomain(d *Domain) (strmatcher.Matcher, error) {
|
func parseDomain(d *Domain) (strmatcher.Matcher, error) {
|
||||||
@@ -231,7 +214,7 @@ func parseDomain(d *Domain) (strmatcher.Matcher, error) {
|
|||||||
func newDomainMatcherFactory() DomainMatcherFactory {
|
func newDomainMatcherFactory() DomainMatcherFactory {
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "ios", "android":
|
case "ios", "android":
|
||||||
return &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()}
|
return &CompactMphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()}
|
||||||
default:
|
default:
|
||||||
return &MphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()}
|
return &MphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/geodata/strmatcher"
|
"github.com/xtls/xray-core/common/geodata/strmatcher"
|
||||||
@@ -11,7 +12,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
|
func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
|
||||||
factory := &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()}
|
factory := &CompactMphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()}
|
||||||
matcher, err := factory.BuildMatcher([]*DomainRule{
|
matcher, err := factory.BuildMatcher([]*DomainRule{
|
||||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
|
||||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
|
||||||
@@ -32,7 +33,7 @@ func TestCompactDomainMatcher_PreservesCustomRuleIndices(t *testing.T) {
|
|||||||
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
|
func TestCompactDomainMatcher_PreservesMixedRuleIndices(t *testing.T) {
|
||||||
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||||
|
|
||||||
factory := &CompactDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.LinearAnyMatcher]()}
|
factory := &CompactMphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()}
|
||||||
matcher, err := factory.BuildMatcher([]*DomainRule{
|
matcher, err := factory.BuildMatcher([]*DomainRule{
|
||||||
{Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}},
|
{Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}},
|
||||||
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}},
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "163.com"}}},
|
||||||
@@ -72,3 +73,76 @@ func TestMphDomainMatcher_MatchReturnsDetachedSlice(t *testing.T) {
|
|||||||
t.Fatalf("Match() after caller mutation = %v, want %v", gotAgain, []uint32{0, 1})
|
t.Fatalf("Match() after caller mutation = %v, want %v", gotAgain, []uint32{0, 1})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DNS sorts every Match result in place, so a matcher must never hand out a
|
||||||
|
// slice it keeps, also when only its keyword or regex part matches.
|
||||||
|
func TestDomainMatcher_MatchResultsCanBeSortedConcurrently(t *testing.T) {
|
||||||
|
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
|
||||||
|
|
||||||
|
rules := []*DomainRule{
|
||||||
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "example.com"}}},
|
||||||
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Domain, Value: "example.com"}}},
|
||||||
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Substr, Value: "exam"}}},
|
||||||
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Regex, Value: `^ex.*\.org$`}}},
|
||||||
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Substr, Value: "exam"}}},
|
||||||
|
{Value: &DomainRule_Geosite{Geosite: &GeoSiteRule{File: DefaultGeoSiteDat, Code: "CN"}}},
|
||||||
|
{Value: &DomainRule_Custom{Custom: &Domain{Type: Domain_Full, Value: "only.full.test"}}},
|
||||||
|
}
|
||||||
|
cases := []struct {
|
||||||
|
input string
|
||||||
|
want []uint32
|
||||||
|
}{
|
||||||
|
{"example.com", []uint32{0, 1, 2, 4}},
|
||||||
|
{"www.example.com", []uint32{1, 2, 4}},
|
||||||
|
{"exam.net", []uint32{2, 4}}, // keyword part only
|
||||||
|
{"example.org", []uint32{2, 3, 4}},
|
||||||
|
{"163.com", []uint32{5}},
|
||||||
|
{"www.163.com", []uint32{5}},
|
||||||
|
{"only.full.test", []uint32{6}}, // full part only
|
||||||
|
{"nomatch.test", nil},
|
||||||
|
}
|
||||||
|
factories := map[string]DomainMatcherFactory{
|
||||||
|
"mph": &MphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()},
|
||||||
|
"compact": &CompactMphDomainMatcherFactory{shared: utils.NewWeakCacheMap[string, strmatcher.MphValueMatcher]()},
|
||||||
|
}
|
||||||
|
for name, factory := range factories {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
matcher, err := factory.BuildMatcher(rules)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildMatcher() failed: %v", err)
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
got := matcher.Match(c.input)
|
||||||
|
if sorted := slices.Sorted(slices.Values(got)); !slices.Equal(sorted, c.want) {
|
||||||
|
t.Fatalf("Match(%q) = %v, want %v", c.input, sorted, c.want)
|
||||||
|
}
|
||||||
|
got = got[:cap(got)]
|
||||||
|
for j := range got {
|
||||||
|
got[j] = ^uint32(0)
|
||||||
|
}
|
||||||
|
if again := slices.Sorted(slices.Values(matcher.Match(c.input))); !slices.Equal(again, c.want) {
|
||||||
|
t.Fatalf("Match(%q) after caller mutation = %v, want %v", c.input, again, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for range 8 {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for range 500 {
|
||||||
|
for _, c := range cases {
|
||||||
|
got := matcher.Match(c.input)
|
||||||
|
slices.Sort(got)
|
||||||
|
if !slices.Equal(got, c.want) {
|
||||||
|
t.Errorf("Match(%q) = %v, want %v", c.input, got, c.want)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+212
-61
@@ -5,11 +5,14 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
"io"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/platform/filesystem"
|
"github.com/xtls/xray-core/common/platform/filesystem"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/encoding/protowire"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,17 +55,56 @@ func loadIP(file, code string) ([]*CIDR, error) {
|
|||||||
return geoip.Cidr, nil
|
return geoip.Cidr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSite(file, code string) ([]*Domain, error) {
|
// loadSite calls fn, in file order, with the type and value of every domain of the geosite code
|
||||||
bs, err := loadFile(file, code)
|
// that has all the "@"-separated attrs. It decodes the entry while reading the file instead of
|
||||||
|
// unmarshalling it into a []*Domain, so value is only valid during fn.
|
||||||
|
func loadSite(file, code, attrs string, fn func(Domain_Type, []byte)) error {
|
||||||
|
runtime.GC() // peak mem
|
||||||
|
r, err := filesystem.OpenAsset(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return errors.New("failed to open ", file).Base(err)
|
||||||
}
|
}
|
||||||
defer runtime.GC() // peak mem
|
defer r.Close()
|
||||||
var geosite GeoSite
|
br := bufio.NewReaderSize(r, 64*1024)
|
||||||
if err := proto.Unmarshal(bs, &geosite); err != nil {
|
n, err := seek(br, []byte(code))
|
||||||
return nil, errors.New("error unmarshal Site in ", file, ":", code).Base(err)
|
if err != nil {
|
||||||
|
return errors.New("failed to load code ", code, " from ", file).Base(err)
|
||||||
}
|
}
|
||||||
return geosite.Domain, nil
|
loadErr := func(err error) error {
|
||||||
|
if err == io.EOF {
|
||||||
|
err = io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
return errors.New("failed to load code ", code, " from ", file).Base(err)
|
||||||
|
}
|
||||||
|
unmarshalErr := func(err error) error {
|
||||||
|
return errors.New("error unmarshal Site in ", file, ":", code).Base(err)
|
||||||
|
}
|
||||||
|
d := newSiteDecoder(attrs, fn)
|
||||||
|
for n > 0 {
|
||||||
|
w, err := br.Peek(min(n, br.Size()))
|
||||||
|
if err != nil {
|
||||||
|
return loadErr(err)
|
||||||
|
}
|
||||||
|
used, err := d.decode(w, len(w) < n)
|
||||||
|
if err != nil {
|
||||||
|
return unmarshalErr(err)
|
||||||
|
}
|
||||||
|
if used == 0 {
|
||||||
|
break // a field longer than the buffer
|
||||||
|
}
|
||||||
|
br.Discard(used)
|
||||||
|
n -= used
|
||||||
|
}
|
||||||
|
if n > 0 {
|
||||||
|
w := make([]byte, n)
|
||||||
|
if _, err := io.ReadFull(br, w); err != nil {
|
||||||
|
return loadErr(err)
|
||||||
|
}
|
||||||
|
if _, err := d.decode(w, false); err != nil {
|
||||||
|
return unmarshalErr(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeVarint(br *bufio.Reader) (uint64, error) {
|
func decodeVarint(br *bufio.Reader) (uint64, error) {
|
||||||
@@ -82,68 +124,63 @@ func decodeVarint(br *bufio.Reader) (uint64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func find(r io.Reader, code []byte, readBody bool) ([]byte, error) {
|
func find(r io.Reader, code []byte, readBody bool) ([]byte, error) {
|
||||||
|
br := bufio.NewReaderSize(r, 64*1024)
|
||||||
|
bodyL, err := seek(br, code)
|
||||||
|
if err != nil || !readBody {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out := make([]byte, bodyL)
|
||||||
|
if _, err := io.ReadFull(br, out); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// seek advances br to the body of the entry for code and returns the body length.
|
||||||
|
func seek(br *bufio.Reader, code []byte) (int, error) {
|
||||||
codeL := len(code)
|
codeL := len(code)
|
||||||
if codeL == 0 {
|
if codeL == 0 {
|
||||||
return nil, errors.New("empty code")
|
return 0, errors.New("empty code")
|
||||||
}
|
}
|
||||||
|
|
||||||
br := bufio.NewReaderSize(r, 64*1024)
|
|
||||||
need := 2 + codeL // TODO: if code too long
|
need := 2 + codeL // TODO: if code too long
|
||||||
prefixBuf := make([]byte, need)
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if _, err := br.ReadByte(); err != nil {
|
if _, err := br.ReadByte(); err != nil {
|
||||||
return nil, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
x, err := decodeVarint(br)
|
x, err := decodeVarint(br)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return 0, err
|
||||||
}
|
}
|
||||||
bodyL := int(x)
|
bodyL := int(x)
|
||||||
if bodyL <= 0 {
|
if bodyL <= 0 {
|
||||||
return nil, errors.New("invalid body length: ", bodyL)
|
return 0, errors.New("invalid body length: ", bodyL)
|
||||||
}
|
}
|
||||||
|
|
||||||
prefixL := bodyL
|
// Peek no more than the buffer holds: a code longer than the buffer cannot match a single
|
||||||
if prefixL > need {
|
// length byte anyway, so a short peek only skips it, as base find (io.ReadFull) does.
|
||||||
prefixL = need
|
prefix, err := br.Peek(min(bodyL, need, br.Size()))
|
||||||
}
|
if err != nil {
|
||||||
prefix := prefixBuf[:prefixL]
|
if err == io.EOF && len(prefix) > 0 {
|
||||||
if _, err := io.ReadFull(br, prefix); err != nil {
|
err = io.ErrUnexpectedEOF // as io.ReadFull
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
match := false
|
|
||||||
if bodyL >= need {
|
|
||||||
if int(prefix[1]) == codeL && bytes.Equal(prefix[2:need], code) {
|
|
||||||
if !readBody {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
match = true
|
|
||||||
}
|
}
|
||||||
|
return 0, err
|
||||||
}
|
}
|
||||||
|
if bodyL >= need && len(prefix) >= need && int(prefix[1]) == codeL && bytes.Equal(prefix[2:], code) {
|
||||||
remain := bodyL - prefixL
|
return bodyL, nil
|
||||||
if match {
|
|
||||||
out := make([]byte, bodyL)
|
|
||||||
copy(out, prefix)
|
|
||||||
if remain > 0 {
|
|
||||||
if _, err := io.ReadFull(br, out[prefixL:]); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
}
|
||||||
|
if _, err := br.Discard(bodyL); err != nil {
|
||||||
if remain > 0 {
|
return 0, err
|
||||||
if _, err := br.Discard(remain); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AttributeMatcher, HasAttrMatcher, AllAttrsMatcher and NewAllAttrsMatcher are the exported
|
||||||
|
// attribute helpers that have been part of this package's API since #5814. The streaming loader
|
||||||
|
// above filters attributes itself without building a *Domain, so it does not use them, but they
|
||||||
|
// are kept for external callers. Their behaviour is unchanged.
|
||||||
|
|
||||||
type AttributeMatcher interface {
|
type AttributeMatcher interface {
|
||||||
Match(*Domain) bool
|
Match(*Domain) bool
|
||||||
}
|
}
|
||||||
@@ -185,23 +222,137 @@ func NewAllAttrsMatcher(attrs string) AttributeMatcher {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSiteWithAttrs(file, code, attrs string) ([]*Domain, error) {
|
var errInvalidUTF8 = errors.New("string field contains invalid UTF-8")
|
||||||
domains, err := loadSite(file, code)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
matcher := NewAllAttrsMatcher(attrs)
|
type siteDecoder struct {
|
||||||
if matcher == nil {
|
want []string
|
||||||
return domains, nil
|
has []bool
|
||||||
}
|
fn func(Domain_Type, []byte)
|
||||||
|
}
|
||||||
|
|
||||||
filtered := make([]*Domain, 0, len(domains))
|
func newSiteDecoder(attrs string, fn func(Domain_Type, []byte)) *siteDecoder {
|
||||||
for _, d := range domains {
|
d := &siteDecoder{fn: fn}
|
||||||
if matcher.Match(d) {
|
if attrs != "" {
|
||||||
filtered = append(filtered, d)
|
d.want = strings.Split(attrs, "@")
|
||||||
|
d.has = make([]bool, len(d.want))
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode walks the whole fields at the start of b, a part of an encoded GeoSite (see geodat.proto),
|
||||||
|
// calls fn for every domain that has all attrs and returns how many bytes it used. A field cut off
|
||||||
|
// by the end of b is an error unless more is set. It accepts and rejects what proto.Unmarshal does.
|
||||||
|
func (d *siteDecoder) decode(b []byte, more bool) (int, error) {
|
||||||
|
used := 0
|
||||||
|
for used < len(b) {
|
||||||
|
f, n, err := consumeField(b[used:])
|
||||||
|
if err == io.ErrUnexpectedEOF && more {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return used, err
|
||||||
|
}
|
||||||
|
used += n
|
||||||
|
if f.typ != protowire.BytesType {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch f.num {
|
||||||
|
case 1: // code
|
||||||
|
if !utf8.Valid(f.v) {
|
||||||
|
return used, errInvalidUTF8
|
||||||
|
}
|
||||||
|
case 2: // domain
|
||||||
|
t, value, err := decodeDomain(f.v, d.want, d.has)
|
||||||
|
if err != nil {
|
||||||
|
return used, err
|
||||||
|
}
|
||||||
|
if !slices.Contains(d.has, false) {
|
||||||
|
d.fn(t, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return used, nil
|
||||||
|
}
|
||||||
|
|
||||||
return filtered, nil
|
// decodeDomain decodes an encoded Domain and sets has[i] if one of its attributes has the key want[i].
|
||||||
|
func decodeDomain(b []byte, want []string, has []bool) (t Domain_Type, value []byte, err error) {
|
||||||
|
clear(has)
|
||||||
|
for len(b) > 0 {
|
||||||
|
f, n, err := consumeField(b)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
b = b[n:]
|
||||||
|
switch {
|
||||||
|
case f.num == 1 && f.typ == protowire.VarintType: // type
|
||||||
|
t = Domain_Type(f.x)
|
||||||
|
case f.num == 2 && f.typ == protowire.BytesType: // value
|
||||||
|
if !utf8.Valid(f.v) {
|
||||||
|
return 0, nil, errInvalidUTF8
|
||||||
|
}
|
||||||
|
value = f.v
|
||||||
|
case f.num == 3 && f.typ == protowire.BytesType: // attribute
|
||||||
|
key, err := decodeAttributeKey(f.v)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
for i, w := range want {
|
||||||
|
if string(key) == w {
|
||||||
|
has[i] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return t, value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeAttributeKey returns the key of an encoded Domain.Attribute.
|
||||||
|
func decodeAttributeKey(b []byte) ([]byte, error) {
|
||||||
|
var key []byte
|
||||||
|
for len(b) > 0 {
|
||||||
|
f, n, err := consumeField(b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
b = b[n:]
|
||||||
|
if f.num == 1 && f.typ == protowire.BytesType {
|
||||||
|
if !utf8.Valid(f.v) {
|
||||||
|
return nil, errInvalidUTF8
|
||||||
|
}
|
||||||
|
key = f.v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type protoField struct {
|
||||||
|
num protowire.Number
|
||||||
|
typ protowire.Type
|
||||||
|
v []byte // payload of a length-delimited field
|
||||||
|
x uint64 // value of a varint field
|
||||||
|
}
|
||||||
|
|
||||||
|
// consumeField parses the first field of an encoded message and returns it with its length.
|
||||||
|
func consumeField(b []byte) (protoField, int, error) {
|
||||||
|
num, typ, n := protowire.ConsumeTag(b)
|
||||||
|
if n < 0 {
|
||||||
|
return protoField{}, 0, protowire.ParseError(n)
|
||||||
|
}
|
||||||
|
if num > protowire.MaxValidNumber {
|
||||||
|
return protoField{}, 0, errors.New("invalid field number ", num)
|
||||||
|
}
|
||||||
|
f := protoField{num: num, typ: typ}
|
||||||
|
var m int
|
||||||
|
switch typ {
|
||||||
|
case protowire.BytesType:
|
||||||
|
f.v, m = protowire.ConsumeBytes(b[n:])
|
||||||
|
case protowire.VarintType:
|
||||||
|
f.x, m = protowire.ConsumeVarint(b[n:])
|
||||||
|
default:
|
||||||
|
m = protowire.ConsumeFieldValue(num, typ, b[n:])
|
||||||
|
}
|
||||||
|
if m < 0 {
|
||||||
|
return protoField{}, 0, protowire.ParseError(m)
|
||||||
|
}
|
||||||
|
return f, n + m, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
package geodata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/encoding/protowire"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type siteEntry struct {
|
||||||
|
Type Domain_Type
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalSite is what loadSite used to do: proto.Unmarshal, then keep the domains that have all attrs.
|
||||||
|
func unmarshalSite(b []byte, attrs string) ([]siteEntry, error) {
|
||||||
|
var site GeoSite
|
||||||
|
if err := proto.Unmarshal(b, &site); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var entries []siteEntry
|
||||||
|
for _, d := range site.Domain {
|
||||||
|
ok := true
|
||||||
|
for _, key := range strings.Split(attrs, "@") {
|
||||||
|
ok = ok && (attrs == "" || slices.ContainsFunc(d.Attribute, func(a *Domain_Attribute) bool { return a.Key == key }))
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
entries = append(entries, siteEntry{d.Type, d.Value})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkDecodeSite(t *testing.T, name string, b []byte, attrs string) {
|
||||||
|
t.Helper()
|
||||||
|
want, wantErr := unmarshalSite(b, attrs)
|
||||||
|
var got []siteEntry
|
||||||
|
_, err := newSiteDecoder(attrs, func(typ Domain_Type, value []byte) {
|
||||||
|
got = append(got, siteEntry{typ, string(value)})
|
||||||
|
}).decode(b, false)
|
||||||
|
if (err == nil) != (wantErr == nil) {
|
||||||
|
t.Fatalf("%s@%s: error %v, proto.Unmarshal: %v", name, attrs, err, wantErr)
|
||||||
|
}
|
||||||
|
if err == nil && !slices.Equal(got, want) {
|
||||||
|
t.Fatalf("%s@%s: got %v, want %v", name, attrs, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeSiteMatchesUnmarshal(t *testing.T) {
|
||||||
|
bs, err := os.ReadFile(filepath.Join("..", "..", "resources", DefaultGeoSiteDat))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for len(bs) > 0 {
|
||||||
|
num, typ, n := protowire.ConsumeTag(bs)
|
||||||
|
if n < 0 || num != 1 || typ != protowire.BytesType {
|
||||||
|
t.Fatal("unexpected GeoSiteList field")
|
||||||
|
}
|
||||||
|
entry, m := protowire.ConsumeBytes(bs[n:])
|
||||||
|
if m < 0 {
|
||||||
|
t.Fatal(protowire.ParseError(m))
|
||||||
|
}
|
||||||
|
bs = bs[n+m:]
|
||||||
|
|
||||||
|
var site GeoSite
|
||||||
|
if err := proto.Unmarshal(entry, &site); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
queries := []string{"", "none"}
|
||||||
|
for _, d := range site.Domain {
|
||||||
|
for _, a := range d.Attribute {
|
||||||
|
if !slices.Contains(queries, a.Key) {
|
||||||
|
queries = append(queries, a.Key, a.Key+"@none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, attrs := range queries {
|
||||||
|
checkDecodeSite(t, site.Code, entry, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeSiteUnusualEncodings(t *testing.T) {
|
||||||
|
field := func(num protowire.Number, v []byte) []byte {
|
||||||
|
return protowire.AppendBytes(protowire.AppendTag(nil, num, protowire.BytesType), v)
|
||||||
|
}
|
||||||
|
typ := func(v Domain_Type) []byte {
|
||||||
|
return protowire.AppendVarint(protowire.AppendTag(nil, 1, protowire.VarintType), uint64(v))
|
||||||
|
}
|
||||||
|
value := func(s string) []byte { return field(2, []byte(s)) }
|
||||||
|
attr := func(keys ...string) []byte {
|
||||||
|
var b []byte
|
||||||
|
for _, k := range keys {
|
||||||
|
b = append(b, field(1, []byte(k))...)
|
||||||
|
}
|
||||||
|
return field(3, b)
|
||||||
|
}
|
||||||
|
domain := func(fields ...[]byte) []byte { return field(2, slices.Concat(fields...)) }
|
||||||
|
unknown := protowire.AppendFixed32(protowire.AppendTag(nil, 9, protowire.Fixed32Type), 1)
|
||||||
|
|
||||||
|
for name, b := range map[string][]byte{
|
||||||
|
"unknown field": domain(typ(Domain_Full), unknown, value("example.com")),
|
||||||
|
"repeated value": domain(value("a.com"), typ(Domain_Full), value("b.com")),
|
||||||
|
"repeated type": domain(typ(Domain_Full), value("a.com"), typ(Domain_Regex)),
|
||||||
|
"repeated key": domain(value("a.com"), attr("cn", "ads")),
|
||||||
|
"type as bytes": domain(field(1, []byte("x")), value("a.com")),
|
||||||
|
"no value": domain(typ(Domain_Domain), attr("cn")),
|
||||||
|
"truncated": domain(typ(Domain_Full), value("example.com"))[:10],
|
||||||
|
"invalid utf8": domain(value("example.\xff")),
|
||||||
|
"invalid key": domain(value("a.com"), attr("\xff")),
|
||||||
|
"bad field": protowire.AppendVarint(protowire.AppendTag(nil, protowire.MaxValidNumber+1, protowire.VarintType), 1),
|
||||||
|
"stray end group": protowire.AppendTag(nil, 5, protowire.EndGroupType),
|
||||||
|
} {
|
||||||
|
for _, attrs := range []string{"", "cn", "ads", "cn@ads"} {
|
||||||
|
checkDecodeSite(t, name, b, attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLoadSiteReadsInPieces covers what real lists never do: an entry far longer than the read
|
||||||
|
// buffer, with a field longer than the buffer in the middle, and a file cut short.
|
||||||
|
func TestLoadSiteReadsInPieces(t *testing.T) {
|
||||||
|
site := &GeoSite{Code: "BIG"}
|
||||||
|
for i := range 5000 {
|
||||||
|
d := &Domain{Type: Domain_Domain, Value: strings.Repeat("x", i%40) + ".example.com"}
|
||||||
|
if i%3 == 0 {
|
||||||
|
d.Attribute = []*Domain_Attribute{{Key: "cn"}}
|
||||||
|
}
|
||||||
|
if i == 2500 {
|
||||||
|
d = &Domain{Type: Domain_Regex, Value: strings.Repeat("a", 100_000)}
|
||||||
|
}
|
||||||
|
site.Domain = append(site.Domain, d)
|
||||||
|
}
|
||||||
|
list := &GeoSiteList{Entry: []*GeoSite{{Code: "SMALL", Domain: []*Domain{{Type: Domain_Full, Value: "a.com"}}}, site}}
|
||||||
|
bs, err := proto.Marshal(list)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
entry, err := proto.Marshal(site)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Setenv("xray.location.asset", dir)
|
||||||
|
write := func(b []byte) {
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "big.dat"), b, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, attrs := range []string{"", "cn"} {
|
||||||
|
want, _ := unmarshalSite(entry, attrs)
|
||||||
|
var got []siteEntry
|
||||||
|
write(bs)
|
||||||
|
err := loadSite("big.dat", "BIG", attrs, func(typ Domain_Type, value []byte) {
|
||||||
|
got = append(got, siteEntry{typ, string(value)})
|
||||||
|
})
|
||||||
|
if err != nil || !slices.Equal(got, want) {
|
||||||
|
t.Fatalf("attrs %q: %d entries, want %d, error %v", attrs, len(got), len(want), err)
|
||||||
|
}
|
||||||
|
for _, cut := range []int{30_000, len(bs) - 150_000, len(bs) - 1} {
|
||||||
|
write(bs[:cut])
|
||||||
|
if err := loadSite("big.dat", "BIG", attrs, func(Domain_Type, []byte) {}); err == nil {
|
||||||
|
t.Fatalf("file cut at %d of %d: no error", cut, len(bs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// oneEntryGeoSiteFile wraps an encoded GeoSite as a one-entry GeoSiteList, the file loadSite reads.
|
||||||
|
func oneEntryGeoSiteFile(entry []byte) []byte {
|
||||||
|
return protowire.AppendBytes(protowire.AppendTag(nil, 1, protowire.BytesType), entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLoadSiteWindowedMatchesSingleShot checks that the windowed reader in loadSite (its Peek/Discard
|
||||||
|
// loop, the more-break when a field is cut by a window edge, the used==0 fallback for a field longer
|
||||||
|
// than the buffer, and the tail path) reaches exactly the same result as decoding the whole entry at
|
||||||
|
// once, for a category several 64 KiB windows long, valid and then mutated near a window edge and
|
||||||
|
// early in the file: same error-or-not, and the same emitted (type, value) sequence when both accept.
|
||||||
|
func TestLoadSiteWindowedMatchesSingleShot(t *testing.T) {
|
||||||
|
const window = 64 * 1024
|
||||||
|
site := &GeoSite{Code: "BIG"}
|
||||||
|
for i := range 12000 { // ~250 KiB, four windows
|
||||||
|
d := &Domain{Type: Domain_Domain, Value: fmt.Sprintf("host%d.%s.example.com", i, strings.Repeat("y", i%30))}
|
||||||
|
if i%3 == 0 {
|
||||||
|
d.Attribute = []*Domain_Attribute{{Key: "cn"}}
|
||||||
|
}
|
||||||
|
site.Domain = append(site.Domain, d)
|
||||||
|
}
|
||||||
|
// a field longer than the buffer, straddling the third window, to force the used==0 fallback
|
||||||
|
site.Domain = slices.Insert(site.Domain, 8000, &Domain{Type: Domain_Regex, Value: strings.Repeat("a", 90_000)})
|
||||||
|
entry, err := proto.Marshal(site)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Setenv("xray.location.asset", dir)
|
||||||
|
|
||||||
|
// mutations of the encoded entry: unchanged, a byte flipped at several offsets (early windows and
|
||||||
|
// either side of a window edge), and truncations at the same places.
|
||||||
|
type mut struct {
|
||||||
|
name string
|
||||||
|
make func([]byte) []byte
|
||||||
|
}
|
||||||
|
muts := []mut{{"valid", func(b []byte) []byte { return b }}}
|
||||||
|
for _, off := range []int{3, 40, 4000, window - 1, window, window + 1, 2*window - 2, 2 * window} {
|
||||||
|
if off < len(entry) {
|
||||||
|
off := off
|
||||||
|
muts = append(muts, mut{fmt.Sprintf("flip@%d", off), func(b []byte) []byte {
|
||||||
|
c := slices.Clone(b)
|
||||||
|
c[off] ^= 0xff
|
||||||
|
return c
|
||||||
|
}})
|
||||||
|
muts = append(muts, mut{fmt.Sprintf("cut@%d", off), func(b []byte) []byte { return slices.Clone(b[:off]) }})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, attrs := range []string{"", "cn"} {
|
||||||
|
for _, m := range muts {
|
||||||
|
e := m.make(entry)
|
||||||
|
// single-shot reference: decode the whole entry in one call
|
||||||
|
var want []siteEntry
|
||||||
|
_, wantErr := newSiteDecoder(attrs, func(typ Domain_Type, value []byte) {
|
||||||
|
want = append(want, siteEntry{typ, string(value)})
|
||||||
|
}).decode(e, false)
|
||||||
|
// windowed: loadSite reads the file 64 KiB at a time
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "w.dat"), oneEntryGeoSiteFile(e), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var got []siteEntry
|
||||||
|
gotErr := loadSite("w.dat", "BIG", attrs, func(typ Domain_Type, value []byte) {
|
||||||
|
got = append(got, siteEntry{typ, string(value)})
|
||||||
|
})
|
||||||
|
if (gotErr == nil) != (wantErr == nil) {
|
||||||
|
t.Fatalf("%s attrs=%q: windowed err %v, single-shot err %v", m.name, attrs, gotErr, wantErr)
|
||||||
|
}
|
||||||
|
if gotErr == nil && !slices.Equal(got, want) {
|
||||||
|
t.Fatalf("%s attrs=%q: windowed got %d entries, single-shot %d", m.name, attrs, len(got), len(want))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLoadSiteLongCode covers a geosite entry whose code is longer than the 64 KiB read buffer. seek
|
||||||
|
// must skip it (find compares a single length byte, so it never matches such a code) and still find a
|
||||||
|
// later entry, and looking the long code up must fail cleanly, like a missing code, not panic.
|
||||||
|
func TestLoadSiteLongCode(t *testing.T) {
|
||||||
|
longCode := strings.Repeat("Z", 70000)
|
||||||
|
list := &GeoSiteList{Entry: []*GeoSite{
|
||||||
|
{Code: "FIRST", Domain: []*Domain{{Type: Domain_Full, Value: "first.com"}}},
|
||||||
|
{Code: longCode, Domain: []*Domain{{Type: Domain_Full, Value: "huge.com"}}},
|
||||||
|
{Code: "AFTER", Domain: []*Domain{{Type: Domain_Domain, Value: "after.com"}}},
|
||||||
|
}}
|
||||||
|
bs, err := proto.Marshal(list)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
t.Setenv("xray.location.asset", dir)
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "lc.dat"), bs, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
collect := func(code string) ([]siteEntry, error) {
|
||||||
|
var got []siteEntry
|
||||||
|
err := loadSite("lc.dat", code, "", func(typ Domain_Type, value []byte) {
|
||||||
|
got = append(got, siteEntry{typ, string(value)})
|
||||||
|
})
|
||||||
|
return got, err
|
||||||
|
}
|
||||||
|
if got, err := collect("FIRST"); err != nil || !slices.Equal(got, []siteEntry{{Domain_Full, "first.com"}}) {
|
||||||
|
t.Fatalf("FIRST: %v %v", got, err)
|
||||||
|
}
|
||||||
|
if got, err := collect("AFTER"); err != nil || !slices.Equal(got, []siteEntry{{Domain_Domain, "after.com"}}) {
|
||||||
|
t.Fatalf("AFTER (past the oversized entry): %v %v", got, err)
|
||||||
|
}
|
||||||
|
if _, err := collect(longCode); err == nil {
|
||||||
|
t.Fatal("oversized code: expected a not-found error, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package strmatcher_test
|
package strmatcher_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -72,6 +73,64 @@ func BenchmarkSubstrMatcher(b *testing.B) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkRegexMatcher(b *testing.B) {
|
||||||
|
patterns := []string{ // taken from geosite
|
||||||
|
`(^|\.)91porn\.(best|com|cool|fun|group|party|plus|site|tw|work)$`,
|
||||||
|
`(^|\.)91porn[0-9]{3}\.me$`,
|
||||||
|
`(^|\.)apiproxy-device-prod-nlb-.+\.amazonaws\.com$`,
|
||||||
|
`(^|\.)dualstack\.apiproxy-.+\.amazonaws\.com$`,
|
||||||
|
`(^|\.)aqdk[0-9]{3}\.com$`,
|
||||||
|
`(^|\.)bilibili3(0[1-9]|1[0-2])\.xyz$`,
|
||||||
|
`(^|\.)byyum([3589]|2[235689]|3[34]|4[1-9]|5[1-79]|6[0134679])?\.com$`,
|
||||||
|
`(^|\.)fiftymvapi\..+$`,
|
||||||
|
`(^|\.)gossipfuli[0-9]{3,4}\.xyz$`,
|
||||||
|
`(^|\.)kpkuang\.(bond|fun|info|one|us)$`,
|
||||||
|
`(^|\.)rule34\.(asia|us|world|xxx|xyz)$`,
|
||||||
|
`(^|\.)[a-z][1-9][0-9][a-z]\.com$`,
|
||||||
|
`.+\.awsdns-[0-9][0-9]\.(co\.uk|com|net|org)$`,
|
||||||
|
`.+\.dkr\.ecr\.[^\.]+\.amazonaws\.com$`,
|
||||||
|
`^(.+\.)*zh\.okaapps\.com$`,
|
||||||
|
`^cdn\d-epicgames-\d+\.file\.myqcloud\.com$`,
|
||||||
|
`^chatgpt-async-webps-prod-\S+-\d+\.webpubsub\.azure\.com$`,
|
||||||
|
`^r+[0-9]+(---|\.)sn-(2x3|ni5|j5o)\w{5}\.googlevideo\.com$`,
|
||||||
|
`^speed\.(coe|open)\.ad\.[a-z]{2,6}\.prod\.hosts\.ooklaserver\.net$`,
|
||||||
|
`javdb\d+\.com$`,
|
||||||
|
}
|
||||||
|
domains := []string{
|
||||||
|
"www.google.com", "rr3---sn-4g5edndy.googlevideo.com", "r1---sn-2x3abcde.googlevideo.com", "i.ytimg.com",
|
||||||
|
"graph.facebook.com", "api.twitter.com", "www.baidu.com", "github.com", "objects.githubusercontent.com",
|
||||||
|
"login.microsoftonline.com", "e1234.dscb.akamaiedge.net", "d1a2b3c4d5e6f7.cloudfront.net",
|
||||||
|
"s3.us-east-1.amazonaws.com", "123456789012.dkr.ecr.us-east-1.amazonaws.com", "www.wikipedia.org",
|
||||||
|
"discord.com", "telegram.org", "store.steampowered.com", "www.91porn.com", "ns-1234.awsdns-12.org",
|
||||||
|
}
|
||||||
|
bench := func(b *testing.B, ctor func(pattern string) func(string) bool) {
|
||||||
|
var matchers []func(string) bool
|
||||||
|
for _, p := range patterns {
|
||||||
|
matchers = append(matchers, ctor(p))
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
for _, d := range domains {
|
||||||
|
for _, match := range matchers {
|
||||||
|
_ = match(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.Run("regexp", func(b *testing.B) {
|
||||||
|
bench(b, func(pattern string) func(string) bool {
|
||||||
|
return regexp.MustCompile(pattern).MatchString
|
||||||
|
})
|
||||||
|
})
|
||||||
|
b.Run("prefilter", func(b *testing.B) {
|
||||||
|
bench(b, func(pattern string) func(string) bool {
|
||||||
|
m, err := Regex.New(pattern)
|
||||||
|
common.Must(err)
|
||||||
|
return m.Match
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Utility functions for benchmark
|
// Utility functions for benchmark
|
||||||
|
|
||||||
func benchmarkMatcherType(b *testing.B, t Type, ctor func() MatcherGroup) {
|
func benchmarkMatcherType(b *testing.B, t Type, ctor func() MatcherGroup) {
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ func (g *MphIndexMatcher) Add(matcher Matcher) uint32 {
|
|||||||
func (g *MphIndexMatcher) Build() error {
|
func (g *MphIndexMatcher) Build() error {
|
||||||
if g.mph != nil {
|
if g.mph != nil {
|
||||||
runtime.GC() // peak mem
|
runtime.GC() // peak mem
|
||||||
g.mph.Build()
|
if err := g.mph.Build(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
runtime.GC() // peak mem
|
runtime.GC() // peak mem
|
||||||
if g.ac != nil {
|
if g.ac != nil {
|
||||||
@@ -64,23 +66,17 @@ func (g *MphIndexMatcher) Build() error {
|
|||||||
|
|
||||||
// Match implements IndexMatcher.Match.
|
// Match implements IndexMatcher.Match.
|
||||||
func (g *MphIndexMatcher) Match(input string) []uint32 {
|
func (g *MphIndexMatcher) Match(input string) []uint32 {
|
||||||
result := make([][]uint32, 0, 5)
|
var result []uint32
|
||||||
if g.mph != nil {
|
if g.mph != nil {
|
||||||
if matches := g.mph.Match(input); len(matches) > 0 {
|
result = g.mph.Match(input) // a new slice, returned without another copy
|
||||||
result = append(result, matches)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if g.ac != nil {
|
if g.ac != nil {
|
||||||
if matches := g.ac.Match(input); len(matches) > 0 {
|
result = append(result, g.ac.Match(input)...)
|
||||||
result = append(result, matches)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if g.regex != nil {
|
if g.regex != nil {
|
||||||
if matches := g.regex.Match(input); len(matches) > 0 {
|
result = append(result, g.regex.Match(input)...)
|
||||||
result = append(result, matches)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return CompositeMatches(result)
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchAny implements IndexMatcher.MatchAny.
|
// MatchAny implements IndexMatcher.MatchAny.
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ func TestMphIndexMatcher(t *testing.T) {
|
|||||||
Input: "example.com",
|
Input: "example.com",
|
||||||
Output: []uint32{10, 4},
|
Output: []uint32{10, 4},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Input: "apis.org",
|
||||||
|
Output: []uint32{2, 6},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
matcherGroup := NewMphIndexMatcher()
|
matcherGroup := NewMphIndexMatcher()
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
@@ -87,8 +91,13 @@ func TestMphIndexMatcher(t *testing.T) {
|
|||||||
}
|
}
|
||||||
matcherGroup.Build()
|
matcherGroup.Build()
|
||||||
for _, test := range cases {
|
for _, test := range cases {
|
||||||
if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) {
|
m := matcherGroup.Match(test.Input)
|
||||||
|
if !reflect.DeepEqual(m, test.Output) {
|
||||||
t.Error("unexpected output: ", m, " for test case ", test)
|
t.Error("unexpected output: ", m, " for test case ", test)
|
||||||
}
|
}
|
||||||
|
clear(m) // the caller owns the result, so this must not change the next one
|
||||||
|
if m := matcherGroup.Match(test.Input); !reflect.DeepEqual(m, test.Output) {
|
||||||
|
t.Error("unexpected output after clearing the previous one: ", m, " for test case ", test)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,198 +1,440 @@
|
|||||||
package strmatcher
|
package strmatcher
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/bits"
|
"bytes"
|
||||||
"runtime"
|
"cmp"
|
||||||
"sort"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrimeRK is the prime base used in Rabin-Karp algorithm.
|
// Flags of a level1 slot, stored above the record offset.
|
||||||
const PrimeRK = 16777619
|
|
||||||
|
|
||||||
// RollingHash calculates the rolling murmurHash of given string based on a provided suffix hash.
|
|
||||||
func RollingHash(hash uint32, input string) uint32 {
|
|
||||||
for i := len(input) - 1; i >= 0; i-- {
|
|
||||||
hash = hash*PrimeRK + uint32(input[i])
|
|
||||||
}
|
|
||||||
return hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// MemHash is the hash function used by go map, it utilizes available hardware instructions(behaves
|
|
||||||
// as aeshash if aes instruction is available).
|
|
||||||
// With different seed, each MemHash<seed> performs as distinct hash functions.
|
|
||||||
func MemHash(seed uint32, input string) uint32 {
|
|
||||||
return uint32(strhash(unsafe.Pointer(&input), uintptr(seed))) // nosemgrep
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
mphMatchTypeCount = 2 // Full and Domain
|
mphDomain = 1 << 31 // matches the pattern and its subdomains
|
||||||
|
mphFull = 1 << 30 // matches the pattern only
|
||||||
|
mphParent = 1 << 29 // matches subdomains only, from a pattern with a leading dot
|
||||||
|
mphOffMask = mphParent - 1
|
||||||
)
|
)
|
||||||
|
|
||||||
type mphRuleInfo struct {
|
// Kinds of an added pattern, indexes of mphKinds.
|
||||||
rollingHash uint32
|
const (
|
||||||
matchers [mphMatchTypeCount][]uint32
|
mphKindFull = iota
|
||||||
|
mphKindParent
|
||||||
|
mphKindDomain
|
||||||
|
)
|
||||||
|
|
||||||
|
// mphKinds are the slot flags in the order Match reports their values.
|
||||||
|
var mphKinds = [...]uint32{mphFull, mphParent, mphDomain}
|
||||||
|
|
||||||
|
// mphMultipliers are odd multipliers for the suffix hash. Build moves to the next one if two patterns collide.
|
||||||
|
var mphMultipliers = [...]uint64{0x9e3779b97f4a7c15, 0xc2b2ae3d27d4eb4f, 0x165667b19e3779f9, 0x27d4eb2f165667c5}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errMphCollision = errors.New("strmatcher: suffix hash collision in MphMatcherGroup")
|
||||||
|
errMphBuilt = errors.New("strmatcher: MphMatcherGroup is already built")
|
||||||
|
)
|
||||||
|
|
||||||
|
type mphEntry struct {
|
||||||
|
off uint32 // pattern start in buf
|
||||||
|
value uint32
|
||||||
|
n uint32 // pattern length
|
||||||
|
kind uint8
|
||||||
}
|
}
|
||||||
|
|
||||||
// MphMatcherGroup is an implementation of MatcherGroup.
|
// MphMatcherGroup is an implementation of MatcherGroup for Full and Domain matchers.
|
||||||
// It implements Rabin-Karp algorithm and minimal perfect hash table for Full and Domain matcher.
|
// Each distinct pattern is stored once as a record in arena: its length (255 means a uvarint length follows),
|
||||||
|
// its bytes and, if the group holds more than one distinct value, its values. A minimal perfect hash table
|
||||||
|
// built with hash, displace and compress (http://cmph.sourceforge.net/papers/esa09.pdf) maps a pattern to its
|
||||||
|
// record. Patterns are hashed from the right, so one pass over the input hashes all its parent domains.
|
||||||
type MphMatcherGroup struct {
|
type MphMatcherGroup struct {
|
||||||
rules []string // RuleIdx -> pattern string, index 0 reserved for failed lookup
|
arena string
|
||||||
values [][]uint32 // RuleIdx -> registered matcher values for the pattern (Full Matcher takes precedence)
|
level0 []uint16 // bucket -> seed
|
||||||
level0 []uint32 // RollingHash & Mask -> seed for Memhash
|
level1 []uint32 // slot -> flags | record offset
|
||||||
level0Mask uint32 // Mask restricting RollingHash to 0 ~ len(level0)
|
fp []uint8 // slot -> low byte of its pattern's hash, rejects most misses without reading arena
|
||||||
level1 []uint32 // Memhash<seed> & Mask -> stored index for rules
|
n0, n1 uint32
|
||||||
level1Mask uint32 // Mask for restricting Memhash<seed> to 0 ~ len(level1)
|
mul uint64 // multiplier of the suffix hash
|
||||||
ruleInfos *map[string]mphRuleInfo
|
single uint32 // the only value if !multi
|
||||||
|
multi bool
|
||||||
|
|
||||||
|
buf []byte // build only, patterns in Add order
|
||||||
|
entries []mphEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMphMatcherGroup() *MphMatcherGroup {
|
func NewMphMatcherGroup() *MphMatcherGroup {
|
||||||
return &MphMatcherGroup{
|
return new(MphMatcherGroup)
|
||||||
rules: []string{""},
|
|
||||||
values: [][]uint32{nil},
|
|
||||||
level0: nil,
|
|
||||||
level0Mask: 0,
|
|
||||||
level1: nil,
|
|
||||||
level1Mask: 0,
|
|
||||||
ruleInfos: &map[string]mphRuleInfo{}, // Only used for building, destroyed after build complete
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddFullMatcher implements MatcherGroupForFull.
|
// AddFullMatcher implements MatcherGroupForFull.
|
||||||
func (g *MphMatcherGroup) AddFullMatcher(matcher FullMatcher, value uint32) {
|
func (g *MphMatcherGroup) AddFullMatcher(matcher FullMatcher, value uint32) {
|
||||||
pattern := strings.ToLower(matcher.Pattern())
|
g.add(matcher.Pattern(), mphKindFull, value)
|
||||||
g.addPattern(0, "", pattern, matcher.Type(), value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddDomainMatcher implements MatcherGroupForDomain.
|
// AddDomainMatcher implements MatcherGroupForDomain.
|
||||||
func (g *MphMatcherGroup) AddDomainMatcher(matcher DomainMatcher, value uint32) {
|
func (g *MphMatcherGroup) AddDomainMatcher(matcher DomainMatcher, value uint32) {
|
||||||
pattern := strings.ToLower(matcher.Pattern())
|
g.add(matcher.Pattern(), mphKindDomain, value)
|
||||||
hash := g.addPattern(0, "", pattern, matcher.Type(), value) // For full domain match
|
|
||||||
g.addPattern(hash, pattern, ".", matcher.Type(), value) // For partial domain match
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *MphMatcherGroup) addPattern(suffixHash uint32, suffixPattern string, pattern string, matcherType Type, value uint32) uint32 {
|
func (g *MphMatcherGroup) add(pattern string, kind uint8, value uint32) {
|
||||||
fullPattern := pattern + suffixPattern
|
if g.arena != "" {
|
||||||
info, found := (*g.ruleInfos)[fullPattern]
|
panic(errMphBuilt)
|
||||||
if !found {
|
}
|
||||||
info = mphRuleInfo{rollingHash: RollingHash(suffixHash, pattern)}
|
pattern = strings.ToLower(pattern)
|
||||||
g.rules = append(g.rules, fullPattern)
|
off := uint32(len(g.buf))
|
||||||
g.values = append(g.values, nil)
|
g.buf = append(g.buf, pattern...)
|
||||||
|
g.entries = append(g.entries, mphEntry{off: off, value: value, n: uint32(len(pattern)), kind: kind})
|
||||||
|
if len(pattern) > 0 && pattern[0] == '.' {
|
||||||
|
// ".x" has always matched "*.x" as well, so it also gets a parent-only record for "x"
|
||||||
|
g.entries = append(g.entries, mphEntry{off: off + 1, value: value, n: uint32(len(pattern) - 1), kind: mphKindParent})
|
||||||
}
|
}
|
||||||
info.matchers[matcherType] = append(info.matchers[matcherType], value)
|
|
||||||
(*g.ruleInfos)[fullPattern] = info
|
|
||||||
return info.rollingHash
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build builds a minimal perfect hash table for insert rules.
|
func (g *MphMatcherGroup) key(i uint32) []byte {
|
||||||
// Algorithm used: Hash, displace, and compress. See http://cmph.sourceforge.net/papers/esa09.pdf
|
e := &g.entries[i]
|
||||||
|
return g.buf[e.off : e.off+e.n]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build builds the hash table. It must be called once, after the last Add.
|
||||||
func (g *MphMatcherGroup) Build() error {
|
func (g *MphMatcherGroup) Build() error {
|
||||||
ruleCount := len(*g.ruleInfos)
|
if g.arena != "" {
|
||||||
g.level0 = make([]uint32, nextPow2(ruleCount/4))
|
return errMphBuilt
|
||||||
g.level0Mask = uint32(len(g.level0) - 1)
|
|
||||||
g.level1 = make([]uint32, nextPow2(ruleCount))
|
|
||||||
g.level1Mask = uint32(len(g.level1) - 1)
|
|
||||||
|
|
||||||
// Create buckets based on all rule's rolling hash
|
|
||||||
buckets := make([][]uint32, len(g.level0))
|
|
||||||
for ruleIdx := 1; ruleIdx < len(g.rules); ruleIdx++ { // Traverse rules starting from index 1 (0 reserved for failed lookup)
|
|
||||||
ruleInfo := (*g.ruleInfos)[g.rules[ruleIdx]]
|
|
||||||
bucketIdx := ruleInfo.rollingHash & g.level0Mask
|
|
||||||
buckets[bucketIdx] = append(buckets[bucketIdx], uint32(ruleIdx))
|
|
||||||
g.values[ruleIdx] = append(ruleInfo.matchers[Full], ruleInfo.matchers[Domain]...) // nolint:gocritic
|
|
||||||
}
|
}
|
||||||
g.ruleInfos = nil // Set ruleInfos nil to release memory
|
if uint64(len(g.buf)) > math.MaxUint32 {
|
||||||
runtime.GC() // peak mem
|
return errors.New("too many rules for MphMatcherGroup")
|
||||||
|
|
||||||
// Sort buckets in descending order with respect to each bucket's size
|
|
||||||
bucketIdxs := make([]int, len(buckets))
|
|
||||||
for bucketIdx := range buckets {
|
|
||||||
bucketIdxs[bucketIdx] = bucketIdx
|
|
||||||
}
|
}
|
||||||
sort.Slice(bucketIdxs, func(i, j int) bool { return len(buckets[bucketIdxs[i]]) > len(buckets[bucketIdxs[j]]) })
|
recs := g.writeRecords()
|
||||||
|
if len(g.arena) > mphOffMask {
|
||||||
|
return errors.New("too many rules for MphMatcherGroup")
|
||||||
|
}
|
||||||
|
hashes := make([]uint64, len(recs))
|
||||||
|
for _, mul := range mphMultipliers {
|
||||||
|
for i, rec := range recs {
|
||||||
|
hashes[i] = mphMix(mphHash(mul, g.recKey(rec)))
|
||||||
|
}
|
||||||
|
g.mul = mul
|
||||||
|
if err := g.place(recs, hashes); err != errMphCollision {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errMphCollision
|
||||||
|
}
|
||||||
|
|
||||||
// Exercise Hash, Displace, and Compress algorithm to construct minimal perfect hash table
|
// writeRecords writes one record per distinct pattern to arena and returns flags | offset of each.
|
||||||
occupied := make([]bool, len(g.level1)) // Whether a second-level hash has been already used
|
func (g *MphMatcherGroup) writeRecords() []uint32 {
|
||||||
hashedBucket := make([]uint32, 0, 4) // Second-level hashes for each rule in a specific bucket
|
g.multi = false
|
||||||
for _, bucketIdx := range bucketIdxs {
|
if len(g.entries) > 0 {
|
||||||
bucket := buckets[bucketIdx]
|
g.single = g.entries[0].value
|
||||||
hashedBucket = hashedBucket[:0]
|
for _, e := range g.entries {
|
||||||
seed := uint32(0)
|
if e.value != g.single {
|
||||||
for len(hashedBucket) != len(bucket) {
|
g.multi = true
|
||||||
for _, ruleIdx := range bucket {
|
break
|
||||||
memHash := MemHash(seed, g.rules[ruleIdx]) & g.level1Mask
|
|
||||||
if occupied[memHash] { // Collision occurred with this seed
|
|
||||||
for _, hash := range hashedBucket { // Revert all values in this hashed bucket
|
|
||||||
occupied[hash] = false
|
|
||||||
g.level1[hash] = 0
|
|
||||||
}
|
|
||||||
hashedBucket = hashedBucket[:0]
|
|
||||||
seed++ // Try next seed
|
|
||||||
break
|
|
||||||
}
|
|
||||||
occupied[memHash] = true
|
|
||||||
g.level1[memHash] = ruleIdx // The final value in the hash table
|
|
||||||
hashedBucket = append(hashedBucket, memHash)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
g.level0[bucketIdx] = seed // Displacement value for this bucket
|
}
|
||||||
|
// Equal patterns become neighbours in Add order, so their values keep their priority
|
||||||
|
order := make([]uint32, len(g.entries))
|
||||||
|
for i := range order {
|
||||||
|
order[i] = uint32(i)
|
||||||
|
}
|
||||||
|
slices.SortFunc(order, func(a, b uint32) int {
|
||||||
|
return cmp.Or(bytes.Compare(g.key(a), g.key(b)), cmp.Compare(a, b))
|
||||||
|
})
|
||||||
|
|
||||||
|
size := len(g.buf) + len(g.entries) + 2
|
||||||
|
if g.multi {
|
||||||
|
size += 3 * len(g.entries)
|
||||||
|
}
|
||||||
|
arena := make([]byte, 0, size)
|
||||||
|
recs := make([]uint32, 0, len(order))
|
||||||
|
var vals [len(mphKinds)][]uint32
|
||||||
|
for i := 0; i < len(order); {
|
||||||
|
k := g.key(order[i])
|
||||||
|
for t := range vals {
|
||||||
|
vals[t] = vals[t][:0]
|
||||||
|
}
|
||||||
|
for ; i < len(order) && bytes.Equal(g.key(order[i]), k); i++ {
|
||||||
|
e := &g.entries[order[i]]
|
||||||
|
if !slices.Contains(vals[e.kind], e.value) {
|
||||||
|
vals[e.kind] = append(vals[e.kind], e.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rec := uint32(len(arena))
|
||||||
|
if len(k) < 255 {
|
||||||
|
arena = append(arena, byte(len(k)))
|
||||||
|
} else {
|
||||||
|
arena = binary.AppendUvarint(append(arena, 255), uint64(len(k)))
|
||||||
|
}
|
||||||
|
arena = append(arena, k...)
|
||||||
|
for t, v := range vals {
|
||||||
|
if len(v) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rec |= mphKinds[t]
|
||||||
|
if g.multi {
|
||||||
|
arena = binary.AppendUvarint(arena, uint64(len(v)))
|
||||||
|
for _, x := range v {
|
||||||
|
arena = binary.AppendUvarint(arena, uint64(x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recs = append(recs, rec)
|
||||||
|
}
|
||||||
|
// Lookups may point one byte past a pattern, and an empty group needs a record at offset 0 for empty slots
|
||||||
|
arena = append(arena, 0)
|
||||||
|
if len(recs) == 0 {
|
||||||
|
arena = append(arena, 0)
|
||||||
|
}
|
||||||
|
g.buf, g.entries = nil, nil
|
||||||
|
if cap(arena)-len(arena) > len(arena)/32 {
|
||||||
|
arena = slices.Clone(arena)
|
||||||
|
}
|
||||||
|
g.arena = unsafe.String(unsafe.SliceData(arena), len(arena)) // arena is not written after this
|
||||||
|
return recs
|
||||||
|
}
|
||||||
|
|
||||||
|
// place fills level0, level1 and fp: records are bucketed by hash, and each bucket, largest first, gets
|
||||||
|
// the first seed that puts all its records in free slots.
|
||||||
|
func (g *MphMatcherGroup) place(recs []uint32, hashes []uint64) error {
|
||||||
|
r := len(recs)
|
||||||
|
n0, n1 := max(1, r/3), max(1, r+r/99)
|
||||||
|
g.n0, g.n1 = uint32(n0), uint32(n1)
|
||||||
|
g.level0 = make([]uint16, n0)
|
||||||
|
g.level1 = make([]uint32, n1)
|
||||||
|
g.fp = make([]uint8, n1)
|
||||||
|
|
||||||
|
start := make([]uint32, n0+1)
|
||||||
|
for _, h := range hashes {
|
||||||
|
start[g.bucket(h)+1]++
|
||||||
|
}
|
||||||
|
for b := range n0 {
|
||||||
|
start[b+1] += start[b]
|
||||||
|
}
|
||||||
|
members := make([]uint32, r)
|
||||||
|
fill := slices.Clone(start[:n0])
|
||||||
|
for i, h := range hashes {
|
||||||
|
b := g.bucket(h)
|
||||||
|
members[fill[b]] = uint32(i)
|
||||||
|
fill[b]++
|
||||||
|
}
|
||||||
|
fill = nil
|
||||||
|
buckets := make([]uint32, n0)
|
||||||
|
for b := range buckets {
|
||||||
|
buckets[b] = uint32(b)
|
||||||
|
}
|
||||||
|
slices.SortStableFunc(buckets, func(a, b uint32) int {
|
||||||
|
return cmp.Compare(start[b+1]-start[b], start[a+1]-start[a])
|
||||||
|
})
|
||||||
|
|
||||||
|
occupied := make([]uint64, (n1+63)/64)
|
||||||
|
var slots []uint32
|
||||||
|
next:
|
||||||
|
for _, b := range buckets {
|
||||||
|
m := members[start[b]:start[b+1]]
|
||||||
|
if len(m) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for i := range m {
|
||||||
|
for j := range i {
|
||||||
|
if hashes[m[i]] == hashes[m[j]] {
|
||||||
|
return errMphCollision // no seed can separate them
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
search:
|
||||||
|
for seed := range math.MaxUint16 + 1 {
|
||||||
|
slots = slots[:0]
|
||||||
|
for _, ri := range m {
|
||||||
|
s := g.slot(hashes[ri], uint16(seed))
|
||||||
|
if occupied[s/64]&(1<<(s%64)) != 0 || slices.Contains(slots, s) {
|
||||||
|
continue search
|
||||||
|
}
|
||||||
|
slots = append(slots, s)
|
||||||
|
}
|
||||||
|
for k, ri := range m {
|
||||||
|
s := slots[k]
|
||||||
|
occupied[s/64] |= 1 << (s % 64)
|
||||||
|
g.level1[s] = recs[ri]
|
||||||
|
g.fp[s] = uint8(hashes[ri])
|
||||||
|
}
|
||||||
|
g.level0[b] = uint16(seed)
|
||||||
|
continue next
|
||||||
|
}
|
||||||
|
return errors.New("strmatcher: no seed found for a bucket in MphMatcherGroup")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lookup searches for input in minimal perfect hash table and returns its index. 0 indicates not found.
|
// mphHash is the suffix hash of s, taken from the right: the hash of s[i:] is the state after reading s[i].
|
||||||
func (g *MphMatcherGroup) Lookup(rollingHash uint32, input string) uint32 {
|
func mphHash(mul uint64, s string) uint64 {
|
||||||
i0 := rollingHash & g.level0Mask
|
h := uint64(0)
|
||||||
seed := g.level0[i0]
|
for i := len(s) - 1; i >= 0; i-- {
|
||||||
i1 := MemHash(seed, input) & g.level1Mask
|
h = h*mul + uint64(s[i])
|
||||||
if n := g.level1[i1]; g.rules[n] == input {
|
}
|
||||||
return n
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// mphMix spreads the weak low bits of a suffix hash.
|
||||||
|
func mphMix(h uint64) uint64 {
|
||||||
|
h ^= h >> 32
|
||||||
|
h *= 0xd6e8feb86659fd93
|
||||||
|
return h ^ h>>32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *MphMatcherGroup) bucket(f uint64) uint32 {
|
||||||
|
return uint32(((f >> 32) * uint64(g.n0)) >> 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *MphMatcherGroup) slot(f uint64, seed uint16) uint32 {
|
||||||
|
x := ((f ^ uint64(seed)*0x9e3779b97f4a7c15) * 0xc4ceb9fe1a85ec53) >> 32
|
||||||
|
return uint32((x * uint64(g.n1)) >> 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *MphMatcherGroup) uvarint(p uint32) (x, next uint32) {
|
||||||
|
for shift := 0; ; shift += 7 {
|
||||||
|
c := g.arena[p]
|
||||||
|
p++
|
||||||
|
x |= uint32(c&0x7f) << shift
|
||||||
|
if c < 0x80 {
|
||||||
|
return x, p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// recSpan returns where the pattern of the record at off starts and how long it is.
|
||||||
|
func (g *MphMatcherGroup) recSpan(off uint32) (p, n uint32) {
|
||||||
|
n, p = uint32(g.arena[off]), off+1
|
||||||
|
if n == 255 {
|
||||||
|
n, p = g.uvarint(p)
|
||||||
|
}
|
||||||
|
return p, n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *MphMatcherGroup) recKey(rec uint32) string {
|
||||||
|
p, n := g.recSpan(rec & mphOffMask)
|
||||||
|
return g.arena[p : p+n]
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookup returns the level1 entry of s, or 0 if s is not a pattern. h is the suffix hash of s.
|
||||||
|
func (g *MphMatcherGroup) lookup(h uint64, s string) uint32 {
|
||||||
|
f := mphMix(h)
|
||||||
|
// bucket < n0 == len(level0) and slot < n1 == len(level1) == len(fp), skip the bounds checks
|
||||||
|
seed := *(*uint16)(unsafe.Add(unsafe.Pointer(unsafe.SliceData(g.level0)), uintptr(g.bucket(f))*2))
|
||||||
|
slot := uintptr(g.slot(f, seed))
|
||||||
|
if *(*uint8)(unsafe.Add(unsafe.Pointer(unsafe.SliceData(g.fp)), slot)) != uint8(f) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
e := *(*uint32)(unsafe.Add(unsafe.Pointer(unsafe.SliceData(g.level1)), slot*4))
|
||||||
|
if len(s) < 255 {
|
||||||
|
// A record whose length byte is len(s) has len(s) pattern bytes after it
|
||||||
|
p := unsafe.Add(unsafe.Pointer(unsafe.StringData(g.arena)), e&mphOffMask)
|
||||||
|
if int(*(*byte)(p)) == len(s) && unsafe.String((*byte)(unsafe.Add(p, 1)), len(s)) == s {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if g.recKey(e) == s {
|
||||||
|
return e
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match implements MatcherGroup.Match.
|
// appendValues appends the values of record e for the flags in want, in mphKinds order.
|
||||||
func (g *MphMatcherGroup) Match(input string) []uint32 {
|
func (g *MphMatcherGroup) appendValues(dst []uint32, e, want uint32) []uint32 {
|
||||||
matches := make([][]uint32, 0, 5)
|
if !g.multi {
|
||||||
hash := uint32(0)
|
for _, flag := range mphKinds {
|
||||||
for i := len(input) - 1; i >= 0; i-- {
|
if e&want&flag != 0 {
|
||||||
hash = hash*PrimeRK + uint32(input[i])
|
dst = append(dst, g.single)
|
||||||
if input[i] == '.' {
|
}
|
||||||
if mphIdx := g.Lookup(hash, input[i:]); mphIdx != 0 {
|
}
|
||||||
matches = append(matches, g.values[mphIdx])
|
return dst
|
||||||
|
}
|
||||||
|
if e&want == 0 {
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
p, n := g.recSpan(e & mphOffMask)
|
||||||
|
p += n
|
||||||
|
for _, flag := range mphKinds {
|
||||||
|
if e&flag == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var count, v uint32
|
||||||
|
for count, p = g.uvarint(p); count > 0; count-- {
|
||||||
|
v, p = g.uvarint(p)
|
||||||
|
if want&flag != 0 {
|
||||||
|
dst = append(dst, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if mphIdx := g.Lookup(hash, input); mphIdx != 0 {
|
return dst
|
||||||
matches = append(matches, g.values[mphIdx])
|
}
|
||||||
|
|
||||||
|
// Match implements MatcherGroup.Match. Values of an exact match come first (Full, then Domain), then those of
|
||||||
|
// the parent domains, nearest first.
|
||||||
|
func (g *MphMatcherGroup) Match(input string) []uint32 {
|
||||||
|
var stack [8]uint32
|
||||||
|
parents := stack[:0] // TLD side first
|
||||||
|
h, mul := uint64(0), g.mul
|
||||||
|
for i := len(input) - 1; i >= 0; i-- {
|
||||||
|
if input[i] == '.' {
|
||||||
|
if e := g.lookup(h, input[i+1:]); e&(mphDomain|mphParent) != 0 {
|
||||||
|
parents = append(parents, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h = h*mul + uint64(input[i])
|
||||||
}
|
}
|
||||||
return CompositeMatchesReverse(matches)
|
exact := g.lookup(h, input)
|
||||||
|
if exact&(mphFull|mphDomain) == 0 && len(parents) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := g.appendValues(make([]uint32, 0, len(parents)+1), exact, mphFull|mphDomain)
|
||||||
|
for k := len(parents) - 1; k >= 0; k-- {
|
||||||
|
result = g.appendValues(result, parents[k], mphParent|mphDomain)
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchAny implements MatcherGroup.MatchAny.
|
// MatchAny implements MatcherGroup.MatchAny.
|
||||||
func (g *MphMatcherGroup) MatchAny(input string) bool {
|
func (g *MphMatcherGroup) MatchAny(input string) bool {
|
||||||
hash := uint32(0)
|
h, mul := uint64(0), g.mul
|
||||||
|
for i := len(input) - 1; i >= 0; i-- {
|
||||||
|
if input[i] == '.' && g.lookup(h, input[i+1:])&(mphDomain|mphParent) != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
h = h*mul + uint64(input[i])
|
||||||
|
}
|
||||||
|
return g.lookup(h, input)&(mphFull|mphDomain) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// mphSuffix is the suffix hash of input[off:], a parent domain of the input.
|
||||||
|
type mphSuffix struct {
|
||||||
|
h uint64
|
||||||
|
off int
|
||||||
|
}
|
||||||
|
|
||||||
|
// mphSuffixes appends the suffix hashes of the parent domains of input to dst, TLD side first, and returns them
|
||||||
|
// with the hash of input itself: what MatchAny computes, computed once for several groups.
|
||||||
|
func mphSuffixes(dst []mphSuffix, mul uint64, input string) ([]mphSuffix, uint64) {
|
||||||
|
h := uint64(0)
|
||||||
for i := len(input) - 1; i >= 0; i-- {
|
for i := len(input) - 1; i >= 0; i-- {
|
||||||
hash = hash*PrimeRK + uint32(input[i])
|
|
||||||
if input[i] == '.' {
|
if input[i] == '.' {
|
||||||
if g.Lookup(hash, input[i:]) != 0 {
|
dst = append(dst, mphSuffix{h, i + 1})
|
||||||
return true
|
}
|
||||||
}
|
h = h*mul + uint64(input[i])
|
||||||
|
}
|
||||||
|
return dst, h
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchAnyHashed is MatchAny with parents and h from mphSuffixes(_, mul, input).
|
||||||
|
func (g *MphMatcherGroup) matchAnyHashed(input string, parents []mphSuffix, h, mul uint64) bool {
|
||||||
|
if g.mul != mul {
|
||||||
|
return g.MatchAny(input) // built with a later multiplier after a collision
|
||||||
|
}
|
||||||
|
for _, p := range parents {
|
||||||
|
if g.lookup(p.h, input[p.off:])&(mphDomain|mphParent) != 0 {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return g.Lookup(hash, input) != 0
|
return g.lookup(h, input)&(mphFull|mphDomain) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func nextPow2(v int) int {
|
|
||||||
if v <= 1 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
const MaxUInt = ^uint(0)
|
|
||||||
n := (MaxUInt >> bits.LeadingZeros(uint(v))) + 1
|
|
||||||
return int(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:noescape
|
|
||||||
//go:linkname strhash runtime.strhash
|
|
||||||
func strhash(p unsafe.Pointer, h uintptr) uintptr
|
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package strmatcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMphMatcherGroupHashCollision(t *testing.T) {
|
||||||
|
saved := mphMultipliers
|
||||||
|
defer func() { mphMultipliers = saved }()
|
||||||
|
|
||||||
|
mphMultipliers[0] = 1 // anagrams collide
|
||||||
|
g := NewMphMatcherGroup()
|
||||||
|
g.AddFullMatcher(FullMatcher("ab.com"), 1)
|
||||||
|
g.AddDomainMatcher(DomainMatcher("ba.com"), 2)
|
||||||
|
g.AddDomainMatcher(DomainMatcher("com"), 3)
|
||||||
|
if err := g.Build(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if g.mul != saved[1] {
|
||||||
|
t.Errorf("multiplier %#x, want the second one %#x", g.mul, saved[1])
|
||||||
|
}
|
||||||
|
for input, want := range map[string][]uint32{"ab.com": {1, 3}, "x.ba.com": {2, 3}, "x.ab.com": {3}, "ba.com": {2, 3}} {
|
||||||
|
if m := g.Match(input); !slices.Equal(m, want) {
|
||||||
|
t.Errorf("Match(%q) = %v, want %v", input, m, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thue-Morse strings of 2048 bytes and their complements collide for every odd multiplier
|
||||||
|
mphMultipliers = saved
|
||||||
|
a, b := make([]byte, 2048), make([]byte, 2048)
|
||||||
|
for i := range a {
|
||||||
|
a[i], b[i] = "ab"[bitsOnes(i)%2], "ba"[bitsOnes(i)%2]
|
||||||
|
}
|
||||||
|
g = NewMphMatcherGroup()
|
||||||
|
g.AddFullMatcher(FullMatcher(a), 1)
|
||||||
|
g.AddFullMatcher(FullMatcher(b), 1)
|
||||||
|
if err := g.Build(); err != errMphCollision {
|
||||||
|
t.Errorf("Build() = %v, want %v", err, errMphCollision)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bitsOnes(i int) int {
|
||||||
|
n := 0
|
||||||
|
for ; i > 0; i &= i - 1 {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMphValueMatcherCombiner(t *testing.T) {
|
||||||
|
build := func(matchers ...Matcher) *MphValueMatcher {
|
||||||
|
m := NewMphValueMatcher()
|
||||||
|
for _, x := range matchers {
|
||||||
|
m.Add(x, 0)
|
||||||
|
}
|
||||||
|
if err := m.Build(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
regex, err := Regex.New(`^a\d+\.net$`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
saved := mphMultipliers
|
||||||
|
t.Cleanup(func() { mphMultipliers = saved })
|
||||||
|
mphMultipliers[0] = 1 // anagrams collide, so this one falls back to its own hash pass
|
||||||
|
collided := build(FullMatcher("ab.com"), DomainMatcher("ba.com"))
|
||||||
|
mphMultipliers = saved
|
||||||
|
if collided.mph.mul == mphMultipliers[0] {
|
||||||
|
t.Fatal("collided matcher uses the first multiplier")
|
||||||
|
}
|
||||||
|
matchers := []*MphValueMatcher{
|
||||||
|
build(DomainMatcher("example.com"), FullMatcher("full.org"), DomainMatcher(".dot.io")),
|
||||||
|
collided,
|
||||||
|
build(regex, SubstrMatcher("keyword")),
|
||||||
|
build(),
|
||||||
|
build(DomainMatcher("com"), DomainMatcher("a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s")),
|
||||||
|
}
|
||||||
|
var s MphValueMatcherCombiner
|
||||||
|
for i, m := range matchers {
|
||||||
|
s.Add(m, uint32(10+i))
|
||||||
|
}
|
||||||
|
inputs := []string{
|
||||||
|
"", ".", "..", "com", "example.com", "www.example.com", "xexample.com", "example.com.", "full.org", "x.full.org",
|
||||||
|
"dot.io", "x.dot.io", ".dot.io", "ab.com", "x.ab.com", "ba.com", "x.ba.com", "a12.net", "a12.net.x", "my-keyword.org",
|
||||||
|
"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s", "0.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s", "b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s",
|
||||||
|
"x.y.z.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.ab.com", "x.y.z.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.org",
|
||||||
|
}
|
||||||
|
for _, input := range inputs {
|
||||||
|
var want []uint32
|
||||||
|
for i, m := range matchers {
|
||||||
|
if m.MatchAny(input) {
|
||||||
|
want = append(want, uint32(10+i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := s.Match(input); !slices.Equal(got, want) {
|
||||||
|
t.Errorf("Match(%q) = %v, want %v", input, got, want)
|
||||||
|
}
|
||||||
|
if got := s.MatchAny(input); got != (len(want) > 0) {
|
||||||
|
t.Errorf("MatchAny(%q) = %v", input, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if n := testing.AllocsPerRun(100, func() { s.MatchAny("www.a.b.c.example.org") }); n != 0 {
|
||||||
|
t.Errorf("MatchAny allocates %v times", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
package strmatcher_test
|
package strmatcher_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math/rand"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
@@ -276,3 +279,142 @@ func TestEmptyMphMatcherGroup(t *testing.T) {
|
|||||||
t.Error("Expect [], but ", r)
|
t.Error("Expect [], but ", r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMphMatcherGroupRandom(t *testing.T) {
|
||||||
|
inputs := []string{""} // All strings over "ab." up to 7 bytes
|
||||||
|
for i := 0; len(inputs[i]) < 7; i++ {
|
||||||
|
for _, c := range []string{"a", "b", "."} {
|
||||||
|
inputs = append(inputs, inputs[i]+c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for seed := int64(0); seed < 300; seed++ {
|
||||||
|
r := rand.New(rand.NewSource(seed))
|
||||||
|
g := NewMphMatcherGroup()
|
||||||
|
full, domain := map[string][]uint32{}, map[string][]uint32{} // Stored pattern -> values
|
||||||
|
for value := uint32(r.Intn(200)); value > 0; value-- {
|
||||||
|
pattern := make([]byte, r.Intn(8))
|
||||||
|
for i := range pattern {
|
||||||
|
pattern[i] = "ab."[r.Intn(3)]
|
||||||
|
}
|
||||||
|
if p := string(pattern); r.Intn(2) == 0 {
|
||||||
|
g.AddFullMatcher(FullMatcher(p), value)
|
||||||
|
full[p] = append(full[p], value)
|
||||||
|
} else {
|
||||||
|
g.AddDomainMatcher(DomainMatcher(p), value)
|
||||||
|
domain[p] = append(domain[p], value)
|
||||||
|
domain["."+p] = append(domain["."+p], value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
common.Must(g.Build())
|
||||||
|
for _, input := range inputs {
|
||||||
|
keys := []string{input} // Whole input first, then "." suffixes from longest to shortest
|
||||||
|
for i := range len(input) {
|
||||||
|
if input[i] == '.' {
|
||||||
|
keys = append(keys, input[i:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var want []uint32
|
||||||
|
for _, k := range keys {
|
||||||
|
want = append(append(want, full[k]...), domain[k]...)
|
||||||
|
}
|
||||||
|
// Compared as sets: Match reports a value once per matching pattern, and orders them differently
|
||||||
|
// from want for patterns and inputs with a leading dot
|
||||||
|
m := g.Match(input)
|
||||||
|
if !slices.Equal(sortedSet(m), sortedSet(want)) {
|
||||||
|
t.Fatalf("seed %d: Match(%q) = %v, want %v", seed, input, m, want)
|
||||||
|
}
|
||||||
|
if m := g.MatchAny(input); m != (len(want) > 0) {
|
||||||
|
t.Fatalf("seed %d: MatchAny(%q) = %v", seed, input, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMphMatcherGroupAppend(t *testing.T) {
|
||||||
|
g := NewMphMatcherGroup()
|
||||||
|
g.AddFullMatcher(FullMatcher("a.com"), 1)
|
||||||
|
g.AddFullMatcher(FullMatcher("b.com"), 2)
|
||||||
|
g.Build()
|
||||||
|
if m := append(g.Match("a.com"), 3); !slices.Equal(m, []uint32{1, 3}) {
|
||||||
|
t.Error("expect [1 3], but ", m)
|
||||||
|
}
|
||||||
|
if m := g.Match("b.com"); !slices.Equal(m, []uint32{2}) {
|
||||||
|
t.Error("expect [2], but ", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortedSet(v []uint32) []uint32 {
|
||||||
|
v = slices.Clone(v)
|
||||||
|
slices.Sort(v)
|
||||||
|
return slices.Compact(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMphMatcherGroupLongPattern(t *testing.T) {
|
||||||
|
long := strings.Repeat("a", 300) + ".com"
|
||||||
|
for _, values := range [][4]uint32{{1, 2, 3, 4}, {7, 7, 7, 7}} {
|
||||||
|
g := NewMphMatcherGroup()
|
||||||
|
g.AddDomainMatcher(DomainMatcher(long), values[0])
|
||||||
|
g.AddFullMatcher(FullMatcher("x."+long), values[1])
|
||||||
|
g.AddFullMatcher(FullMatcher(long[:255]), values[2]) // the shortest pattern stored with a long length
|
||||||
|
g.AddFullMatcher(FullMatcher(long[:254]), values[3])
|
||||||
|
common.Must(g.Build())
|
||||||
|
cases := []struct {
|
||||||
|
input string
|
||||||
|
want []uint32
|
||||||
|
}{
|
||||||
|
{long, []uint32{values[0]}},
|
||||||
|
{"www." + long, []uint32{values[0]}},
|
||||||
|
{"x." + long, []uint32{values[1], values[0]}},
|
||||||
|
{long[1:], nil},
|
||||||
|
{"a" + long, nil},
|
||||||
|
{long[:255], []uint32{values[2]}},
|
||||||
|
{long[:254], []uint32{values[3]}},
|
||||||
|
{long[:256], nil},
|
||||||
|
{long[:253], nil},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if m := g.Match(c.input); !slices.Equal(m, c.want) {
|
||||||
|
t.Errorf("Match(%d bytes) = %v, want %v", len(c.input), m, c.want)
|
||||||
|
}
|
||||||
|
if m := g.MatchAny(c.input); m != (c.want != nil) {
|
||||||
|
t.Errorf("MatchAny(%d bytes) = %v", len(c.input), m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pattern longer than 65535 bytes builds and matches: a record's length is a uvarint,
|
||||||
|
// so the only cap was the build-time length field, now widened to uint32.
|
||||||
|
huge := strings.Repeat("a", 70000)
|
||||||
|
g := NewMphMatcherGroup()
|
||||||
|
g.AddFullMatcher(FullMatcher(strings.Repeat("a", 65535)), 1)
|
||||||
|
g.AddDomainMatcher(DomainMatcher(huge+".com"), 2)
|
||||||
|
g.AddFullMatcher(FullMatcher("a.com"), 3)
|
||||||
|
common.Must(g.Build())
|
||||||
|
if !g.MatchAny(strings.Repeat("a", 65535)) || g.MatchAny(strings.Repeat("a", 65534)) {
|
||||||
|
t.Error("wrong answer for a 65535-byte pattern")
|
||||||
|
}
|
||||||
|
if m := g.Match(huge + ".com"); !slices.Equal(m, []uint32{2}) {
|
||||||
|
t.Errorf("Match(%d-byte input) = %v, want [2]", len(huge)+4, m)
|
||||||
|
}
|
||||||
|
if m := g.Match("x." + huge + ".com"); !slices.Equal(m, []uint32{2}) {
|
||||||
|
t.Errorf("Match(subdomain of a %d-byte pattern) = %v, want [2]", len(huge)+4, m)
|
||||||
|
}
|
||||||
|
if g.MatchAny(huge) { // the 70000-byte label on its own is not a rule
|
||||||
|
t.Error("unexpected match for the bare 70000-byte label")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMphMatcherGroupBuildOnce(t *testing.T) {
|
||||||
|
g := NewMphMatcherGroup()
|
||||||
|
g.AddFullMatcher(FullMatcher("a.com"), 1)
|
||||||
|
common.Must(g.Build())
|
||||||
|
if err := g.Build(); err == nil || !g.MatchAny("a.com") {
|
||||||
|
t.Errorf("second Build() = %v, MatchAny(a.com) = %v", err, g.MatchAny("a.com"))
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if recover() == nil {
|
||||||
|
t.Error("Add after Build did not panic")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
g.AddDomainMatcher(DomainMatcher("b.com"), 2)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package strmatcher
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"math/bits"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"regexp/syntax"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"golang.org/x/net/idna"
|
"golang.org/x/net/idna"
|
||||||
@@ -73,7 +76,274 @@ func (m SubstrMatcher) Match(s string) bool {
|
|||||||
|
|
||||||
// RegexMatcher is an implementation of Matcher.
|
// RegexMatcher is an implementation of Matcher.
|
||||||
type RegexMatcher struct {
|
type RegexMatcher struct {
|
||||||
pattern *regexp.Regexp
|
pattern *regexp.Regexp
|
||||||
|
literals []string // every match contains all of them, longest first
|
||||||
|
tail []byteSet // tail[i] holds the bytes a matching input can have i bytes before its end
|
||||||
|
rest *byteSet // the bytes it can have further before, nil if any
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRegexMatcher(pattern string) (Matcher, error) {
|
||||||
|
regex, err := regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m := &RegexMatcher{pattern: regex}
|
||||||
|
if re, err := syntax.Parse(pattern, syntax.Perl); err == nil { // same flags as regexp.Compile
|
||||||
|
m.literals = requiredLiterals(re, nil)
|
||||||
|
slices.SortStableFunc(m.literals, func(a, b string) int { return len(b) - len(a) })
|
||||||
|
m.tail, m.rest = tailGuard(re)
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// byteSet is a set of bytes. The bytes >= 0x80 share one bit with 0x7f.
|
||||||
|
type byteSet [4]uint32
|
||||||
|
|
||||||
|
func (s *byteSet) add(c byte) { c = min(c, 0x7f); s[c>>5] |= 1 << (c & 31) }
|
||||||
|
func (s *byteSet) has(c byte) bool { c = min(c, 0x7f); return s[c>>5]&(1<<(c&31)) != 0 }
|
||||||
|
func (s *byteSet) or(t *byteSet) {
|
||||||
|
for i := range s {
|
||||||
|
s[i] |= t[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var allBytes = byteSet{^uint32(0), ^uint32(0), ^uint32(0), ^uint32(0)}
|
||||||
|
|
||||||
|
// tailLen is how many positions before the end of the input tailGuard tells apart.
|
||||||
|
const tailLen = 8
|
||||||
|
|
||||||
|
// tailBudget caps how many repetition steps tailGuard walks. Only nested repeats can make the
|
||||||
|
// walk explode, so only they are charged: a flat pattern, however long, is walked once and keeps
|
||||||
|
// its guard.
|
||||||
|
const tailBudget = 100000
|
||||||
|
|
||||||
|
// tailWalk is a set of positions in the input, counted in bytes before its end.
|
||||||
|
type tailWalk struct {
|
||||||
|
at uint32 // bit i: exactly i bytes before the end, for i < tailLen
|
||||||
|
far bool // tailLen or more bytes before the end
|
||||||
|
free bool // not tied to the end of the input yet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w tailWalk) union(v tailWalk) tailWalk {
|
||||||
|
return tailWalk{w.at | v.at, w.far || v.far, w.free || v.free}
|
||||||
|
}
|
||||||
|
|
||||||
|
type tailBuilder struct {
|
||||||
|
tail [tailLen]byteSet
|
||||||
|
rest byteSet
|
||||||
|
void bool
|
||||||
|
work int
|
||||||
|
}
|
||||||
|
|
||||||
|
// tailGuard walks re backwards from the end of the input and collects the bytes an input
|
||||||
|
// matching re can have at each position before its end. It returns nil, nil when a branch
|
||||||
|
// of re does not end with $ or when nested repeats push the walk past tailBudget.
|
||||||
|
func tailGuard(re *syntax.Regexp) ([]byteSet, *byteSet) {
|
||||||
|
var b tailBuilder
|
||||||
|
w := b.walk(re, tailWalk{free: true})
|
||||||
|
b.stop(w)
|
||||||
|
if b.void {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if w.at != 0 { // a match can start here, so any bytes can come before
|
||||||
|
for i := bits.TrailingZeros32(w.at); i < tailLen; i++ {
|
||||||
|
b.tail[i] = allBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if w.at != 0 || w.far {
|
||||||
|
b.rest = allBytes
|
||||||
|
}
|
||||||
|
n := tailLen
|
||||||
|
for n > 0 && b.tail[n-1] == b.rest {
|
||||||
|
n--
|
||||||
|
}
|
||||||
|
var tail []byteSet
|
||||||
|
if n > 0 {
|
||||||
|
tail = slices.Clone(b.tail[:n])
|
||||||
|
}
|
||||||
|
if b.rest != allBytes {
|
||||||
|
rest := b.rest
|
||||||
|
return tail, &rest
|
||||||
|
}
|
||||||
|
return tail, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop ends the paths of w. One that never met $ lets its match be followed by anything.
|
||||||
|
func (b *tailBuilder) stop(w tailWalk) {
|
||||||
|
if w.free {
|
||||||
|
b.void = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *tailBuilder) walk(re *syntax.Regexp, w tailWalk) tailWalk {
|
||||||
|
if w == (tailWalk{}) || b.void {
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
switch re.Op {
|
||||||
|
case syntax.OpNoMatch:
|
||||||
|
return tailWalk{}
|
||||||
|
case syntax.OpLiteral:
|
||||||
|
for i := len(re.Rune) - 1; i >= 0; i-- {
|
||||||
|
var set byteSet
|
||||||
|
set.add(byte(min(re.Rune[i], utf8.RuneSelf)))
|
||||||
|
if re.Flags&syntax.FoldCase != 0 {
|
||||||
|
for f := unicode.SimpleFold(re.Rune[i]); f != re.Rune[i]; f = unicode.SimpleFold(f) {
|
||||||
|
set.add(byte(min(f, utf8.RuneSelf)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w = b.step(w, &set)
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
case syntax.OpCharClass:
|
||||||
|
var set byteSet
|
||||||
|
for i := 0; i+1 < len(re.Rune); i += 2 {
|
||||||
|
for r := min(re.Rune[i], utf8.RuneSelf); r <= min(re.Rune[i+1], utf8.RuneSelf); r++ {
|
||||||
|
set.add(byte(r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.step(w, &set)
|
||||||
|
case syntax.OpAnyChar, syntax.OpAnyCharNotNL: // a domain has no \n to reject
|
||||||
|
return b.step(w, &allBytes)
|
||||||
|
case syntax.OpBeginText: // nothing comes before
|
||||||
|
b.stop(w)
|
||||||
|
return tailWalk{}
|
||||||
|
case syntax.OpEndText:
|
||||||
|
out := tailWalk{at: w.at & 1}
|
||||||
|
if w.free {
|
||||||
|
out.at = 1
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
case syntax.OpCapture:
|
||||||
|
return b.walk(re.Sub[0], w)
|
||||||
|
case syntax.OpConcat:
|
||||||
|
for i := len(re.Sub) - 1; i >= 0; i-- {
|
||||||
|
w = b.walk(re.Sub[i], w)
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
case syntax.OpAlternate:
|
||||||
|
var out tailWalk
|
||||||
|
for _, sub := range re.Sub {
|
||||||
|
out = out.union(b.walk(sub, w))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
case syntax.OpQuest:
|
||||||
|
return b.repeat(re.Sub[0], w, 1)
|
||||||
|
case syntax.OpStar:
|
||||||
|
return b.repeat(re.Sub[0], w, -1)
|
||||||
|
case syntax.OpPlus:
|
||||||
|
return b.repeat(re.Sub[0], b.walk(re.Sub[0], w), -1)
|
||||||
|
case syntax.OpRepeat:
|
||||||
|
for i := 0; i < re.Min; i++ {
|
||||||
|
if b.charge() {
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
w = b.walk(re.Sub[0], w)
|
||||||
|
}
|
||||||
|
if re.Max < 0 {
|
||||||
|
return b.repeat(re.Sub[0], w, -1)
|
||||||
|
}
|
||||||
|
return b.repeat(re.Sub[0], w, re.Max-re.Min)
|
||||||
|
}
|
||||||
|
return w // empty match, line and word boundaries: no constraint
|
||||||
|
}
|
||||||
|
|
||||||
|
// charge counts one repetition step and reports whether the walk has run out of budget. Only
|
||||||
|
// repeats re-walk their body, so charging them alone bounds the blow-up of nested repeats while
|
||||||
|
// leaving a single linear pass, of any length, free.
|
||||||
|
func (b *tailBuilder) charge() bool {
|
||||||
|
b.work++
|
||||||
|
if b.work > tailBudget {
|
||||||
|
b.void = true
|
||||||
|
}
|
||||||
|
return b.void
|
||||||
|
}
|
||||||
|
|
||||||
|
// repeat walks back over up to n more repetitions of re, any number if n < 0.
|
||||||
|
func (b *tailBuilder) repeat(re *syntax.Regexp, w tailWalk, n int) tailWalk {
|
||||||
|
for ; n != 0; n-- {
|
||||||
|
if b.charge() {
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
next := w.union(b.walk(re, w))
|
||||||
|
if next == w {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
w = next
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// step walks back over one character whose last byte is in set. A character that can be
|
||||||
|
// non-ASCII can take up to 4 bytes, all >= 0x80; regexp matches an invalid byte as U+FFFD.
|
||||||
|
func (b *tailBuilder) step(w tailWalk, set *byteSet) tailWalk {
|
||||||
|
out := tailWalk{far: w.far, free: w.free}
|
||||||
|
if w.far {
|
||||||
|
b.rest.or(set)
|
||||||
|
}
|
||||||
|
width := 1
|
||||||
|
if set.has(0x80) {
|
||||||
|
width = utf8.UTFMax
|
||||||
|
}
|
||||||
|
for i := 0; i < tailLen; i++ {
|
||||||
|
if w.at&(1<<i) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.tail[i].or(set)
|
||||||
|
for n := 1; n <= width; n++ {
|
||||||
|
if j := i + n; j < tailLen {
|
||||||
|
out.at |= 1 << j
|
||||||
|
if n < width {
|
||||||
|
b.tail[j].add(0x80)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.far = true
|
||||||
|
if n < width {
|
||||||
|
b.rest.add(0x80)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// mayMatch reports whether s passes the tail guard.
|
||||||
|
func (m *RegexMatcher) mayMatch(s string) bool {
|
||||||
|
n := len(s)
|
||||||
|
if m.rest == nil {
|
||||||
|
n = min(n, len(m.tail))
|
||||||
|
}
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
set := m.rest
|
||||||
|
if i < len(m.tail) {
|
||||||
|
set = &m.tail[i]
|
||||||
|
}
|
||||||
|
if !set.has(s[len(s)-1-i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// requiredLiterals appends to dst the case-sensitive strings that every match of re contains.
|
||||||
|
func requiredLiterals(re *syntax.Regexp, dst []string) []string {
|
||||||
|
switch re.Op {
|
||||||
|
case syntax.OpLiteral:
|
||||||
|
// regexp matches U+FFFD against invalid UTF-8 bytes, strings.Contains does not
|
||||||
|
if re.Flags&syntax.FoldCase == 0 && !slices.Contains(re.Rune, utf8.RuneError) {
|
||||||
|
dst = append(dst, string(re.Rune))
|
||||||
|
}
|
||||||
|
case syntax.OpCapture, syntax.OpPlus:
|
||||||
|
dst = requiredLiterals(re.Sub[0], dst)
|
||||||
|
case syntax.OpRepeat:
|
||||||
|
if re.Min > 0 {
|
||||||
|
dst = requiredLiterals(re.Sub[0], dst)
|
||||||
|
}
|
||||||
|
case syntax.OpConcat:
|
||||||
|
for _, sub := range re.Sub {
|
||||||
|
dst = requiredLiterals(sub, dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*RegexMatcher) Type() Type {
|
func (*RegexMatcher) Type() Type {
|
||||||
@@ -89,6 +359,14 @@ func (m *RegexMatcher) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *RegexMatcher) Match(s string) bool {
|
func (m *RegexMatcher) Match(s string) bool {
|
||||||
|
if !m.mayMatch(s) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, l := range m.literals {
|
||||||
|
if !strings.Contains(s, l) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
return m.pattern.MatchString(s)
|
return m.pattern.MatchString(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,11 +380,7 @@ func (t Type) New(pattern string) (Matcher, error) {
|
|||||||
case Domain:
|
case Domain:
|
||||||
return DomainMatcher(pattern), nil
|
return DomainMatcher(pattern), nil
|
||||||
case Regex: // 1. regex matching is case-sensitive
|
case Regex: // 1. regex matching is case-sensitive
|
||||||
regex, err := regexp.Compile(pattern)
|
return newRegexMatcher(pattern)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &RegexMatcher{pattern: regex}, nil
|
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("unknown matcher type")
|
return nil, errors.New("unknown matcher type")
|
||||||
}
|
}
|
||||||
@@ -135,11 +409,7 @@ func (t Type) NewDomainPattern(pattern string) (Matcher, error) {
|
|||||||
}
|
}
|
||||||
return DomainMatcher(pattern), nil
|
return DomainMatcher(pattern), nil
|
||||||
case Regex: // Regex's charset not in LDH subset
|
case Regex: // Regex's charset not in LDH subset
|
||||||
regex, err := regexp.Compile(pattern)
|
return newRegexMatcher(pattern)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &RegexMatcher{pattern: regex}, nil
|
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("unknown matcher type")
|
return nil, errors.New("unknown matcher type")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package strmatcher
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hash/fnv"
|
||||||
|
"math/rand/v2"
|
||||||
|
"regexp"
|
||||||
|
"regexp/syntax"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
var regexLiteralCases = []struct {
|
||||||
|
pattern string
|
||||||
|
literals []string
|
||||||
|
}{
|
||||||
|
{`(^|\.)91porn\.(best|com)$`, []string{"91porn."}},
|
||||||
|
{`.+\.awsdns-cn-[0-9][0-9]\.(biz|com|net|top)$`, []string{".awsdns-cn-", "."}},
|
||||||
|
{`^r+[0-9]+(---|\.)sn-(2x3|ni5|j5o)\w{5}\.googlevideo\.com$`, []string{".googlevideo.com", "sn-", "r"}},
|
||||||
|
{`(?i)abc`, nil},
|
||||||
|
{`ab(?i:CD)ef`, []string{"ab", "ef"}},
|
||||||
|
{`(abc)?x`, []string{"x"}},
|
||||||
|
{`(abc)*x`, []string{"x"}},
|
||||||
|
{`x{0,3}yy`, []string{"yy"}},
|
||||||
|
{`(ab)+c{2}`, []string{"ab", "c"}},
|
||||||
|
{`abc|abd`, []string{"ab"}},
|
||||||
|
{`\Qa.b\E`, []string{"a.b"}},
|
||||||
|
{`a\x{FFFD}b`, nil},
|
||||||
|
{`^[^.]+$`, nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegexRequiredLiterals(t *testing.T) {
|
||||||
|
for _, test := range regexLiteralCases {
|
||||||
|
m, err := newRegexMatcher(test.pattern)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := m.(*RegexMatcher).literals; !slices.Equal(got, test.literals) {
|
||||||
|
t.Errorf("%s: got %q, want %q", test.pattern, got, test.literals)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var regexTailCases = []struct {
|
||||||
|
pattern string
|
||||||
|
guard bool
|
||||||
|
match []string // inputs the pattern matches
|
||||||
|
reject []string // inputs the tail guard alone rejects
|
||||||
|
}{
|
||||||
|
{`^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$`, true, []string{"a", "localhost", "x-1"}, []string{"www.example.com", "localhost.", "LOCALHOST", "a b"}},
|
||||||
|
{`(^|\.)[a-z][1-9][0-9][a-z]\.com$`, true, []string{"a12b.com", "x.q10z.com"}, []string{"google.com", "a12b.co", "a12b.com.", "ab12.com"}},
|
||||||
|
{`^hses[1-7]?\.akamaized\.net$`, true, []string{"hses.akamaized.net", "hses3.akamaized.net"}, []string{"xhses.akamaized.net", "www.hses.akamaized.net"}},
|
||||||
|
{`(?i)k\.net$`, true, []string{"k.net", "K.NET", "\u212a.net"}, []string{"x.net", "k.nex"}},
|
||||||
|
{`[^.]+\.cn$`, true, []string{"a.cn", "\xff.cn", "\u4e2d.cn"}, []string{"a.cnn", "a.c"}},
|
||||||
|
{`\x{FFFD}$`, true, []string{"\xff", "a\xc3", "\uFFFD"}, []string{"a", "\xff."}},
|
||||||
|
{`^.\.cn$`, true, []string{"a.cn", "\u4E2D.cn", "\xff.cn"}, []string{"ab.cn"}},
|
||||||
|
{`^$`, true, []string{""}, []string{"a"}},
|
||||||
|
{`(^|\.)youyuapi\..+$`, false, []string{"youyuapi.com"}, nil},
|
||||||
|
{`abc`, false, []string{"abc", "xabcx"}, nil},
|
||||||
|
{`^ab`, false, []string{"ab", "abc"}, nil},
|
||||||
|
{`a$|b`, false, []string{"a", "bx"}, nil},
|
||||||
|
{`(?m)a$`, false, []string{"a", "a\nb"}, nil},
|
||||||
|
{strings.Repeat(`(?:abcdefgh(?:a`, 20) + strings.Repeat(`)*)*`, 20) + `\.com$`, false, []string{".com", "abcdefgha.com"}, nil}, // over tailBudget
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegexTailGuard(t *testing.T) {
|
||||||
|
for _, test := range regexTailCases {
|
||||||
|
m, err := newRegexMatcher(test.pattern)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rm := m.(*RegexMatcher)
|
||||||
|
if guard := rm.tail != nil || rm.rest != nil; guard != test.guard {
|
||||||
|
t.Errorf("%s: guard %v, want %v", test.pattern, guard, test.guard)
|
||||||
|
}
|
||||||
|
for _, s := range test.match {
|
||||||
|
if !rm.pattern.MatchString(s) || !rm.Match(s) {
|
||||||
|
t.Errorf("%s: %q does not match", test.pattern, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range test.reject {
|
||||||
|
if rm.pattern.MatchString(s) || rm.mayMatch(s) {
|
||||||
|
t.Errorf("%s: %q passes the guard", test.pattern, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRegexTailGuardFlatAlternation checks that a long but non-recursive pattern keeps its
|
||||||
|
// guard. Only nested repeats are charged against tailBudget, so a flat alternation of many
|
||||||
|
// names, however large, is walked once and guarded; its guard is checked against regexp.
|
||||||
|
func TestRegexTailGuardFlatAlternation(t *testing.T) {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("(?:")
|
||||||
|
for i := 0; i < 20000; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteByte('|')
|
||||||
|
}
|
||||||
|
sb.WriteString("name")
|
||||||
|
sb.WriteString(strconv.Itoa(i))
|
||||||
|
}
|
||||||
|
sb.WriteString(`)\.example\.com$`)
|
||||||
|
m, err := newRegexMatcher(sb.String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
rm := m.(*RegexMatcher)
|
||||||
|
if rm.tail == nil && rm.rest == nil {
|
||||||
|
t.Fatal("flat alternation of 20000 names lost its guard")
|
||||||
|
}
|
||||||
|
for _, s := range []string{"name0.example.com", "name19999.example.com", "x.name12345.example.com"} {
|
||||||
|
if !rm.pattern.MatchString(s) || !rm.Match(s) {
|
||||||
|
t.Errorf("%q should match", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range []string{"name0.example.org", "name0.example.com.", "name0.example.con", "google.com"} {
|
||||||
|
if rm.pattern.MatchString(s) {
|
||||||
|
t.Fatalf("test bug: %q matches the pattern", s)
|
||||||
|
}
|
||||||
|
if rm.mayMatch(s) {
|
||||||
|
t.Errorf("%q should be rejected by the guard", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sampleMatch appends a string that re matches, assertions aside, unless it runs out of
|
||||||
|
// budget, which it spends one per call so that nested repeats stay cheap.
|
||||||
|
func sampleMatch(sb *strings.Builder, re *syntax.Regexp, rnd *rand.Rand, budget *int) {
|
||||||
|
if *budget <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
*budget--
|
||||||
|
switch re.Op {
|
||||||
|
case syntax.OpLiteral:
|
||||||
|
for _, r := range re.Rune {
|
||||||
|
if re.Flags&syntax.FoldCase != 0 {
|
||||||
|
for n := rnd.IntN(4); n > 0; n-- {
|
||||||
|
r = unicode.SimpleFold(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sampleRune(sb, r, rnd)
|
||||||
|
}
|
||||||
|
case syntax.OpCharClass:
|
||||||
|
if len(re.Rune) > 0 {
|
||||||
|
i := rnd.IntN(len(re.Rune)/2) * 2
|
||||||
|
sampleRune(sb, re.Rune[i]+rnd.Int32N(min(re.Rune[i+1]-re.Rune[i]+1, 300)), rnd)
|
||||||
|
}
|
||||||
|
case syntax.OpAnyChar, syntax.OpAnyCharNotNL:
|
||||||
|
sampleRune(sb, []rune{'a', '.', '\n', 0xe9, 0x212a, utf8.RuneError}[rnd.IntN(6)], rnd)
|
||||||
|
case syntax.OpCapture:
|
||||||
|
sampleMatch(sb, re.Sub[0], rnd, budget)
|
||||||
|
case syntax.OpConcat:
|
||||||
|
for _, sub := range re.Sub {
|
||||||
|
sampleMatch(sb, sub, rnd, budget)
|
||||||
|
}
|
||||||
|
case syntax.OpAlternate:
|
||||||
|
sampleMatch(sb, re.Sub[rnd.IntN(len(re.Sub))], rnd, budget)
|
||||||
|
case syntax.OpQuest, syntax.OpStar, syntax.OpPlus, syntax.OpRepeat:
|
||||||
|
lo, hi := 0, 3
|
||||||
|
switch re.Op {
|
||||||
|
case syntax.OpQuest:
|
||||||
|
hi = 1
|
||||||
|
case syntax.OpPlus:
|
||||||
|
lo = 1
|
||||||
|
case syntax.OpRepeat:
|
||||||
|
lo, hi = re.Min, re.Min+3
|
||||||
|
if re.Max >= 0 {
|
||||||
|
hi = min(hi, re.Max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for n := lo + rnd.IntN(hi-lo+1); n > 0; n-- {
|
||||||
|
sampleMatch(sb, re.Sub[0], rnd, budget)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sampleRune(sb *strings.Builder, r rune, rnd *rand.Rand) {
|
||||||
|
if r == utf8.RuneError && rnd.IntN(2) == 0 {
|
||||||
|
sb.WriteByte(0x80 | byte(rnd.IntN(0x80))) // regexp matches an invalid byte as U+FFFD
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sb.WriteRune(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FuzzRegexMatcher(f *testing.F) {
|
||||||
|
inputs := []string{
|
||||||
|
"", "x", "yy", "abd", "ccc", "ABC", "abCDef", "abcdef", "abababcc", "a.b", "a\xffb", "a\uFFFDb",
|
||||||
|
"www.91porn.com", "ns1.awsdns-cn-01.top", "r1---sn-2x3abcde.googlevideo.com",
|
||||||
|
}
|
||||||
|
for _, test := range regexLiteralCases {
|
||||||
|
for _, s := range inputs {
|
||||||
|
f.Add(test.pattern, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, test := range regexTailCases {
|
||||||
|
for _, s := range append(test.match, test.reject...) {
|
||||||
|
f.Add(test.pattern, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.Fuzz(func(t *testing.T, pattern, s string) {
|
||||||
|
re, err := regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m, _ := newRegexMatcher(pattern)
|
||||||
|
check := func(s string) {
|
||||||
|
if got, want := m.Match(s), re.MatchString(s); got != want {
|
||||||
|
t.Errorf("pattern %q, input %q: got %v, want %v", pattern, s, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(s)
|
||||||
|
// random inputs seldom match, so also try strings built from the pattern
|
||||||
|
parsed, _ := syntax.Parse(pattern, syntax.Perl)
|
||||||
|
h := fnv.New64a()
|
||||||
|
h.Write([]byte(s))
|
||||||
|
rnd := rand.New(rand.NewPCG(h.Sum64(), 1))
|
||||||
|
for range 8 {
|
||||||
|
var sb strings.Builder
|
||||||
|
budget := 256
|
||||||
|
sampleMatch(&sb, parsed, rnd, &budget)
|
||||||
|
sample := sb.String()
|
||||||
|
check(sample)
|
||||||
|
check(s + sample)
|
||||||
|
if len(sample) > 0 && len(s) > 0 {
|
||||||
|
i := rnd.IntN(len(sample))
|
||||||
|
check(sample[:i] + s[:1] + sample[i+1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -46,7 +46,9 @@ func (g *MphValueMatcher) Add(matcher Matcher, value uint32) {
|
|||||||
func (g *MphValueMatcher) Build() error {
|
func (g *MphValueMatcher) Build() error {
|
||||||
if g.mph != nil {
|
if g.mph != nil {
|
||||||
runtime.GC() // peak mem
|
runtime.GC() // peak mem
|
||||||
g.mph.Build()
|
if err := g.mph.Build(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
runtime.GC() // peak mem
|
runtime.GC() // peak mem
|
||||||
if g.ac != nil {
|
if g.ac != nil {
|
||||||
@@ -58,23 +60,17 @@ func (g *MphValueMatcher) Build() error {
|
|||||||
|
|
||||||
// Match implements ValueMatcher.Match.
|
// Match implements ValueMatcher.Match.
|
||||||
func (g *MphValueMatcher) Match(input string) []uint32 {
|
func (g *MphValueMatcher) Match(input string) []uint32 {
|
||||||
result := make([][]uint32, 0, 5)
|
var result []uint32
|
||||||
if g.mph != nil {
|
if g.mph != nil {
|
||||||
if matches := g.mph.Match(input); len(matches) > 0 {
|
result = g.mph.Match(input) // a new slice, returned without another copy
|
||||||
result = append(result, matches)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if g.ac != nil {
|
if g.ac != nil {
|
||||||
if matches := g.ac.Match(input); len(matches) > 0 {
|
result = append(result, g.ac.Match(input)...)
|
||||||
result = append(result, matches)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if g.regex != nil {
|
if g.regex != nil {
|
||||||
if matches := g.regex.Match(input); len(matches) > 0 {
|
result = append(result, g.regex.Match(input)...)
|
||||||
result = append(result, matches)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return CompositeMatches(result)
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// MatchAny implements ValueMatcher.MatchAny.
|
// MatchAny implements ValueMatcher.MatchAny.
|
||||||
@@ -87,3 +83,62 @@ func (g *MphValueMatcher) MatchAny(input string) bool {
|
|||||||
}
|
}
|
||||||
return g.regex != nil && g.regex.MatchAny(input)
|
return g.regex != nil && g.regex.MatchAny(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *MphValueMatcher) matchAnyHashed(input string, parents []mphSuffix, h, mul uint64) bool {
|
||||||
|
if g.mph != nil && g.mph.matchAnyHashed(input, parents, h, mul) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if g.ac != nil && g.ac.MatchAny(input) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return g.regex != nil && g.regex.MatchAny(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MphValueMatcherCombiner combines several built MphValueMatchers, each bound to one value, and matches an input
|
||||||
|
// against them as their MatchAny would, hashing the input once for all of them.
|
||||||
|
type MphValueMatcherCombiner struct {
|
||||||
|
matchers []*MphValueMatcher
|
||||||
|
values []uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds a built matcher that stands for value.
|
||||||
|
func (s *MphValueMatcherCombiner) Add(m *MphValueMatcher, value uint32) {
|
||||||
|
s.matchers = append(s.matchers, m)
|
||||||
|
s.values = append(s.values, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match returns the values of the matchers that match input, in Add order.
|
||||||
|
func (s *MphValueMatcherCombiner) Match(input string) []uint32 {
|
||||||
|
if len(s.matchers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var stack [16]mphSuffix
|
||||||
|
mul := mphMultipliers[0]
|
||||||
|
parents, h := mphSuffixes(stack[:0], mul, input)
|
||||||
|
var result []uint32
|
||||||
|
for i, m := range s.matchers {
|
||||||
|
if m.matchAnyHashed(input, parents, h, mul) {
|
||||||
|
result = append(result, s.values[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchAny returns true as soon as one matcher matches input.
|
||||||
|
func (s *MphValueMatcherCombiner) MatchAny(input string) bool {
|
||||||
|
switch len(s.matchers) {
|
||||||
|
case 0:
|
||||||
|
return false
|
||||||
|
case 1:
|
||||||
|
return s.matchers[0].MatchAny(input) // nothing to share, and it stops at the first matching suffix
|
||||||
|
}
|
||||||
|
var stack [16]mphSuffix
|
||||||
|
mul := mphMultipliers[0]
|
||||||
|
parents, h := mphSuffixes(stack[:0], mul, input)
|
||||||
|
for _, m := range s.matchers {
|
||||||
|
if m.matchAnyHashed(input, parents, h, mul) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
+21
-25
@@ -1,7 +1,7 @@
|
|||||||
package log // import "github.com/xtls/xray-core/common/log"
|
package log // import "github.com/xtls/xray-core/common/log"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/serial"
|
"github.com/xtls/xray-core/common/serial"
|
||||||
)
|
)
|
||||||
@@ -29,36 +29,32 @@ func (m *GeneralMessage) String() string {
|
|||||||
|
|
||||||
// Record writes a message into log stream.
|
// Record writes a message into log stream.
|
||||||
func Record(msg Message) {
|
func Record(msg Message) {
|
||||||
logHandler.Handle(msg)
|
if h := logHandler.Load(); h != nil {
|
||||||
|
(*h).Handle(msg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var logHandler syncHandler
|
type SeverityLogger interface {
|
||||||
|
Handler
|
||||||
|
Severity() Severity
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSeverity() Severity {
|
||||||
|
if h := logHandler.Load(); h != nil {
|
||||||
|
if sh, ok := (*h).(SeverityLogger); ok {
|
||||||
|
return sh.Severity()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// log everything by default
|
||||||
|
return Severity_Debug
|
||||||
|
}
|
||||||
|
|
||||||
|
var logHandler atomic.Pointer[Handler]
|
||||||
|
|
||||||
// RegisterHandler registers a new handler as current log handler. Previous registered handler will be discarded.
|
// RegisterHandler registers a new handler as current log handler. Previous registered handler will be discarded.
|
||||||
func RegisterHandler(handler Handler) {
|
func RegisterHandler(handler Handler) {
|
||||||
if handler == nil {
|
if handler == nil {
|
||||||
panic("Log handler is nil")
|
panic("Log handler is nil")
|
||||||
}
|
}
|
||||||
logHandler.Set(handler)
|
logHandler.Store(&handler)
|
||||||
}
|
|
||||||
|
|
||||||
type syncHandler struct {
|
|
||||||
sync.RWMutex
|
|
||||||
Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *syncHandler) Handle(msg Message) {
|
|
||||||
h.RLock()
|
|
||||||
defer h.RUnlock()
|
|
||||||
|
|
||||||
if h.Handler != nil {
|
|
||||||
h.Handler.Handle(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *syncHandler) Set(handler Handler) {
|
|
||||||
h.Lock()
|
|
||||||
defer h.Unlock()
|
|
||||||
|
|
||||||
h.Handler = handler
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ func (l *serverityLogger) Handle(msg Message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *serverityLogger) Severity() Severity {
|
||||||
|
return l.logLevel
|
||||||
|
}
|
||||||
|
|
||||||
func (l *generalLogger) run() {
|
func (l *generalLogger) run() {
|
||||||
defer l.access.Signal()
|
defer l.access.Signal()
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func (m *ClientManager) Dispatch(ctx context.Context, link *transport.Link) erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New("unable to find an available mux client").AtWarning()
|
return errors.New("unable to find an available mux client")
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkerPicker interface {
|
type WorkerPicker interface {
|
||||||
|
|||||||
+1
-1
@@ -117,7 +117,7 @@ func (f *FrameMetadata) Unmarshal(reader io.Reader, readSourceAndLocal bool) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if metaLen > 512 {
|
if metaLen > 512 {
|
||||||
return errors.New("invalid metalen ", metaLen).AtError()
|
return errors.New("invalid metalen ", metaLen)
|
||||||
}
|
}
|
||||||
|
|
||||||
b := buf.New()
|
b := buf.New()
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ func (w *ServerWorker) handleFrame(ctx context.Context, reader *buf.BufferedRead
|
|||||||
err = w.handleStatusKeep(&meta, reader)
|
err = w.handleStatusKeep(&meta, reader)
|
||||||
default:
|
default:
|
||||||
status := meta.SessionStatus
|
status := meta.SessionStatus
|
||||||
return errors.New("unknown status: ", status).AtError()
|
return errors.New("unknown status: ", status)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
// PacketConnWrapper wraps a PacketConn into a Conn with a fixed destination address.
|
||||||
|
type PacketConnWrapper struct {
|
||||||
|
PacketConn
|
||||||
|
Dest Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PacketConnWrapper) Read(p []byte) (int, error) {
|
||||||
|
n, _, err := c.PacketConn.ReadFrom(p)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PacketConnWrapper) Write(p []byte) (int, error) {
|
||||||
|
return c.PacketConn.WriteTo(p, c.Dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PacketConnWrapper) RemoteAddr() Addr {
|
||||||
|
return c.Dest
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
func (u *User) GetTypedAccount() (Account, error) {
|
func (u *User) GetTypedAccount() (Account, error) {
|
||||||
if u.GetAccount() == nil {
|
if u.GetAccount() == nil {
|
||||||
return nil, errors.New("Account is missing").AtWarning()
|
return nil, errors.New("Account is missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
rawAccount, err := u.Account.GetInstance()
|
rawAccount, err := u.Account.GetInstance()
|
||||||
|
|||||||
@@ -70,8 +70,6 @@ type Outbound struct {
|
|||||||
Tag string
|
Tag string
|
||||||
// Name of the outbound proxy that handles the connection.
|
// Name of the outbound proxy that handles the connection.
|
||||||
Name string
|
Name string
|
||||||
// Unused. Conn is actually internet.Connection. May be nil. It is currently nil for outbound with proxySettings
|
|
||||||
Conn net.Conn
|
|
||||||
// CanSpliceCopy is a property for this connection
|
// CanSpliceCopy is a property for this connection
|
||||||
// 1 = can, 2 = after processing protocol info should be able to, 3 = cannot
|
// 1 = can, 2 = after processing protocol info should be able to, 3 = cannot
|
||||||
CanSpliceCopy int
|
CanSpliceCopy int
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
package singbridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/xtls/xray-core/common/errors"
|
|
||||||
"github.com/xtls/xray-core/common/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ToNetwork(network string) net.Network {
|
|
||||||
switch N.NetworkName(network) {
|
|
||||||
case N.NetworkTCP:
|
|
||||||
return net.Network_TCP
|
|
||||||
case N.NetworkUDP:
|
|
||||||
return net.Network_UDP
|
|
||||||
default:
|
|
||||||
return net.Network_Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToDestination(socksaddr M.Socksaddr, network net.Network) (net.Destination, error) {
|
|
||||||
// IsFqdn() implicitly checks if the domain name is valid
|
|
||||||
if socksaddr.IsFqdn() {
|
|
||||||
return net.Destination{
|
|
||||||
Network: network,
|
|
||||||
Address: net.DomainAddress(socksaddr.Fqdn),
|
|
||||||
Port: net.Port(socksaddr.Port),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsIP() implicitly checks if the IP address is valid
|
|
||||||
if socksaddr.IsIP() {
|
|
||||||
return net.Destination{
|
|
||||||
Network: network,
|
|
||||||
Address: net.IPAddress(socksaddr.Addr.AsSlice()),
|
|
||||||
Port: net.Port(socksaddr.Port),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return net.Destination{}, errors.New("invalid socks address: ", socksaddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToSocksaddr(destination net.Destination) M.Socksaddr {
|
|
||||||
var addr M.Socksaddr
|
|
||||||
switch destination.Address.Family() {
|
|
||||||
case net.AddressFamilyDomain:
|
|
||||||
addr.Fqdn = destination.Address.Domain()
|
|
||||||
default:
|
|
||||||
addr.Addr = M.AddrFromIP(destination.Address.IP())
|
|
||||||
}
|
|
||||||
addr.Port = uint16(destination.Port)
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package singbridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/xtls/xray-core/common/net"
|
|
||||||
"github.com/xtls/xray-core/common/net/cnc"
|
|
||||||
"github.com/xtls/xray-core/common/session"
|
|
||||||
"github.com/xtls/xray-core/proxy"
|
|
||||||
"github.com/xtls/xray-core/transport"
|
|
||||||
"github.com/xtls/xray-core/transport/internet"
|
|
||||||
"github.com/xtls/xray-core/transport/pipe"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ N.Dialer = (*XrayDialer)(nil)
|
|
||||||
|
|
||||||
type XrayDialer struct {
|
|
||||||
internet.Dialer
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDialer(dialer internet.Dialer) *XrayDialer {
|
|
||||||
return &XrayDialer{dialer}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *XrayDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
|
||||||
dest, err := ToDestination(destination, ToNetwork(network))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return d.Dialer.Dial(ctx, dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *XrayDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
|
||||||
return nil, os.ErrInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
type XrayOutboundDialer struct {
|
|
||||||
outbound proxy.Outbound
|
|
||||||
dialer internet.Dialer
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOutboundDialer(outbound proxy.Outbound, dialer internet.Dialer) *XrayOutboundDialer {
|
|
||||||
return &XrayOutboundDialer{outbound, dialer}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *XrayOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
|
||||||
dest, err := ToDestination(destination, ToNetwork(network))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
outbounds := session.OutboundsFromContext(ctx)
|
|
||||||
if len(outbounds) == 0 {
|
|
||||||
outbounds = []*session.Outbound{{}}
|
|
||||||
ctx = session.ContextWithOutbounds(ctx, outbounds)
|
|
||||||
}
|
|
||||||
ob := outbounds[len(outbounds)-1]
|
|
||||||
ob.Target = dest
|
|
||||||
|
|
||||||
opts := []pipe.Option{pipe.WithSizeLimit(64 * 1024)}
|
|
||||||
uplinkReader, uplinkWriter := pipe.New(opts...)
|
|
||||||
downlinkReader, downlinkWriter := pipe.New(opts...)
|
|
||||||
conn := cnc.NewConnection(cnc.ConnectionInputMulti(downlinkWriter), cnc.ConnectionOutputMulti(uplinkReader))
|
|
||||||
go d.outbound.Process(ctx, &transport.Link{Reader: downlinkReader, Writer: uplinkWriter}, d.dialer)
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *XrayOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
|
||||||
return nil, os.ErrInvalid
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package singbridge
|
|
||||||
|
|
||||||
import E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
func ReturnError(err error) error {
|
|
||||||
if E.IsClosedOrCanceled(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package singbridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"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/features/routing"
|
|
||||||
"github.com/xtls/xray-core/transport"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ N.TCPConnectionHandler = (*Dispatcher)(nil)
|
|
||||||
_ N.UDPConnectionHandler = (*Dispatcher)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
type Dispatcher struct {
|
|
||||||
upstream routing.Dispatcher
|
|
||||||
newErrorFunc func(values ...any) *errors.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDispatcher(dispatcher routing.Dispatcher, newErrorFunc func(values ...any) *errors.Error) *Dispatcher {
|
|
||||||
return &Dispatcher{
|
|
||||||
upstream: dispatcher,
|
|
||||||
newErrorFunc: newErrorFunc,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dispatcher) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
|
||||||
dest, err := ToDestination(metadata.Destination, net.Network_TCP)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
xConn := NewConn(conn)
|
|
||||||
return d.upstream.DispatchLink(ctx, dest, &transport.Link{
|
|
||||||
Reader: xConn,
|
|
||||||
Writer: xConn,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dispatcher) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
|
||||||
dest, err := ToDestination(metadata.Destination, net.Network_UDP)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return d.upstream.DispatchLink(ctx, dest, &transport.Link{
|
|
||||||
Reader: buf.NewPacketReader(conn.(io.Reader)),
|
|
||||||
Writer: buf.NewWriter(conn.(io.Writer)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dispatcher) NewError(ctx context.Context, err error) {
|
|
||||||
errors.LogInfo(ctx, err.Error())
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package singbridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
"github.com/xtls/xray-core/common/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ logger.ContextLogger = (*XrayLogger)(nil)
|
|
||||||
|
|
||||||
type XrayLogger struct {
|
|
||||||
newError func(values ...any) *errors.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLogger(newErrorFunc func(values ...any) *errors.Error) *XrayLogger {
|
|
||||||
return &XrayLogger{
|
|
||||||
newErrorFunc,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *XrayLogger) Trace(args ...any) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *XrayLogger) Debug(args ...any) {
|
|
||||||
errors.LogDebug(context.Background(), args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *XrayLogger) Info(args ...any) {
|
|
||||||
errors.LogInfo(context.Background(), args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *XrayLogger) Warn(args ...any) {
|
|
||||||
errors.LogWarning(context.Background(), args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *XrayLogger) Error(args ...any) {
|
|
||||||
errors.LogError(context.Background(), args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *XrayLogger) Fatal(args ...any) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *XrayLogger) Panic(args ...any) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *XrayLogger) TraceContext(ctx context.Context, args ...any) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *XrayLogger) DebugContext(ctx context.Context, args ...any) {
|
|
||||||
errors.LogDebug(ctx, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *XrayLogger) InfoContext(ctx context.Context, args ...any) {
|
|
||||||
errors.LogInfo(ctx, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *XrayLogger) WarnContext(ctx context.Context, args ...any) {
|
|
||||||
errors.LogWarning(ctx, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *XrayLogger) ErrorContext(ctx context.Context, args ...any) {
|
|
||||||
errors.LogError(ctx, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *XrayLogger) FatalContext(ctx context.Context, args ...any) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *XrayLogger) PanicContext(ctx context.Context, args ...any) {
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
package singbridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
B "github.com/sagernet/sing/common/buf"
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
"github.com/xtls/xray-core/common"
|
|
||||||
"github.com/xtls/xray-core/common/buf"
|
|
||||||
"github.com/xtls/xray-core/common/net"
|
|
||||||
"github.com/xtls/xray-core/common/signal"
|
|
||||||
"github.com/xtls/xray-core/transport"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CopyPacketConn(ctx context.Context, inboundConn net.Conn, link *transport.Link, destination net.Destination, serverConn net.PacketConn) error {
|
|
||||||
cancel := func() {
|
|
||||||
common.Interrupt(link.Reader)
|
|
||||||
common.Interrupt(serverConn)
|
|
||||||
}
|
|
||||||
conn := &PacketConnWrapper{
|
|
||||||
Reader: link.Reader,
|
|
||||||
Writer: link.Writer,
|
|
||||||
Dest: destination,
|
|
||||||
Conn: inboundConn,
|
|
||||||
T: signal.CancelAfterInactivity(ctx, cancel, 300*time.Second),
|
|
||||||
}
|
|
||||||
return ReturnError(bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(serverConn)))
|
|
||||||
}
|
|
||||||
|
|
||||||
type PacketConnWrapper struct {
|
|
||||||
buf.Reader
|
|
||||||
buf.Writer
|
|
||||||
net.Conn
|
|
||||||
Dest net.Destination
|
|
||||||
cached buf.MultiBuffer
|
|
||||||
|
|
||||||
// A simple patch to avoid goroutine leak since sing infra cannot awake read block by write err
|
|
||||||
T *signal.ActivityTimer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *PacketConnWrapper) ReadPacket(buffer *B.Buffer) (addr M.Socksaddr, err error) {
|
|
||||||
w.T.Update()
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
// uplinkonly
|
|
||||||
w.T.SetTimeout(2 * time.Second)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if w.cached != nil {
|
|
||||||
mb, bb := buf.SplitFirst(w.cached)
|
|
||||||
if bb == nil {
|
|
||||||
w.cached = nil
|
|
||||||
} else {
|
|
||||||
buffer.Write(bb.Bytes())
|
|
||||||
w.cached = mb
|
|
||||||
var destination net.Destination
|
|
||||||
if bb.UDP != nil {
|
|
||||||
destination = *bb.UDP
|
|
||||||
} else {
|
|
||||||
destination = w.Dest
|
|
||||||
}
|
|
||||||
bb.Release()
|
|
||||||
return ToSocksaddr(destination), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mb, err := w.ReadMultiBuffer()
|
|
||||||
nb, bb := buf.SplitFirst(mb)
|
|
||||||
if bb == nil {
|
|
||||||
return M.Socksaddr{}, nil
|
|
||||||
} else {
|
|
||||||
buffer.Write(bb.Bytes())
|
|
||||||
w.cached = nb
|
|
||||||
var destination net.Destination
|
|
||||||
if bb.UDP != nil {
|
|
||||||
destination = *bb.UDP
|
|
||||||
} else {
|
|
||||||
destination = w.Dest
|
|
||||||
}
|
|
||||||
bb.Release()
|
|
||||||
return ToSocksaddr(destination), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *PacketConnWrapper) WritePacket(buffer *B.Buffer, destination M.Socksaddr) (err error) {
|
|
||||||
w.T.Update()
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
// downlinkonly
|
|
||||||
w.T.SetTimeout(5 * time.Second)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
endpoint, err := ToDestination(destination, net.Network_UDP)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
vBuf := buf.New()
|
|
||||||
vBuf.Write(buffer.Bytes())
|
|
||||||
vBuf.UDP = &endpoint
|
|
||||||
return w.WriteMultiBuffer(buf.MultiBuffer{vBuf})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *PacketConnWrapper) Close() error {
|
|
||||||
buf.ReleaseMulti(w.cached)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package singbridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
"github.com/xtls/xray-core/common"
|
|
||||||
"github.com/xtls/xray-core/common/buf"
|
|
||||||
"github.com/xtls/xray-core/common/signal"
|
|
||||||
"github.com/xtls/xray-core/transport"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CopyConn(ctx context.Context, inboundConn net.Conn, link *transport.Link, serverConn net.Conn) error {
|
|
||||||
conn := &PipeConnWrapper{
|
|
||||||
W: link.Writer,
|
|
||||||
Conn: inboundConn,
|
|
||||||
}
|
|
||||||
if ir, ok := link.Reader.(io.Reader); ok {
|
|
||||||
conn.R = ir
|
|
||||||
} else {
|
|
||||||
conn.R = &buf.BufferedReader{Reader: link.Reader}
|
|
||||||
}
|
|
||||||
cancel := func() {
|
|
||||||
common.Interrupt(link.Reader)
|
|
||||||
common.Interrupt(serverConn)
|
|
||||||
}
|
|
||||||
conn.T = signal.CancelAfterInactivity(ctx, cancel, 300*time.Second)
|
|
||||||
return ReturnError(bufio.CopyConn(ctx, conn, serverConn))
|
|
||||||
}
|
|
||||||
|
|
||||||
type PipeConnWrapper struct {
|
|
||||||
R io.Reader
|
|
||||||
W buf.Writer
|
|
||||||
net.Conn
|
|
||||||
|
|
||||||
// A simple patch to avoid goroutine leak since sing infra cannot awake read block by write err
|
|
||||||
T *signal.ActivityTimer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *PipeConnWrapper) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *PipeConnWrapper) Read(b []byte) (n int, err error) {
|
|
||||||
w.T.Update()
|
|
||||||
n, err = w.R.Read(b)
|
|
||||||
if err != nil {
|
|
||||||
// uplinkonly
|
|
||||||
w.T.SetTimeout(2 * time.Second)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *PipeConnWrapper) Write(p []byte) (n int, err error) {
|
|
||||||
w.T.Update()
|
|
||||||
n = len(p)
|
|
||||||
var mb buf.MultiBuffer
|
|
||||||
pLen := len(p)
|
|
||||||
for pLen > 0 {
|
|
||||||
buffer := buf.New()
|
|
||||||
if pLen > buf.Size {
|
|
||||||
_, err = buffer.Write(p[:buf.Size])
|
|
||||||
p = p[buf.Size:]
|
|
||||||
} else {
|
|
||||||
buffer.Write(p)
|
|
||||||
}
|
|
||||||
pLen -= int(buffer.Len())
|
|
||||||
mb = append(mb, buffer)
|
|
||||||
}
|
|
||||||
err = w.W.WriteMultiBuffer(mb)
|
|
||||||
if err != nil {
|
|
||||||
n = 0
|
|
||||||
buf.ReleaseMulti(mb)
|
|
||||||
// downlinkonly
|
|
||||||
w.T.SetTimeout(5 * time.Second)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package singbridge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/xtls/xray-core/common/buf"
|
|
||||||
"github.com/xtls/xray-core/common/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ buf.Reader = (*Conn)(nil)
|
|
||||||
_ buf.TimeoutReader = (*Conn)(nil)
|
|
||||||
_ buf.Writer = (*Conn)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
type Conn struct {
|
|
||||||
net.Conn
|
|
||||||
writer N.VectorisedWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConn(conn net.Conn) *Conn {
|
|
||||||
writer, _ := bufio.CreateVectorisedWriter(conn)
|
|
||||||
return &Conn{
|
|
||||||
Conn: conn,
|
|
||||||
writer: writer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
|
||||||
buffer, err := buf.ReadBuffer(c.Conn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return buf.MultiBuffer{buffer}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) ReadMultiBufferTimeout(duration time.Duration) (buf.MultiBuffer, error) {
|
|
||||||
err := c.SetReadDeadline(time.Now().Add(duration))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer c.SetReadDeadline(time.Time{})
|
|
||||||
return c.ReadMultiBuffer()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) WriteMultiBuffer(bufferList buf.MultiBuffer) error {
|
|
||||||
defer buf.ReleaseMulti(bufferList)
|
|
||||||
if c.writer != nil {
|
|
||||||
bytesList := make([][]byte, len(bufferList))
|
|
||||||
for i, buffer := range bufferList {
|
|
||||||
bytesList[i] = buffer.Bytes()
|
|
||||||
}
|
|
||||||
return common.Error(bufio.WriteVectorised(c.writer, bytesList))
|
|
||||||
}
|
|
||||||
// Since this conn is only used by tun, we don't force buffer writes to merge.
|
|
||||||
for _, buffer := range bufferList {
|
|
||||||
_, err := c.Conn.Write(buffer.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
+2
-2
@@ -16,7 +16,7 @@ var typeCreatorRegistry = make(map[reflect.Type]ConfigCreator)
|
|||||||
func RegisterConfig(config interface{}, configCreator ConfigCreator) error {
|
func RegisterConfig(config interface{}, configCreator ConfigCreator) error {
|
||||||
configType := reflect.TypeOf(config)
|
configType := reflect.TypeOf(config)
|
||||||
if _, found := typeCreatorRegistry[configType]; found {
|
if _, found := typeCreatorRegistry[configType]; found {
|
||||||
return errors.New(configType.Name() + " is already registered").AtError()
|
return errors.New(configType.Name() + " is already registered")
|
||||||
}
|
}
|
||||||
typeCreatorRegistry[configType] = configCreator
|
typeCreatorRegistry[configType] = configCreator
|
||||||
return nil
|
return nil
|
||||||
@@ -27,7 +27,7 @@ func CreateObject(ctx context.Context, config interface{}) (interface{}, error)
|
|||||||
configType := reflect.TypeOf(config)
|
configType := reflect.TypeOf(config)
|
||||||
creator, found := typeCreatorRegistry[configType]
|
creator, found := typeCreatorRegistry[configType]
|
||||||
if !found {
|
if !found {
|
||||||
return nil, errors.New(configType.String() + " is not registered").AtError()
|
return nil, errors.New(configType.String() + " is not registered")
|
||||||
}
|
}
|
||||||
return creator(ctx, config)
|
return creator(ctx, config)
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -125,7 +125,7 @@ func LoadConfig(formatName string, input interface{}) (*Config, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if f == "" {
|
if f == "" {
|
||||||
return nil, errors.New("Failed to get format of ", file).AtWarning()
|
return nil, errors.New("Failed to get format of ", file)
|
||||||
}
|
}
|
||||||
|
|
||||||
if f == "protobuf" {
|
if f == "protobuf" {
|
||||||
@@ -142,7 +142,7 @@ func LoadConfig(formatName string, input interface{}) (*Config, error) {
|
|||||||
if len(v) == 1 {
|
if len(v) == 1 {
|
||||||
return configLoaderByName["protobuf"].Loader(v)
|
return configLoaderByName["protobuf"].Loader(v)
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("Only one protobuf config file is allowed").AtWarning()
|
return nil, errors.New("Only one protobuf config file is allowed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,11 +152,11 @@ func LoadConfig(formatName string, input interface{}) (*Config, error) {
|
|||||||
if f, found := configLoaderByName[formatName]; found {
|
if f, found := configLoaderByName[formatName]; found {
|
||||||
return f.Loader(v)
|
return f.Loader(v)
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New("Unable to load config in", formatName).AtWarning()
|
return nil, errors.New("Unable to load config in", formatName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New("Unable to load config").AtWarning()
|
return nil, errors.New("Unable to load config")
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadProtobufConfig(data []byte) (*Config, error) {
|
func loadProtobufConfig(data []byte) (*Config, error) {
|
||||||
|
|||||||
+2
-2
@@ -19,8 +19,8 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
Version_x byte = 26
|
Version_x byte = 26
|
||||||
Version_y byte = 7
|
Version_y byte = 9
|
||||||
Version_z byte = 28
|
Version_z byte = 30
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type FakeDNSEngine interface {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
FakeIPv4Pool = "198.18.0.0/15"
|
FakeIPv4Pool = "198.18.0.0/15"
|
||||||
FakeIPv6Pool = "fc00::/18"
|
FakeIPv6Pool = "2001:2::/48"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FakeDNSEngineRev0 interface {
|
type FakeDNSEngineRev0 interface {
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ func New() *Client {
|
|||||||
r := &net.Resolver{
|
r := &net.Resolver{
|
||||||
PreferGo: true,
|
PreferGo: true,
|
||||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
if internet.IsSkippedDNSServer(address) {
|
||||||
|
return nil, errors.New("skipped DNS server ", address)
|
||||||
|
}
|
||||||
return d.DialContext(ctx, network, address)
|
return d.DialContext(ctx, network, address)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package localdns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/transport/internet"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSkippedDNSServers(t *testing.T) {
|
||||||
|
internet.SkipDNSServers([]netip.Addr{netip.MustParseAddr("203.0.113.53")})
|
||||||
|
t.Cleanup(func() { internet.SkipDNSServers(nil) })
|
||||||
|
c := New()
|
||||||
|
if _, err := c.r.Dial(context.Background(), "udp", "203.0.113.53:53"); err == nil {
|
||||||
|
t.Error("a skipped DNS server was dialed")
|
||||||
|
}
|
||||||
|
conn, err := c.r.Dial(context.Background(), "udp", "127.0.0.1:53")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
module github.com/xtls/xray-core
|
module github.com/xtls/xray-core
|
||||||
|
|
||||||
go 1.26
|
go 1.27
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
|
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
|
||||||
@@ -18,25 +18,24 @@ require (
|
|||||||
github.com/pires/go-proxyproto v0.15.0
|
github.com/pires/go-proxyproto v0.15.0
|
||||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
|
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/sagernet/sing v0.5.1
|
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.7
|
|
||||||
github.com/stretchr/testify v1.12.1
|
github.com/stretchr/testify v1.12.1
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f
|
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||||
golang.org/x/crypto v0.55.0
|
golang.org/x/crypto v0.57.0
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||||
golang.org/x/net v0.58.0
|
golang.org/x/net v0.59.0
|
||||||
golang.org/x/sync v0.22.0
|
golang.org/x/sync v0.23.0
|
||||||
golang.org/x/sys v0.47.0
|
golang.org/x/sys v0.48.0
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||||
golang.zx2c4.com/wireguard/windows v1.0.1
|
golang.zx2c4.com/wireguard/windows v1.1.1
|
||||||
google.golang.org/grpc v1.83.1
|
google.golang.org/grpc v1.84.0
|
||||||
google.golang.org/protobuf v1.36.12
|
google.golang.org/protobuf v1.36.12
|
||||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
|
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
|
||||||
h12.io/socks v1.0.3
|
h12.io/socks v1.0.3
|
||||||
lukechampine.com/blake3 v1.4.1
|
lukechampine.com/blake3 v1.4.1
|
||||||
|
mvdan.cc/gofumpt v0.12.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -48,7 +47,6 @@ require (
|
|||||||
github.com/juju/ratelimit v1.0.2 // indirect
|
github.com/juju/ratelimit v1.0.2 // indirect
|
||||||
github.com/klauspost/compress v1.17.4 // indirect
|
github.com/klauspost/compress v1.17.4 // indirect
|
||||||
github.com/koron/go-ssdp v0.0.4 // indirect
|
github.com/koron/go-ssdp v0.0.4 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/libp2p/go-netroute v0.2.1 // indirect
|
github.com/libp2p/go-netroute v0.2.1 // indirect
|
||||||
github.com/pion/dtls/v3 v3.1.5 // indirect
|
github.com/pion/dtls/v3 v3.1.5 // indirect
|
||||||
github.com/pion/logging v0.2.4 // indirect
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
@@ -57,8 +55,9 @@ require (
|
|||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||||
golang.org/x/text v0.41.0 // indirect
|
golang.org/x/text v0.42.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
golang.org/x/tools v0.49.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,17 +2,12 @@ github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sx
|
|||||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
|
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
|
||||||
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
|
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
|
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
|
||||||
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
|
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
|
||||||
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
|
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
|
||||||
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
|
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
@@ -73,12 +68,8 @@ github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er
|
|||||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||||
github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y=
|
|
||||||
github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
|
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
|
|
||||||
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||||
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||||
@@ -87,21 +78,9 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
|
|||||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
|
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0 h1:rb+fKQFhz+5I2PPuQsNYxI5mUU840XWYtRF0ZBjvkws=
|
||||||
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
|
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
|
||||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
|
||||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
|
||||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
|
||||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
|
||||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
|
||||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
|
||||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
|
||||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
@@ -110,8 +89,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
|
|||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
@@ -120,12 +99,12 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
|||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
|
||||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -133,20 +112,22 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||||
|
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||||
|
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -154,14 +135,14 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
|
|||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||||
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
golang.zx2c4.com/wireguard/windows v1.1.1 h1:8/H97U1v1PNDNcBsMZgU3KFuND9MQdTsU2NOwmCXArE=
|
||||||
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
golang.zx2c4.com/wireguard/windows v1.1.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
||||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
|
google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
|
||||||
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
|
google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
|
||||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -176,3 +157,5 @@ h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo=
|
|||||||
h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck=
|
h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck=
|
||||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||||
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||||
|
mvdan.cc/gofumpt v0.12.0 h1:1Lbudkz2kpM9Cjz2pL4M19u7q+GaEhCTNf7N9mfpcho=
|
||||||
|
mvdan.cc/gofumpt v0.12.0/go.mod h1:SmBHHrljiZu/uoypeKup3rFzP6eoC9UwCp2iH5E3jZA=
|
||||||
|
|||||||
+20
-30
@@ -1,52 +1,42 @@
|
|||||||
package conf
|
package conf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/serial"
|
|
||||||
"github.com/xtls/xray-core/proxy/blackhole"
|
"github.com/xtls/xray-core/proxy/blackhole"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NoneResponse struct{}
|
type ResponseConfig struct {
|
||||||
|
Type string `json:"type"`
|
||||||
func (*NoneResponse) Build() (proto.Message, error) {
|
CustomResponseData string `json:"customResponseData"`
|
||||||
return new(blackhole.NoneResponse), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPResponse struct{}
|
|
||||||
|
|
||||||
func (*HTTPResponse) Build() (proto.Message, error) {
|
|
||||||
return new(blackhole.HTTPResponse), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BlackholeConfig struct {
|
type BlackholeConfig struct {
|
||||||
Response json.RawMessage `json:"response"`
|
Response *ResponseConfig `json:"response"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *BlackholeConfig) Build() (proto.Message, error) {
|
func (v *BlackholeConfig) Build() (proto.Message, error) {
|
||||||
config := new(blackhole.Config)
|
config := new(blackhole.Config)
|
||||||
if v.Response != nil {
|
if v.Response != nil {
|
||||||
response, _, err := configLoader.Load(v.Response)
|
responseName := strings.ToLower(v.Response.Type)
|
||||||
if err != nil {
|
switch responseName {
|
||||||
return nil, errors.New("Config: Failed to parse Blackhole response config.").Base(err)
|
case "none", "":
|
||||||
|
config.Response = &blackhole.Response{Type: "none"}
|
||||||
|
case "http":
|
||||||
|
config.Response = &blackhole.Response{Type: "http"}
|
||||||
|
case "custom":
|
||||||
|
data, err := base64.StdEncoding.DecodeString(v.Response.CustomResponseData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("failed to decode custom response data: " + err.Error())
|
||||||
|
}
|
||||||
|
config.Response = &blackhole.Response{Type: "custom", CustomResponseData: data}
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unknown blackhole response: " + responseName)
|
||||||
}
|
}
|
||||||
responseSettings, err := response.(Buildable).Build()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.Response = serial.ToTypedMessage(responseSettings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var configLoader = NewJSONConfigLoader(
|
|
||||||
ConfigCreatorCache{
|
|
||||||
"none": func() interface{} { return new(NoneResponse) },
|
|
||||||
"http": func() interface{} { return new(HTTPResponse) },
|
|
||||||
},
|
|
||||||
"type",
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package conf_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/serial"
|
|
||||||
. "github.com/xtls/xray-core/infra/conf"
|
. "github.com/xtls/xray-core/infra/conf"
|
||||||
"github.com/xtls/xray-core/proxy/blackhole"
|
"github.com/xtls/xray-core/proxy/blackhole"
|
||||||
)
|
)
|
||||||
@@ -22,7 +21,7 @@ func TestHTTPResponseJSON(t *testing.T) {
|
|||||||
}`,
|
}`,
|
||||||
Parser: loadJSON(creator),
|
Parser: loadJSON(creator),
|
||||||
Output: &blackhole.Config{
|
Output: &blackhole.Config{
|
||||||
Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}),
|
Response: &blackhole.Response{Type: "http"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -32,3 +31,27 @@ func TestHTTPResponseJSON(t *testing.T) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCustomResponseJSON(t *testing.T) {
|
||||||
|
creator := func() Buildable {
|
||||||
|
return new(BlackholeConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
runMultiTestCase(t, []TestCase{
|
||||||
|
{
|
||||||
|
Input: `{
|
||||||
|
"response": {
|
||||||
|
"type": "custom",
|
||||||
|
"customResponseData": "Y3VzdG9tIHJlc3BvbnNl"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
Parser: loadJSON(creator),
|
||||||
|
Output: &blackhole.Config{
|
||||||
|
Response: &blackhole.Response{
|
||||||
|
Type: "custom",
|
||||||
|
CustomResponseData: []byte("custom response"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -97,7 +97,7 @@ func (v *HTTPClientConfig) Build() (proto.Message, error) {
|
|||||||
user.Email = v.Email
|
user.Email = v.Email
|
||||||
} else {
|
} else {
|
||||||
if err := json.Unmarshal(rawUser, user); err != nil {
|
if err := json.Unmarshal(rawUser, user); err != nil {
|
||||||
return nil, errors.New("failed to parse HTTP user").Base(err).AtError()
|
return nil, errors.New("failed to parse HTTP user").Base(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
account := new(HTTPAccount)
|
account := new(HTTPAccount)
|
||||||
@@ -106,7 +106,7 @@ func (v *HTTPClientConfig) Build() (proto.Message, error) {
|
|||||||
account.Password = v.Password
|
account.Password = v.Password
|
||||||
} else {
|
} else {
|
||||||
if err := json.Unmarshal(rawUser, account); err != nil {
|
if err := json.Unmarshal(rawUser, account); err != nil {
|
||||||
return nil, errors.New("failed to parse HTTP account").Base(err).AtError()
|
return nil, errors.New("failed to parse HTTP account").Base(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
user.Account = serial.ToTypedMessage(account.Build())
|
user.Account = serial.ToTypedMessage(account.Build())
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ func RegisterConfigureFilePostProcessingStage(name string, stage ConfigureFilePo
|
|||||||
func PostProcessConfigureFile(conf *Config) error {
|
func PostProcessConfigureFile(conf *Config) error {
|
||||||
for k, v := range configureFilePostProcessingStages {
|
for k, v := range configureFilePostProcessingStages {
|
||||||
if err := v.Process(conf); err != nil {
|
if err := v.Process(conf); err != nil {
|
||||||
return errors.New("Rejected by Postprocessing Stage ", k).AtError().Base(err)
|
return errors.New("Rejected by Postprocessing Stage ", k).Base(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type ConfigCreatorCache map[string]ConfigCreator
|
|||||||
|
|
||||||
func (v ConfigCreatorCache) RegisterCreator(id string, creator ConfigCreator) error {
|
func (v ConfigCreatorCache) RegisterCreator(id string, creator ConfigCreator) error {
|
||||||
if _, found := v[id]; found {
|
if _, found := v[id]; found {
|
||||||
return errors.New(id, " already registered.").AtError()
|
return errors.New(id, " already registered.")
|
||||||
}
|
}
|
||||||
|
|
||||||
v[id] = creator
|
v[id] = creator
|
||||||
@@ -61,7 +61,7 @@ func (v *JSONConfigLoader) Load(raw []byte) (interface{}, string, error) {
|
|||||||
}
|
}
|
||||||
rawID, found := obj[v.idKey]
|
rawID, found := obj[v.idKey]
|
||||||
if !found {
|
if !found {
|
||||||
return nil, "", errors.New(v.idKey, " not found in JSON context").AtError()
|
return nil, "", errors.New(v.idKey, " not found in JSON context")
|
||||||
}
|
}
|
||||||
var id string
|
var id string
|
||||||
if err := json.Unmarshal(rawID, &id); err != nil {
|
if err := json.Unmarshal(rawID, &id); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package conf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
|
"github.com/xtls/xray-core/common/serial"
|
||||||
|
"github.com/xtls/xray-core/proxy/masque"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MasqueClientConfig struct {
|
||||||
|
Address *Address `json:"address"`
|
||||||
|
Port uint16 `json:"port"`
|
||||||
|
RemoteDNS []string `json:"remoteDNS"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MasqueClientConfig) Build() (proto.Message, error) {
|
||||||
|
if c.Address == nil {
|
||||||
|
return nil, errors.New(`MASQUE: "address" is not set`)
|
||||||
|
}
|
||||||
|
if c.Port == 0 {
|
||||||
|
return nil, errors.New(`MASQUE: "port" is not set`)
|
||||||
|
}
|
||||||
|
for _, s := range c.RemoteDNS {
|
||||||
|
if _, err := netip.ParseAddr(s); err != nil {
|
||||||
|
return nil, errors.New(`MASQUE: invalid "remoteDNS" `, s).Base(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &masque.ClientConfig{
|
||||||
|
Server: &protocol.ServerEndpoint{
|
||||||
|
Address: c.Address.Build(),
|
||||||
|
Port: uint32(c.Port),
|
||||||
|
},
|
||||||
|
RemoteDns: c.RemoteDNS,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MasqueUserConfig struct {
|
||||||
|
Pass string `json:"pass"`
|
||||||
|
Level uint32 `json:"level"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MasqueServerConfig struct {
|
||||||
|
Users []*MasqueUserConfig `json:"users"`
|
||||||
|
Clients []*MasqueUserConfig `json:"clients"`
|
||||||
|
Address []string `json:"address"`
|
||||||
|
MTU uint32 `json:"mtu"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MasqueServerConfig) Build() (proto.Message, error) {
|
||||||
|
if c.Clients != nil {
|
||||||
|
c.Users = c.Clients
|
||||||
|
}
|
||||||
|
config := &masque.ServerConfig{
|
||||||
|
Address: c.Address,
|
||||||
|
Mtu: c.MTU,
|
||||||
|
}
|
||||||
|
emails := make(map[string]bool)
|
||||||
|
for _, user := range c.Users {
|
||||||
|
if user.Email == "" {
|
||||||
|
return nil, errors.New(`MASQUE: "email" is empty`)
|
||||||
|
}
|
||||||
|
if strings.Contains(user.Email, ":") {
|
||||||
|
return nil, errors.New(`MASQUE: invalid "email" `, user.Email)
|
||||||
|
}
|
||||||
|
if user.Pass == "" {
|
||||||
|
return nil, errors.New(`MASQUE: "pass" of `, user.Email, ` is empty`)
|
||||||
|
}
|
||||||
|
email := strings.ToLower(user.Email)
|
||||||
|
if emails[email] {
|
||||||
|
return nil, errors.New(`MASQUE: duplicate "email" `, user.Email)
|
||||||
|
}
|
||||||
|
emails[email] = true
|
||||||
|
config.Users = append(config.Users, &protocol.User{
|
||||||
|
Email: user.Email,
|
||||||
|
Level: user.Level,
|
||||||
|
Account: serial.ToTypedMessage(&masque.Account{Password: user.Pass}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(c.Address) == 0 {
|
||||||
|
return nil, errors.New(`MASQUE: "address" is not set`)
|
||||||
|
}
|
||||||
|
var v4, v6 bool
|
||||||
|
for _, s := range c.Address {
|
||||||
|
prefix, err := netip.ParsePrefix(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(`MASQUE: invalid "address" `, s).Base(err)
|
||||||
|
}
|
||||||
|
if prefix.Addr().Is4() && v4 || prefix.Addr().Is6() && v6 {
|
||||||
|
return nil, errors.New(`MASQUE: "address" takes at most one IPv4 and one IPv6 prefix`)
|
||||||
|
}
|
||||||
|
v4 = v4 || prefix.Addr().Is4()
|
||||||
|
v6 = v6 || prefix.Addr().Is6()
|
||||||
|
}
|
||||||
|
if c.MTU != 0 && (c.MTU < 1280 || c.MTU > 65535) {
|
||||||
|
return nil, errors.New(`MASQUE: "mtu" must be between 1280 and 65535`)
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package conf_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
|
"github.com/xtls/xray-core/common/serial"
|
||||||
|
. "github.com/xtls/xray-core/infra/conf"
|
||||||
|
masqueproxy "github.com/xtls/xray-core/proxy/masque"
|
||||||
|
"github.com/xtls/xray-core/transport/internet/masque"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMasqueConfig(t *testing.T) {
|
||||||
|
creator := func() Buildable {
|
||||||
|
return new(MasqueConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
runMultiTestCase(t, []TestCase{
|
||||||
|
{
|
||||||
|
Input: `{}`,
|
||||||
|
Parser: loadJSON(creator),
|
||||||
|
Output: &masque.Config{Path: "/.well-known/masque/ip/*/*/"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: `{
|
||||||
|
"host": "example.com:8443",
|
||||||
|
"path": "/.well-known/masque/ip/{target}/{ipproto}/",
|
||||||
|
"headers": {"Authorization": "Basic dTpw"}
|
||||||
|
}`,
|
||||||
|
Parser: loadJSON(creator),
|
||||||
|
Output: &masque.Config{
|
||||||
|
Host: "example.com:8443",
|
||||||
|
Path: "/.well-known/masque/ip/*/*/",
|
||||||
|
Headers: map[string]string{"Authorization": "Basic dTpw"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: `{"path": "/masque/ip{?target,ipproto}"}`,
|
||||||
|
Parser: loadJSON(creator),
|
||||||
|
Output: &masque.Config{Path: "/masque/ip?target=*&ipproto=*"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: `{"user": "u", "pass": "p:q", "headers": {"X-Token": "a"}}`,
|
||||||
|
Parser: loadJSON(creator),
|
||||||
|
Output: &masque.Config{
|
||||||
|
Path: "/.well-known/masque/ip/*/*/",
|
||||||
|
Headers: map[string]string{"Authorization": "Basic dTpwOnE=", "X-Token": "a"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, input := range []string{
|
||||||
|
`{"path": "/masque/{target}/{ipproto}/{dns}"}`,
|
||||||
|
`{"path": "masque"}`,
|
||||||
|
`{"host": "example.com/path"}`,
|
||||||
|
`{"headers": {"host": "example.com"}}`,
|
||||||
|
`{"headers": {"Capsule-Protocol": "?0"}}`,
|
||||||
|
`{"headers": {"X Token": "a"}}`,
|
||||||
|
`{"headers": {"X-Token": "a\r\nb"}}`,
|
||||||
|
`{"user": "u:v", "pass": "p"}`,
|
||||||
|
`{"user": "u", "pass": "p", "headers": {"authorization": "Basic dTpw"}}`,
|
||||||
|
} {
|
||||||
|
if _, err := loadJSON(creator)(input); err == nil {
|
||||||
|
t.Errorf("expected an error for %s", input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMasqueOutboundConfig(t *testing.T) {
|
||||||
|
build := func(s string) error {
|
||||||
|
c := new(OutboundDetourConfig)
|
||||||
|
if err := json.Unmarshal([]byte(s), c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := c.Build()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := build(`{
|
||||||
|
"protocol": "masque",
|
||||||
|
"settings": {"address": "example.com", "port": 443},
|
||||||
|
"streamSettings": {"network": "masque", "security": "tls"},
|
||||||
|
"mux": {"enabled": false, "concurrency": -1}
|
||||||
|
}`); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
for _, input := range []string{
|
||||||
|
`{"protocol": "masque", "settings": {"address": "example.com"}, "streamSettings": {"network": "masque", "security": "tls"}}`,
|
||||||
|
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": true}}`,
|
||||||
|
`{"protocol": "masque", "settings": {"address": "example.com", "port": 443}, "streamSettings": {"network": "masque", "security": "tls"}, "mux": {"enabled": true, "concurrency": -1}}`,
|
||||||
|
`{"protocol": "freedom", "streamSettings": {"network": "masque", "security": "tls"}}`,
|
||||||
|
} {
|
||||||
|
if err := build(input); err == nil {
|
||||||
|
t.Errorf("expected an error for %s", input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMasqueServerConfig(t *testing.T) {
|
||||||
|
creator := func() Buildable {
|
||||||
|
return new(MasqueServerConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
runMultiTestCase(t, []TestCase{
|
||||||
|
{
|
||||||
|
Input: `{
|
||||||
|
"users": [{"email": "u@example.com", "pass": "p", "level": 1}],
|
||||||
|
"address": ["10.13.0.1/24", "fd13::1/64"],
|
||||||
|
"mtu": 1400
|
||||||
|
}`,
|
||||||
|
Parser: loadJSON(creator),
|
||||||
|
Output: &masqueproxy.ServerConfig{
|
||||||
|
Users: []*protocol.User{{
|
||||||
|
Email: "u@example.com",
|
||||||
|
Level: 1,
|
||||||
|
Account: serial.ToTypedMessage(&masqueproxy.Account{Password: "p"}),
|
||||||
|
}},
|
||||||
|
Address: []string{"10.13.0.1/24", "fd13::1/64"},
|
||||||
|
Mtu: 1400,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: `{"clients": [{"email": "u", "pass": "p:q"}], "address": ["10.13.0.1/24"]}`,
|
||||||
|
Parser: loadJSON(creator),
|
||||||
|
Output: &masqueproxy.ServerConfig{
|
||||||
|
Users: []*protocol.User{{
|
||||||
|
Email: "u",
|
||||||
|
Account: serial.ToTypedMessage(&masqueproxy.Account{Password: "p:q"}),
|
||||||
|
}},
|
||||||
|
Address: []string{"10.13.0.1/24"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: `{"address": ["10.13.0.1/24"]}`,
|
||||||
|
Parser: loadJSON(creator),
|
||||||
|
Output: &masqueproxy.ServerConfig{
|
||||||
|
Address: []string{"10.13.0.1/24"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, input := range []string{
|
||||||
|
`{"users": [{"email": "u:v", "pass": "p"}], "address": ["10.13.0.1/24"]}`,
|
||||||
|
`{"users": [{"email": "", "pass": "p"}], "address": ["10.13.0.1/24"]}`,
|
||||||
|
`{"users": [{"pass": "p"}], "address": ["10.13.0.1/24"]}`,
|
||||||
|
`{"users": [{"email": "u", "pass": ""}], "address": ["10.13.0.1/24"]}`,
|
||||||
|
`{"users": [{"email": "u", "pass": "p"}, {"email": "U", "pass": "q"}], "address": ["10.13.0.1/24"]}`,
|
||||||
|
`{"users": [{"email": "u", "pass": "p"}]}`,
|
||||||
|
`{"users": [{"email": "u", "pass": "p"}], "address": ["10.13.0.1"]}`,
|
||||||
|
`{"users": [{"email": "u", "pass": "p"}], "address": ["10.13.0.1/24", "10.14.0.1/24"]}`,
|
||||||
|
`{"users": [{"email": "u", "pass": "p"}], "address": ["fd13::1/64", "fd14::1/64"]}`,
|
||||||
|
`{"users": [{"email": "u", "pass": "p"}], "address": ["10.13.0.1/24"], "mtu": 1000}`,
|
||||||
|
`{"users": [{"email": "u", "pass": "p"}], "address": ["10.13.0.1/24"], "mtu": 70000}`,
|
||||||
|
} {
|
||||||
|
if _, err := loadJSON(creator)(input); err == nil {
|
||||||
|
t.Errorf("expected an error for %s", input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMasqueInboundConfig(t *testing.T) {
|
||||||
|
build := func(s string) error {
|
||||||
|
c := new(InboundDetourConfig)
|
||||||
|
if err := json.Unmarshal([]byte(s), c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := c.Build()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := build(`{
|
||||||
|
"protocol": "masque",
|
||||||
|
"port": 443,
|
||||||
|
"settings": {"users": [{"email": "u@example.com", "pass": "p"}], "address": ["10.13.0.1/24"]},
|
||||||
|
"streamSettings": {"network": "masque", "security": "tls"}
|
||||||
|
}`); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if err := build(`{
|
||||||
|
"protocol": "vless",
|
||||||
|
"port": 443,
|
||||||
|
"settings": {"users": [{"id": "27848739-7e62-4138-9fd3-098a63964b6b"}], "decryption": "none"},
|
||||||
|
"streamSettings": {"network": "masque", "security": "tls"}
|
||||||
|
}`); err == nil {
|
||||||
|
t.Error("expected an error for the masque transport on a vless inbound")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ func MergeConfigFromFiles(files []*core.ConfigSource) (string, error) {
|
|||||||
if j, ok := creflect.MarshalToJson(c, true); ok {
|
if j, ok := creflect.MarshalToJson(c, true); ok {
|
||||||
return j, nil
|
return j, nil
|
||||||
}
|
}
|
||||||
return "", errors.New("marshal to json failed.").AtError()
|
return "", errors.New("marshal to json failed.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeConfigs(files []*core.ConfigSource) (*conf.Config, error) {
|
func mergeConfigs(files []*core.ConfigSource) (*conf.Config, error) {
|
||||||
|
|||||||
+37
-56
@@ -3,8 +3,6 @@ package conf
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
|
|
||||||
C "github.com/sagernet/sing/common"
|
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/protocol"
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
"github.com/xtls/xray-core/common/serial"
|
"github.com/xtls/xray-core/common/serial"
|
||||||
@@ -55,7 +53,7 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
|
|||||||
v.Users = v.Clients
|
v.Users = v.Clients
|
||||||
}
|
}
|
||||||
|
|
||||||
if C.Contains(shadowaead_2022.List, v.Cipher) {
|
if _, err := shadowsocks_2022.GetCipherMethod(v.Cipher); err == nil {
|
||||||
return buildShadowsocks2022(v)
|
return buildShadowsocks2022(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,12 +109,14 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildShadowsocks2022(v *ShadowsocksServerConfig) (proto.Message, error) {
|
func buildShadowsocks2022(v *ShadowsocksServerConfig) (proto.Message, error) {
|
||||||
|
v.Cipher = strings.ToLower(v.Cipher)
|
||||||
if len(v.Users) == 0 {
|
if len(v.Users) == 0 {
|
||||||
config := new(shadowsocks_2022.ServerConfig)
|
config := new(shadowsocks_2022.ServerConfig)
|
||||||
config.Method = v.Cipher
|
config.Method = v.Cipher
|
||||||
config.Key = v.Password
|
config.Key = v.Password
|
||||||
config.Network = v.NetworkList.Build()
|
config.Network = v.NetworkList.Build()
|
||||||
config.Email = v.Email
|
config.Email = v.Email
|
||||||
|
config.Level = int32(v.Level)
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +171,7 @@ func buildShadowsocks2022(v *ShadowsocksServerConfig) (proto.Message, error) {
|
|||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Address: user.Address.Build(),
|
Address: user.Address.Build(),
|
||||||
Port: uint32(user.Port),
|
Port: uint32(user.Port),
|
||||||
|
Level: int32(user.Level),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return config, nil
|
return config, nil
|
||||||
@@ -214,63 +215,43 @@ func (v *ShadowsocksClientConfig) Build() (proto.Message, error) {
|
|||||||
return nil, errors.New(`Shadowsocks settings: "servers" should have one and only one member. Multiple endpoints in "servers" should use multiple Shadowsocks outbounds and routing balancer instead`)
|
return nil, errors.New(`Shadowsocks settings: "servers" should have one and only one member. Multiple endpoints in "servers" should use multiple Shadowsocks outbounds and routing balancer instead`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(v.Servers) == 1 {
|
server := v.Servers[0]
|
||||||
server := v.Servers[0]
|
if server.Address == nil {
|
||||||
if C.Contains(shadowaead_2022.List, server.Cipher) {
|
return nil, errors.New("Shadowsocks server address is not set.")
|
||||||
if server.Address == nil {
|
}
|
||||||
return nil, errors.New("Shadowsocks server address is not set.")
|
if server.Port == 0 {
|
||||||
}
|
return nil, errors.New("Invalid Shadowsocks port.")
|
||||||
if server.Port == 0 {
|
}
|
||||||
return nil, errors.New("Invalid Shadowsocks port.")
|
if server.Password == "" {
|
||||||
}
|
return nil, errors.New("Shadowsocks password is not specified.")
|
||||||
if server.Password == "" {
|
|
||||||
return nil, errors.New("Shadowsocks password is not specified.")
|
|
||||||
}
|
|
||||||
|
|
||||||
config := new(shadowsocks_2022.ClientConfig)
|
|
||||||
config.Address = server.Address.Build()
|
|
||||||
config.Port = uint32(server.Port)
|
|
||||||
config.Method = server.Cipher
|
|
||||||
config.Key = server.Password
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := shadowsocks_2022.GetCipherMethod(server.Cipher); err == nil {
|
||||||
|
config := new(shadowsocks_2022.ClientConfig)
|
||||||
|
config.Address = server.Address.Build()
|
||||||
|
config.Port = uint32(server.Port)
|
||||||
|
config.Method = server.Cipher
|
||||||
|
config.Key = server.Password
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
config := new(shadowsocks.ClientConfig)
|
config := new(shadowsocks.ClientConfig)
|
||||||
for _, server := range v.Servers {
|
account := &shadowsocks.Account{
|
||||||
if C.Contains(shadowaead_2022.List, server.Cipher) {
|
Password: server.Password,
|
||||||
return nil, errors.New("Shadowsocks 2022 accept no multi servers")
|
|
||||||
}
|
|
||||||
if server.Address == nil {
|
|
||||||
return nil, errors.New("Shadowsocks server address is not set.")
|
|
||||||
}
|
|
||||||
if server.Port == 0 {
|
|
||||||
return nil, errors.New("Invalid Shadowsocks port.")
|
|
||||||
}
|
|
||||||
if server.Password == "" {
|
|
||||||
return nil, errors.New("Shadowsocks password is not specified.")
|
|
||||||
}
|
|
||||||
account := &shadowsocks.Account{
|
|
||||||
Password: server.Password,
|
|
||||||
}
|
|
||||||
account.CipherType = cipherFromString(server.Cipher)
|
|
||||||
if account.CipherType == shadowsocks.CipherType_UNKNOWN {
|
|
||||||
return nil, errors.New("unknown cipher method: ", server.Cipher)
|
|
||||||
}
|
|
||||||
|
|
||||||
ss := &protocol.ServerEndpoint{
|
|
||||||
Address: server.Address.Build(),
|
|
||||||
Port: uint32(server.Port),
|
|
||||||
User: &protocol.User{
|
|
||||||
Level: uint32(server.Level),
|
|
||||||
Email: server.Email,
|
|
||||||
Account: serial.ToTypedMessage(account),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
config.Server = ss
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
account.CipherType = cipherFromString(server.Cipher)
|
||||||
|
if account.CipherType == shadowsocks.CipherType_UNKNOWN {
|
||||||
|
return nil, errors.New("unknown cipher method: ", server.Cipher)
|
||||||
|
}
|
||||||
|
ss := &protocol.ServerEndpoint{
|
||||||
|
Address: server.Address.Build(),
|
||||||
|
Port: uint32(server.Port),
|
||||||
|
User: &protocol.User{
|
||||||
|
Level: uint32(server.Level),
|
||||||
|
Email: server.Email,
|
||||||
|
Account: serial.ToTypedMessage(account),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
config.Server = ss
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -44,7 +44,6 @@ func (v *SocksServerConfig) Build() (proto.Message, error) {
|
|||||||
case AuthMethodUserPass:
|
case AuthMethodUserPass:
|
||||||
config.AuthType = socks.AuthType_PASSWORD
|
config.AuthType = socks.AuthType_PASSWORD
|
||||||
default:
|
default:
|
||||||
// errors.New("unknown socks auth method: ", v.AuthMethod, ". Default to noauth.").AtWarning().WriteToLog()
|
|
||||||
config.AuthType = socks.AuthType_NO_AUTH
|
config.AuthType = socks.AuthType_NO_AUTH
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +114,7 @@ func (v *SocksClientConfig) Build() (proto.Message, error) {
|
|||||||
user.Email = v.Email
|
user.Email = v.Email
|
||||||
} else {
|
} else {
|
||||||
if err := json.Unmarshal(rawUser, user); err != nil {
|
if err := json.Unmarshal(rawUser, user); err != nil {
|
||||||
return nil, errors.New("failed to parse Socks user").Base(err).AtError()
|
return nil, errors.New("failed to parse Socks user").Base(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
account := new(SocksAccount)
|
account := new(SocksAccount)
|
||||||
@@ -124,7 +123,7 @@ func (v *SocksClientConfig) Build() (proto.Message, error) {
|
|||||||
account.Password = v.Password
|
account.Password = v.Password
|
||||||
} else {
|
} else {
|
||||||
if err := json.Unmarshal(rawUser, account); err != nil {
|
if err := json.Unmarshal(rawUser, account); err != nil {
|
||||||
return nil, errors.New("failed to parse socks account").Base(err).AtError()
|
return nil, errors.New("failed to parse socks account").Base(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
user.Account = serial.ToTypedMessage(account.Build())
|
user.Account = serial.ToTypedMessage(account.Build())
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package conf
|
package conf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
googleuuid "github.com/google/uuid"
|
googleuuid "github.com/google/uuid"
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
|
"github.com/xtls/xray-core/common/serial"
|
||||||
"github.com/xtls/xray-core/transport/internet/finalmask/fragment"
|
"github.com/xtls/xray-core/transport/internet/finalmask/fragment"
|
||||||
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
|
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
|
||||||
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
|
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
|
||||||
@@ -23,6 +25,7 @@ import (
|
|||||||
"github.com/xtls/xray-core/transport/internet/finalmask/realm"
|
"github.com/xtls/xray-core/transport/internet/finalmask/realm"
|
||||||
"github.com/xtls/xray-core/transport/internet/finalmask/salamander"
|
"github.com/xtls/xray-core/transport/internet/finalmask/salamander"
|
||||||
"github.com/xtls/xray-core/transport/internet/finalmask/sudoku"
|
"github.com/xtls/xray-core/transport/internet/finalmask/sudoku"
|
||||||
|
"github.com/xtls/xray-core/transport/internet/finalmask/udphop"
|
||||||
"github.com/xtls/xray-core/transport/internet/finalmask/xdns"
|
"github.com/xtls/xray-core/transport/internet/finalmask/xdns"
|
||||||
"github.com/xtls/xray-core/transport/internet/finalmask/xicmp"
|
"github.com/xtls/xray-core/transport/internet/finalmask/xicmp"
|
||||||
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
|
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
|
||||||
@@ -80,9 +83,10 @@ var (
|
|||||||
"noise": func() interface{} { return new(NoiseMask) },
|
"noise": func() interface{} { return new(NoiseMask) },
|
||||||
"salamander": func() interface{} { return new(Salamander) },
|
"salamander": func() interface{} { return new(Salamander) },
|
||||||
"sudoku": func() interface{} { return new(Sudoku) },
|
"sudoku": func() interface{} { return new(Sudoku) },
|
||||||
"xdns": func() interface{} { return new(Xdns) },
|
"xdns": func() interface{} { return new(XDNS) },
|
||||||
"xicmp": func() interface{} { return new(Xicmp) },
|
"xicmp": func() interface{} { return new(Xicmp) },
|
||||||
"realm": func() interface{} { return new(Realm) },
|
"realm": func() interface{} { return new(Realm) },
|
||||||
|
"udphop": func() interface{} { return new(UDPHop) },
|
||||||
}, "type", "settings")
|
}, "type", "settings")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -306,14 +310,27 @@ type NoiseMask struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *NoiseMask) Build() (proto.Message, error) {
|
func (c *NoiseMask) Build() (proto.Message, error) {
|
||||||
|
noiseSlice := make([]*noise.Item, 0, len(c.Noise))
|
||||||
for _, item := range c.Noise {
|
for _, item := range c.Noise {
|
||||||
if len(item.Packet) > 0 && item.Rand.To > 0 {
|
if len(item.Packet) > 0 && item.Rand.To > 0 {
|
||||||
return nil, errors.New("len(item.Packet) > 0 && item.Rand.To > 0")
|
return nil, errors.New("len(item.Packet) > 0 && item.Rand.To > 0")
|
||||||
}
|
}
|
||||||
}
|
if strings.ToLower(item.Type) == "exp" {
|
||||||
|
var exp string
|
||||||
noiseSlice := make([]*noise.Item, 0, len(c.Noise))
|
if err := json.Unmarshal(item.Packet, &exp); err != nil {
|
||||||
for _, item := range c.Noise {
|
return nil, errors.New(`"packet" of noise "type": "exp" must be a string`).Base(err)
|
||||||
|
}
|
||||||
|
segments, err := parseNoiseExp(exp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
noiseSlice = append(noiseSlice, &noise.Item{
|
||||||
|
Segments: segments,
|
||||||
|
DelayMin: int64(item.Delay.From),
|
||||||
|
DelayMax: int64(item.Delay.To),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
if item.RandRange == nil {
|
if item.RandRange == nil {
|
||||||
item.RandRange = &Int32Range{From: 0, To: 255}
|
item.RandRange = &Int32Range{From: 0, To: 255}
|
||||||
}
|
}
|
||||||
@@ -342,6 +359,88 @@ func (c *NoiseMask) Build() (proto.Message, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var noiseExpPattern = regexp.MustCompile(`<\s*([a-z]+)(?:\s+([^>]*?))?\s*>`)
|
||||||
|
|
||||||
|
func parseNoiseExp(exp string) ([]*noise.Segment, error) {
|
||||||
|
var segments []*noise.Segment
|
||||||
|
matches := noiseExpPattern.FindAllStringSubmatchIndex(exp, -1)
|
||||||
|
last := 0
|
||||||
|
for _, m := range matches {
|
||||||
|
if strings.TrimSpace(exp[last:m[0]]) != "" {
|
||||||
|
return nil, errors.New("invalid noise exp near ", exp[last:m[0]])
|
||||||
|
}
|
||||||
|
last = m[1]
|
||||||
|
key := exp[m[2]:m[3]]
|
||||||
|
arg := ""
|
||||||
|
if m[4] >= 0 {
|
||||||
|
arg = exp[m[4]:m[5]]
|
||||||
|
}
|
||||||
|
segment, err := buildNoiseSegment(key, arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
segments = append(segments, segment)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(exp[last:]) != "" {
|
||||||
|
return nil, errors.New("invalid noise exp near ", exp[last:])
|
||||||
|
}
|
||||||
|
if len(segments) == 0 {
|
||||||
|
return nil, errors.New("empty noise exp: ", exp)
|
||||||
|
}
|
||||||
|
return segments, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildNoiseSegment(key, arg string) (*noise.Segment, error) {
|
||||||
|
sizeSegment := func(kind noise.Segment_Kind) (*noise.Segment, error) {
|
||||||
|
if arg == "" {
|
||||||
|
return nil, errors.New("<", key, "> in noise exp needs a size")
|
||||||
|
}
|
||||||
|
lo, hi, err := ParseRangeString(arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if lo < 0 || hi < lo || hi > 65535 {
|
||||||
|
return nil, errors.New("invalid size in noise exp: ", arg)
|
||||||
|
}
|
||||||
|
return &noise.Segment{Kind: kind, MinSize: int64(lo), MaxSize: int64(hi)}, nil
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
case "b":
|
||||||
|
hexStr := strings.TrimPrefix(strings.TrimPrefix(strings.Join(strings.Fields(arg), ""), "0x"), "0X")
|
||||||
|
if len(hexStr) == 0 {
|
||||||
|
return nil, errors.New("empty bytes in noise exp")
|
||||||
|
}
|
||||||
|
raw, err := hex.DecodeString(hexStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("invalid hex in noise exp: ", arg).Base(err)
|
||||||
|
}
|
||||||
|
return &noise.Segment{Kind: noise.Segment_BYTES, Bytes: raw}, nil
|
||||||
|
case "r":
|
||||||
|
return sizeSegment(noise.Segment_RANDOM)
|
||||||
|
case "rc":
|
||||||
|
return sizeSegment(noise.Segment_RANDOM_ASCII)
|
||||||
|
case "rd":
|
||||||
|
return sizeSegment(noise.Segment_RANDOM_DIGIT)
|
||||||
|
case "t":
|
||||||
|
if arg != "" {
|
||||||
|
return nil, errors.New("<t> in noise exp takes no argument")
|
||||||
|
}
|
||||||
|
return &noise.Segment{Kind: noise.Segment_TIMESTAMP}, nil
|
||||||
|
case "c":
|
||||||
|
if arg != "" {
|
||||||
|
return nil, errors.New("<c> in noise exp takes no argument")
|
||||||
|
}
|
||||||
|
return &noise.Segment{Kind: noise.Segment_COUNTER}, nil
|
||||||
|
case "n":
|
||||||
|
if arg != "" {
|
||||||
|
return nil, errors.New("<n> in noise exp takes no argument")
|
||||||
|
}
|
||||||
|
return &noise.Segment{Kind: noise.Segment_NONCE}, nil
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unknown <", key, "> in noise exp")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type UDPItem struct {
|
type UDPItem struct {
|
||||||
Rand int32 `json:"rand"`
|
Rand int32 `json:"rand"`
|
||||||
RandRange *Int32Range `json:"randRange"`
|
RandRange *Int32Range `json:"randRange"`
|
||||||
@@ -692,32 +791,88 @@ func (c *Sudoku) Build() (proto.Message, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Xdns struct {
|
type XDNSDomain struct {
|
||||||
Domain json.RawMessage `json:"domain"`
|
Name string `json:"name"`
|
||||||
|
LenLimit int32 `json:"lenLimit"`
|
||||||
Domains []string `json:"domains"`
|
LabelLimit int32 `json:"labelLimit"`
|
||||||
Resolvers []string `json:"resolvers"`
|
Types []int32 `json:"types"`
|
||||||
|
Edns0 int32 `json:"edns0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Xdns) Build() (proto.Message, error) {
|
type XDNSResolverTCP struct {
|
||||||
if c.Domain != nil {
|
Addr string `json:"addr"`
|
||||||
return nil, errors.PrintRemovedFeatureError("domain", "domains(server) & resolvers(client)")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if len(c.Domains) == 0 && len(c.Resolvers) == 0 {
|
func (c *XDNSResolverTCP) Build() (proto.Message, error) {
|
||||||
return nil, errors.New("empty domains & empty resolvers")
|
return &xdns.TCPResolverProto{Addr: c.Addr}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range c.Resolvers {
|
type XDNSResolverUDP struct {
|
||||||
if !strings.Contains(r, "+udp://") {
|
Addr string `json:"addr"`
|
||||||
return nil, errors.New("invalid resolver ", r)
|
}
|
||||||
|
|
||||||
|
func (c *XDNSResolverUDP) Build() (proto.Message, error) {
|
||||||
|
return &xdns.UDPResolverProto{Addr: c.Addr}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var xdnsLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||||
|
"tcp": func() interface{} { return new(XDNSResolverTCP) },
|
||||||
|
"udp": func() interface{} { return new(XDNSResolverUDP) },
|
||||||
|
}, "type", "settings")
|
||||||
|
|
||||||
|
type XDNSResolver struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Settings json.RawMessage `json:"settings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type XDNS struct {
|
||||||
|
Domains []XDNSDomain `json:"domains"`
|
||||||
|
Resolvers []XDNSResolver `json:"resolvers"`
|
||||||
|
ExtraPoll int32 `json:"extraPoll"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *XDNS) Build() (proto.Message, error) {
|
||||||
|
var domains []*xdns.DomainProto
|
||||||
|
var resolvers []*serial.TypedMessage
|
||||||
|
for i := range c.Domains {
|
||||||
|
if c.Domains[i].LenLimit == 0 {
|
||||||
|
c.Domains[i].LenLimit = 255
|
||||||
}
|
}
|
||||||
|
if c.Domains[i].LabelLimit == 0 {
|
||||||
|
c.Domains[i].LabelLimit = 63
|
||||||
|
}
|
||||||
|
types := make([]uint16, 0, len(c.Domains[i].Types))
|
||||||
|
for j := range c.Domains[i].Types {
|
||||||
|
types = append(types, uint16(c.Domains[i].Types[j]))
|
||||||
|
}
|
||||||
|
domain, err := xdns.NewDomain(c.Domains[i].Name, int(c.Domains[i].LenLimit), int(c.Domains[i].LabelLimit), types, uint16(c.Domains[i].Edns0))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
errors.LogInfo(context.Background(), domain.Show())
|
||||||
|
domains = append(domains, &xdns.DomainProto{
|
||||||
|
Name: c.Domains[i].Name,
|
||||||
|
LenLimit: c.Domains[i].LenLimit,
|
||||||
|
LabelLimit: c.Domains[i].LabelLimit,
|
||||||
|
Types: c.Domains[i].Types,
|
||||||
|
Edns0: c.Domains[i].Edns0,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
for i := range c.Resolvers {
|
||||||
return &xdns.Config{
|
config, err := xdnsLoader.LoadWithID(c.Resolvers[i].Settings, c.Resolvers[i].Type)
|
||||||
Domains: c.Domains,
|
if err != nil {
|
||||||
Resolvers: c.Resolvers,
|
return nil, err
|
||||||
}, nil
|
}
|
||||||
|
pm, err := config.(interface{ Build() (proto.Message, error) }).Build()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
resolvers = append(resolvers, serial.ToTypedMessage(pm))
|
||||||
|
}
|
||||||
|
if c.ExtraPoll < 0 || c.ExtraPoll > 3 {
|
||||||
|
return nil, errors.New("c.ExtraPoll < 0 || c.ExtraPoll > 3")
|
||||||
|
}
|
||||||
|
return &xdns.Config{Domains: domains, Resolvers: resolvers, ExtraPoll: c.ExtraPoll}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type XMC struct {
|
type XMC struct {
|
||||||
@@ -905,6 +1060,59 @@ func (c *Realm) Build() (proto.Message, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UDPHop struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Interval Int32Range `json:"interval"`
|
||||||
|
RemoteIPs []string `json:"remoteIPs"`
|
||||||
|
RemotePorts PortList `json:"remotePorts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *UDPHop) Build() (proto.Message, error) {
|
||||||
|
var local, remote, remoteOnce bool
|
||||||
|
for _, mode := range strings.Split(c.Mode, ",") {
|
||||||
|
switch strings.ToLower(mode) {
|
||||||
|
case "intervallocal":
|
||||||
|
local = true
|
||||||
|
case "intervalremote":
|
||||||
|
remote = true
|
||||||
|
case "perconnremote":
|
||||||
|
remoteOnce = true
|
||||||
|
default:
|
||||||
|
return nil, errors.New("invalid mode ", mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var remoteIPs []string
|
||||||
|
for _, ip := range c.RemoteIPs {
|
||||||
|
prefix, err := netip.ParsePrefix(ip)
|
||||||
|
if err == nil {
|
||||||
|
remoteIPs = append(remoteIPs, prefix.String())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr, err := netip.ParseAddr(ip)
|
||||||
|
if err == nil {
|
||||||
|
remoteIPs = append(remoteIPs, netip.PrefixFrom(addr, addr.BitLen()).String())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return nil, errors.New("invalid ip ", ip)
|
||||||
|
}
|
||||||
|
interval := c.Interval
|
||||||
|
if interval.From == 0 && interval.To == 0 {
|
||||||
|
interval.From, interval.To = 30, 30
|
||||||
|
}
|
||||||
|
if interval.From < 5 {
|
||||||
|
return nil, errors.New("interval must be at least 5")
|
||||||
|
}
|
||||||
|
return &udphop.Config{
|
||||||
|
Local: local,
|
||||||
|
Remote: remote,
|
||||||
|
RemoteOnce: remoteOnce,
|
||||||
|
IntervalMin: int64(interval.From),
|
||||||
|
IntervalMax: int64(interval.To),
|
||||||
|
RemoteIPs: remoteIPs,
|
||||||
|
RemotePorts: c.RemotePorts.Build().Ports(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
type Mask struct {
|
type Mask struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Settings *json.RawMessage `json:"settings"`
|
Settings *json.RawMessage `json:"settings"`
|
||||||
@@ -938,7 +1146,6 @@ type QuicParamsConfig struct {
|
|||||||
BrutalUp Bandwidth `json:"brutalUp"`
|
BrutalUp Bandwidth `json:"brutalUp"`
|
||||||
BrutalDown Bandwidth `json:"brutalDown"`
|
BrutalDown Bandwidth `json:"brutalDown"`
|
||||||
BrutalDisableLossCompensation bool `json:"brutalDisableLossCompensation"`
|
BrutalDisableLossCompensation bool `json:"brutalDisableLossCompensation"`
|
||||||
UdpHop UdpHop `json:"udpHop"`
|
|
||||||
InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"`
|
InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"`
|
||||||
MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"`
|
MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"`
|
||||||
InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"`
|
InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"`
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package conf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/transport/internet/finalmask/noise"
|
||||||
|
)
|
||||||
|
|
||||||
|
func expPacket(exp string) json.RawMessage {
|
||||||
|
b, _ := json.Marshal(exp)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildNoiseExp(exp string) (*noise.Config, error) {
|
||||||
|
msg, err := (&NoiseMask{Noise: []NoiseItem{{Type: "exp", Packet: expPacket(exp)}}}).Build()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return msg.(*noise.Config), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoiseExp(t *testing.T) {
|
||||||
|
cfg, err := buildNoiseExp("<b 0d0a0d0a><t><r 24><rc 20-40><rd 8><c><n>")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
segments := cfg.Items[0].Segments
|
||||||
|
if len(segments) != 7 {
|
||||||
|
t.Fatalf("got %d segments, want 7", len(segments))
|
||||||
|
}
|
||||||
|
want := []struct {
|
||||||
|
kind noise.Segment_Kind
|
||||||
|
bytes []byte
|
||||||
|
min, max int64
|
||||||
|
}{
|
||||||
|
{noise.Segment_BYTES, []byte{0x0d, 0x0a, 0x0d, 0x0a}, 0, 0},
|
||||||
|
{noise.Segment_TIMESTAMP, nil, 0, 0},
|
||||||
|
{noise.Segment_RANDOM, nil, 24, 24},
|
||||||
|
{noise.Segment_RANDOM_ASCII, nil, 20, 40},
|
||||||
|
{noise.Segment_RANDOM_DIGIT, nil, 8, 8},
|
||||||
|
{noise.Segment_COUNTER, nil, 0, 0},
|
||||||
|
{noise.Segment_NONCE, nil, 0, 0},
|
||||||
|
}
|
||||||
|
for i, w := range want {
|
||||||
|
s := segments[i]
|
||||||
|
if s.Kind != w.kind || s.MinSize != w.min || s.MaxSize != w.max || string(s.Bytes) != string(w.bytes) {
|
||||||
|
t.Errorf("segment %d = %+v, want %+v", i, s, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoiseExpStripsHexPrefix(t *testing.T) {
|
||||||
|
cfg, err := buildNoiseExp("<b 0x16030100>")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := cfg.Items[0].Segments[0].Bytes; string(got) != string([]byte{0x16, 0x03, 0x01, 0x00}) {
|
||||||
|
t.Errorf("got %x", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoiseExpWhitespace(t *testing.T) {
|
||||||
|
if _, err := buildNoiseExp(" <b 00> <t> "); err != nil {
|
||||||
|
t.Errorf("surrounding whitespace should be allowed: %v", err)
|
||||||
|
}
|
||||||
|
cfg, err := buildNoiseExp("<b 0d 0a 0d 0a>")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := cfg.Items[0].Segments[0].Bytes; string(got) != "\r\n\r\n" {
|
||||||
|
t.Errorf("got %x", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoiseExpRejects(t *testing.T) {
|
||||||
|
for _, exp := range []string{
|
||||||
|
"<x 1>",
|
||||||
|
"<b>",
|
||||||
|
"<b zz>",
|
||||||
|
"<b 0d0>",
|
||||||
|
"<r>",
|
||||||
|
"<r -1>",
|
||||||
|
"<r 40-20>",
|
||||||
|
"<r 70000>",
|
||||||
|
"<t 5>",
|
||||||
|
"<n 5>",
|
||||||
|
"garbage<t>",
|
||||||
|
"<t> tail",
|
||||||
|
"<t><b>",
|
||||||
|
} {
|
||||||
|
if _, err := buildNoiseExp(exp); err == nil {
|
||||||
|
t.Errorf("expected an error for %q", exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoiseExpConflicts(t *testing.T) {
|
||||||
|
if _, err := (&NoiseMask{Noise: []NoiseItem{{Type: "exp", Packet: expPacket("<t>"), Rand: Int32Range{From: 10, To: 20}}}}).Build(); err == nil {
|
||||||
|
t.Error("exp with rand should be rejected")
|
||||||
|
}
|
||||||
|
for _, packet := range []string{``, `[1, 2]`, `5`} {
|
||||||
|
if _, err := (&NoiseMask{Noise: []NoiseItem{{Type: "exp", Packet: json.RawMessage(packet)}}}).Build(); err == nil {
|
||||||
|
t.Errorf("expected an error for packet %q", packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoiseExpFromJSON(t *testing.T) {
|
||||||
|
var mask NoiseMask
|
||||||
|
if err := json.Unmarshal([]byte(`{"noise": [
|
||||||
|
{"type": "exp", "packet": "<b 504f5354><rd 10-20>", "delay": "1-3"},
|
||||||
|
{"type": "EXP", "packet": "<t>"},
|
||||||
|
{"type": "str", "packet": "<t>"},
|
||||||
|
{"rand": "10-20"}
|
||||||
|
]}`), &mask); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
msg, err := mask.Build()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
items := msg.(*noise.Config).Items
|
||||||
|
if len(items[0].Segments) != 2 || items[0].DelayMin != 1 || items[0].DelayMax != 3 {
|
||||||
|
t.Errorf("item 0 = %+v", items[0])
|
||||||
|
}
|
||||||
|
if len(items[1].Segments) != 1 || items[1].Segments[0].Kind != noise.Segment_TIMESTAMP {
|
||||||
|
t.Errorf("item 1 = %+v", items[1])
|
||||||
|
}
|
||||||
|
if len(items[2].Segments) != 0 || string(items[2].Packet) != "<t>" {
|
||||||
|
t.Errorf("item 2 = %+v", items[2])
|
||||||
|
}
|
||||||
|
if len(items[3].Segments) != 0 || items[3].RandMin != 10 || items[3].RandMax != 20 {
|
||||||
|
t.Errorf("item 3 = %+v", items[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,10 @@ func (p TransportProtocol) Build() (string, error) {
|
|||||||
return "", errors.PrintRemovedFeatureError("QUIC transport (without web service, etc.)", "XHTTP stream-one H3")
|
return "", errors.PrintRemovedFeatureError("QUIC transport (without web service, etc.)", "XHTTP stream-one H3")
|
||||||
case "hysteria":
|
case "hysteria":
|
||||||
return "hysteria", nil
|
return "hysteria", nil
|
||||||
|
case "masque":
|
||||||
|
return "masque", nil
|
||||||
|
case "xdrive":
|
||||||
|
return "xdrive", nil
|
||||||
default:
|
default:
|
||||||
return "", errors.New("Config: unknown transport protocol: ", p)
|
return "", errors.New("Config: unknown transport protocol: ", p)
|
||||||
}
|
}
|
||||||
@@ -59,6 +63,8 @@ type StreamConfig struct {
|
|||||||
WSSettings *WebSocketConfig `json:"wsSettings"`
|
WSSettings *WebSocketConfig `json:"wsSettings"`
|
||||||
HTTPUPGRADESettings *HttpUpgradeConfig `json:"httpupgradeSettings"`
|
HTTPUPGRADESettings *HttpUpgradeConfig `json:"httpupgradeSettings"`
|
||||||
HysteriaSettings *HysteriaConfig `json:"hysteriaSettings"`
|
HysteriaSettings *HysteriaConfig `json:"hysteriaSettings"`
|
||||||
|
MASQUESettings *MasqueConfig `json:"masqueSettings"`
|
||||||
|
XDRIVESettings *XDriveConfig `json:"xdriveSettings"`
|
||||||
SocketSettings *SocketConfig `json:"sockopt"`
|
SocketSettings *SocketConfig `json:"sockopt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +198,26 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
|||||||
Settings: serial.ToTypedMessage(hs),
|
Settings: serial.ToTypedMessage(hs),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if c.MASQUESettings != nil {
|
||||||
|
ms, err := c.MASQUESettings.Build()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Failed to build MASQUE config.").Base(err)
|
||||||
|
}
|
||||||
|
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||||
|
ProtocolName: "masque",
|
||||||
|
Settings: serial.ToTypedMessage(ms),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if c.XDRIVESettings != nil {
|
||||||
|
xs, err := c.XDRIVESettings.Build()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Failed to build XDRIVE config.").Base(err)
|
||||||
|
}
|
||||||
|
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||||
|
ProtocolName: "xdrive",
|
||||||
|
Settings: serial.ToTypedMessage(xs),
|
||||||
|
})
|
||||||
|
}
|
||||||
if c.SocketSettings != nil {
|
if c.SocketSettings != nil {
|
||||||
ss, err := c.SocketSettings.Build()
|
ss, err := c.SocketSettings.Build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -253,10 +279,6 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
|||||||
return nil, errors.New("unknown congestion control: ", c.FinalMask.QuicParams.Congestion, ", valid values: reno, bbr, brutal, force-brutal")
|
return nil, errors.New("unknown congestion control: ", c.FinalMask.QuicParams.Congestion, ", valid values: reno, bbr, brutal, force-brutal")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c.FinalMask.QuicParams.UdpHop.Interval.From != 0 && c.FinalMask.QuicParams.UdpHop.Interval.From < 5) || (c.FinalMask.QuicParams.UdpHop.Interval.To != 0 && c.FinalMask.QuicParams.UdpHop.Interval.To < 5) {
|
|
||||||
return nil, errors.New("Interval must be at least 5")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.FinalMask.QuicParams.InitStreamReceiveWindow > 0 && c.FinalMask.QuicParams.InitStreamReceiveWindow < 16384 {
|
if c.FinalMask.QuicParams.InitStreamReceiveWindow > 0 && c.FinalMask.QuicParams.InitStreamReceiveWindow < 16384 {
|
||||||
return nil, errors.New("InitStreamReceiveWindow must be at least 16384")
|
return nil, errors.New("InitStreamReceiveWindow must be at least 16384")
|
||||||
}
|
}
|
||||||
@@ -290,43 +312,20 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
|||||||
BrutalUp: up,
|
BrutalUp: up,
|
||||||
BrutalDown: down,
|
BrutalDown: down,
|
||||||
BrutalDisableLossCompensation: c.FinalMask.QuicParams.BrutalDisableLossCompensation,
|
BrutalDisableLossCompensation: c.FinalMask.QuicParams.BrutalDisableLossCompensation,
|
||||||
UdpHop: &internet.UdpHop{
|
InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow,
|
||||||
Ports: c.FinalMask.QuicParams.UdpHop.PortList.Build().Ports(),
|
MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow,
|
||||||
IntervalMin: int64(c.FinalMask.QuicParams.UdpHop.Interval.From),
|
InitConnReceiveWindow: c.FinalMask.QuicParams.InitConnectionReceiveWindow,
|
||||||
IntervalMax: int64(c.FinalMask.QuicParams.UdpHop.Interval.To),
|
MaxConnReceiveWindow: c.FinalMask.QuicParams.MaxConnectionReceiveWindow,
|
||||||
},
|
MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout,
|
||||||
InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow,
|
KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod,
|
||||||
MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow,
|
DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery,
|
||||||
InitConnReceiveWindow: c.FinalMask.QuicParams.InitConnectionReceiveWindow,
|
DisableChromeParrot: c.FinalMask.QuicParams.DisableChromeParrot,
|
||||||
MaxConnReceiveWindow: c.FinalMask.QuicParams.MaxConnectionReceiveWindow,
|
DisableGSO: c.FinalMask.QuicParams.DisableGSO,
|
||||||
MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout,
|
MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams,
|
||||||
KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod,
|
DisableStatelessReset: c.FinalMask.QuicParams.DisableStatelessReset,
|
||||||
DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery,
|
|
||||||
DisableChromeParrot: c.FinalMask.QuicParams.DisableChromeParrot,
|
|
||||||
DisableGSO: c.FinalMask.QuicParams.DisableGSO,
|
|
||||||
MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams,
|
|
||||||
DisableStatelessReset: c.FinalMask.QuicParams.DisableStatelessReset,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxyConfig struct {
|
|
||||||
Tag string `json:"tag"`
|
|
||||||
|
|
||||||
// TransportLayerProxy: For compatibility.
|
|
||||||
TransportLayerProxy bool `json:"transportLayer"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build implements Buildable.
|
|
||||||
func (v *ProxyConfig) Build() (*internet.ProxyConfig, error) {
|
|
||||||
if v.Tag == "" {
|
|
||||||
return nil, errors.New("Proxy tag is not set.")
|
|
||||||
}
|
|
||||||
return &internet.ProxyConfig{
|
|
||||||
Tag: v.Tag,
|
|
||||||
TransportLayerProxy: v.TransportLayerProxy,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|||||||
+119
-30
@@ -1,8 +1,9 @@
|
|||||||
package conf
|
package conf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"maps"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -21,9 +22,12 @@ import (
|
|||||||
"github.com/xtls/xray-core/transport/internet/httpupgrade"
|
"github.com/xtls/xray-core/transport/internet/httpupgrade"
|
||||||
"github.com/xtls/xray-core/transport/internet/hysteria"
|
"github.com/xtls/xray-core/transport/internet/hysteria"
|
||||||
"github.com/xtls/xray-core/transport/internet/kcp"
|
"github.com/xtls/xray-core/transport/internet/kcp"
|
||||||
|
"github.com/xtls/xray-core/transport/internet/masque"
|
||||||
"github.com/xtls/xray-core/transport/internet/splithttp"
|
"github.com/xtls/xray-core/transport/internet/splithttp"
|
||||||
"github.com/xtls/xray-core/transport/internet/tcp"
|
"github.com/xtls/xray-core/transport/internet/tcp"
|
||||||
"github.com/xtls/xray-core/transport/internet/websocket"
|
"github.com/xtls/xray-core/transport/internet/websocket"
|
||||||
|
"github.com/xtls/xray-core/transport/internet/xdrive"
|
||||||
|
"golang.org/x/net/http/httpguts"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -122,7 +126,7 @@ func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) {
|
|||||||
for _, key := range headerNames {
|
for _, key := range headerNames {
|
||||||
value := v.Headers[key]
|
value := v.Headers[key]
|
||||||
if value == nil {
|
if value == nil {
|
||||||
return nil, errors.New("empty HTTP header value: " + key).AtError()
|
return nil, errors.New("empty HTTP header value: " + key)
|
||||||
}
|
}
|
||||||
config.Header = append(config.Header, &http.Header{
|
config.Header = append(config.Header, &http.Header{
|
||||||
Name: key,
|
Name: key,
|
||||||
@@ -190,7 +194,7 @@ func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) {
|
|||||||
for _, key := range headerNames {
|
for _, key := range headerNames {
|
||||||
value := v.Headers[key]
|
value := v.Headers[key]
|
||||||
if value == nil {
|
if value == nil {
|
||||||
return nil, errors.New("empty HTTP header value: " + key).AtError()
|
return nil, errors.New("empty HTTP header value: " + key)
|
||||||
}
|
}
|
||||||
config.Header = append(config.Header, &http.Header{
|
config.Header = append(config.Header, &http.Header{
|
||||||
Name: key,
|
Name: key,
|
||||||
@@ -240,11 +244,11 @@ func (c *TCPConfig) Build() (proto.Message, error) {
|
|||||||
if len(c.HeaderConfig) > 0 {
|
if len(c.HeaderConfig) > 0 {
|
||||||
headerConfig, _, err := tcpHeaderLoader.Load(c.HeaderConfig)
|
headerConfig, _, err := tcpHeaderLoader.Load(c.HeaderConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("invalid TCP header config").Base(err).AtError()
|
return nil, errors.New("invalid TCP header config").Base(err)
|
||||||
}
|
}
|
||||||
ts, err := headerConfig.(Buildable).Build()
|
ts, err := headerConfig.(Buildable).Build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("invalid TCP header config").Base(err).AtError()
|
return nil, errors.New("invalid TCP header config").Base(err)
|
||||||
}
|
}
|
||||||
config.HeaderSettings = serial.ToTypedMessage(ts)
|
config.HeaderSettings = serial.ToTypedMessage(ts)
|
||||||
}
|
}
|
||||||
@@ -534,10 +538,6 @@ type KCPConfig struct {
|
|||||||
|
|
||||||
// Build implements Buildable.
|
// Build implements Buildable.
|
||||||
func (c *KCPConfig) Build() (proto.Message, error) {
|
func (c *KCPConfig) Build() (proto.Message, error) {
|
||||||
if c.HeaderConfig != nil || c.Seed != nil {
|
|
||||||
return nil, errors.PrintRemovedFeatureError("mkcp header & seed", "finalmask/udp header-* & mkcp-original & mkcp-aes128gcm")
|
|
||||||
}
|
|
||||||
|
|
||||||
config := common.Must2(internet.CreateTransportConfig(kcp.ProtocolName)).(*kcp.Config)
|
config := common.Must2(internet.CreateTransportConfig(kcp.ProtocolName)).(*kcp.Config)
|
||||||
|
|
||||||
if c.Mtu != nil {
|
if c.Mtu != nil {
|
||||||
@@ -560,16 +560,16 @@ func (c *KCPConfig) Build() (proto.Message, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if config.Mtu < 21 {
|
if config.Mtu < 21 {
|
||||||
return nil, errors.New("Mtu must be at least 21").AtError()
|
return nil, errors.New("MTU must be at least 21")
|
||||||
}
|
}
|
||||||
if config.Tti < 10 || config.Tti > 1000 {
|
if config.Tti < 10 || config.Tti > 1000 {
|
||||||
return nil, errors.New("invalid mKCP TTI: ", c.Tti).AtError()
|
return nil, errors.New("TTI must be between 10 and 1000")
|
||||||
}
|
}
|
||||||
if config.CwndMultiplier < 1 {
|
if config.CwndMultiplier < 1 {
|
||||||
return nil, errors.New("CwndMultiplier must be at least 1").AtError()
|
return nil, errors.New("CwndMultiplier must be at least 1")
|
||||||
}
|
}
|
||||||
if config.GetSendingBufferSize() == 0 {
|
if config.GetSendingBufferSize() == 0 {
|
||||||
return nil, errors.New("MaxSendingWindow must be >= Mtu").AtError()
|
return nil, errors.New("MaxSendingWindow must be at least ", config.Mtu)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
@@ -739,11 +739,6 @@ func (b Bandwidth) Bps() (uint64, error) {
|
|||||||
return uint64(val*float64(mul)) / 8, nil
|
return uint64(val*float64(mul)) / 8, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type UdpHop struct {
|
|
||||||
PortList PortList `json:"ports"`
|
|
||||||
Interval Int32Range `json:"interval"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Masquerade struct {
|
type Masquerade struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
|
||||||
@@ -760,14 +755,8 @@ type Masquerade struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type HysteriaConfig struct {
|
type HysteriaConfig struct {
|
||||||
Version int32 `json:"version"`
|
Version int32 `json:"version"`
|
||||||
Auth string `json:"auth"`
|
Auth string `json:"auth"`
|
||||||
|
|
||||||
Congestion *string `json:"congestion"`
|
|
||||||
Up *Bandwidth `json:"up"`
|
|
||||||
Down *Bandwidth `json:"down"`
|
|
||||||
UdpHop *UdpHop `json:"udphop"`
|
|
||||||
|
|
||||||
UdpIdleTimeout int64 `json:"udpIdleTimeout"`
|
UdpIdleTimeout int64 `json:"udpIdleTimeout"`
|
||||||
Masquerade Masquerade `json:"masquerade"`
|
Masquerade Masquerade `json:"masquerade"`
|
||||||
}
|
}
|
||||||
@@ -777,10 +766,6 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
|
|||||||
return nil, errors.New("version != 2")
|
return nil, errors.New("version != 2")
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Congestion != nil || c.Up != nil || c.Down != nil || c.UdpHop != nil {
|
|
||||||
errors.LogWarning(context.Background(), "congestion & up & down & udphop move to finalmask/quicParams")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.UdpIdleTimeout != 0 && (c.UdpIdleTimeout < 2 || c.UdpIdleTimeout > 600) {
|
if c.UdpIdleTimeout != 0 && (c.UdpIdleTimeout < 2 || c.UdpIdleTimeout > 600) {
|
||||||
return nil, errors.New("UdpIdleTimeout must be between 2 and 600")
|
return nil, errors.New("UdpIdleTimeout must be between 2 and 600")
|
||||||
}
|
}
|
||||||
@@ -805,6 +790,63 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MasqueConfig struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
User string `json:"user"`
|
||||||
|
Pass string `json:"pass"`
|
||||||
|
Headers map[string]string `json:"headers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MasqueConfig) Build() (proto.Message, error) {
|
||||||
|
path := c.Path
|
||||||
|
if path == "" {
|
||||||
|
path = masque.DefaultPath
|
||||||
|
}
|
||||||
|
path = strings.NewReplacer(
|
||||||
|
"{target}", "*", "{ipproto}", "*",
|
||||||
|
"{?target,ipproto}", "?target=*&ipproto=*", "{?ipproto,target}", "?ipproto=*&target=*",
|
||||||
|
"{&target,ipproto}", "&target=*&ipproto=*", "{&ipproto,target}", "&ipproto=*&target=*",
|
||||||
|
).Replace(path)
|
||||||
|
if !strings.HasPrefix(path, "/") || strings.ContainsAny(path, "{}") {
|
||||||
|
return nil, errors.New(`invalid "path": `, path, `, only the variables {target} and {ipproto} are supported`)
|
||||||
|
}
|
||||||
|
if c.Host != "" {
|
||||||
|
if u, err := url.Parse("https://" + c.Host); err != nil || u.Host != c.Host {
|
||||||
|
return nil, errors.New(`invalid "host": `, c.Host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for k, v := range c.Headers {
|
||||||
|
if !httpguts.ValidHeaderFieldName(k) || !httpguts.ValidHeaderFieldValue(v) {
|
||||||
|
return nil, errors.New(`invalid header in "headers": `, strconv.Quote(k))
|
||||||
|
}
|
||||||
|
switch strings.ToLower(k) {
|
||||||
|
case "host", "capsule-protocol":
|
||||||
|
return nil, errors.New(`"headers" can't contain "`, k, `"`)
|
||||||
|
case "authorization":
|
||||||
|
if c.User != "" || c.Pass != "" {
|
||||||
|
return nil, errors.New(`"headers" can't contain "`, k, `" when "user" or "pass" is set`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headers := c.Headers
|
||||||
|
if c.User != "" || c.Pass != "" {
|
||||||
|
if strings.Contains(c.User, ":") {
|
||||||
|
return nil, errors.New(`invalid "user": `, c.User)
|
||||||
|
}
|
||||||
|
headers = maps.Clone(c.Headers)
|
||||||
|
if headers == nil {
|
||||||
|
headers = make(map[string]string)
|
||||||
|
}
|
||||||
|
headers["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(c.User+":"+c.Pass))
|
||||||
|
}
|
||||||
|
return &masque.Config{
|
||||||
|
Host: c.Host,
|
||||||
|
Path: path,
|
||||||
|
Headers: headers,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func readFileOrString(f string, s []string) ([]byte, error) {
|
func readFileOrString(f string, s []string) ([]byte, error) {
|
||||||
if len(f) > 0 {
|
if len(f) > 0 {
|
||||||
return filesystem.ReadCert(f)
|
return filesystem.ReadCert(f)
|
||||||
@@ -814,3 +856,50 @@ func readFileOrString(f string, s []string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
return nil, errors.New("both file and bytes are empty.")
|
return nil, errors.New("both file and bytes are empty.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type XDriveConfig struct {
|
||||||
|
RemoteFolder string `json:"remoteFolder"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
Secrets []string `json:"secrets"`
|
||||||
|
SegmentBytes uint32 `json:"segmentBytes"`
|
||||||
|
FlushIntervalMs uint32 `json:"flushIntervalMs"`
|
||||||
|
PollIntervalMs uint32 `json:"pollIntervalMs"`
|
||||||
|
MaxPollIntervalMs uint32 `json:"maxPollIntervalMs"`
|
||||||
|
SessionTTLSeconds uint32 `json:"sessionTtlSeconds"`
|
||||||
|
Concurrency uint32 `json:"concurrency"`
|
||||||
|
EagerWindowMs uint32 `json:"eagerWindowMs"`
|
||||||
|
HoleTimeoutMs uint32 `json:"holeTimeoutMs"`
|
||||||
|
Template json.RawMessage `json:"template"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build implements Buildable.
|
||||||
|
func (c *XDriveConfig) Build() (proto.Message, error) {
|
||||||
|
switch c.Service {
|
||||||
|
case "local":
|
||||||
|
case "Google Drive":
|
||||||
|
if len(c.Secrets) != 3 {
|
||||||
|
return nil, errors.New("Google Drive needs 3 secrets in order of ClientID, ClientSecret, RefreshToken")
|
||||||
|
}
|
||||||
|
case "template":
|
||||||
|
if len(c.Template) == 0 {
|
||||||
|
return nil, errors.New(`service "template" needs a "template" object`)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unsupported service")
|
||||||
|
}
|
||||||
|
config := &xdrive.Config{
|
||||||
|
RemoteFolder: c.RemoteFolder,
|
||||||
|
Service: c.Service,
|
||||||
|
Secrets: c.Secrets,
|
||||||
|
SegmentBytes: c.SegmentBytes,
|
||||||
|
FlushIntervalMs: c.FlushIntervalMs,
|
||||||
|
PollIntervalMs: c.PollIntervalMs,
|
||||||
|
MaxPollIntervalMs: c.MaxPollIntervalMs,
|
||||||
|
SessionTtlSeconds: c.SessionTTLSeconds,
|
||||||
|
Concurrency: c.Concurrency,
|
||||||
|
EagerWindowMs: c.EagerWindowMs,
|
||||||
|
HoleTimeoutMs: c.HoleTimeoutMs,
|
||||||
|
Template: string(c.Template),
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -113,10 +113,10 @@ func (c *REALITYConfig) Build() (proto.Message, error) {
|
|||||||
config.MinClientVer[i] = byte(u)
|
config.MinClientVer[i] = byte(u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
errors.LogWarning(context.Background(), `REALITY: Changing "minClientVer" will increase the likelihood of your server's IP being blocked by the GFW`)
|
// errors.LogWarning(context.Background(), `REALITY: Changing "minClientVer" will increase the likelihood of your server's IP being blocked by the GFW`)
|
||||||
} else {
|
} else {
|
||||||
config.MinClientVer = []byte{26, 3, 27} // change it at your own risk: https://github.com/XTLS/Xray-core/commit/af7eb68028732a8ee3c0e5d6ab2b8a657bb2e770
|
// config.MinClientVer = []byte{26, 3, 27} // change it at your own risk: https://github.com/XTLS/Xray-core/commit/af7eb68028732a8ee3c0e5d6ab2b8a657bb2e770
|
||||||
errors.LogWarning(context.Background(), `REALITY: The default minimal client version is Xray-core v26.3.27, other clients may be refused to connect`)
|
// errors.LogWarning(context.Background(), `REALITY: The default minimal client version is Xray-core v26.3.27, other clients may be refused to connect`)
|
||||||
}
|
}
|
||||||
if c.MaxClientVer != "" {
|
if c.MaxClientVer != "" {
|
||||||
config.MaxClientVer = make([]byte, 3)
|
config.MaxClientVer = make([]byte, 3)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CustomSockoptConfig struct {
|
type CustomSockoptConfig struct {
|
||||||
Syetem string `json:"system"`
|
System string `json:"system"`
|
||||||
Network string `json:"network"`
|
Network string `json:"network"`
|
||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
Opt string `json:"opt"`
|
Opt string `json:"opt"`
|
||||||
@@ -124,7 +124,7 @@ func (c *SocketConfig) Build() (*internet.SocketConfig, error) {
|
|||||||
|
|
||||||
for _, copt := range c.CustomSockopt {
|
for _, copt := range c.CustomSockopt {
|
||||||
customSockopt := &internet.CustomSockopt{
|
customSockopt := &internet.CustomSockopt{
|
||||||
System: copt.Syetem,
|
System: copt.System,
|
||||||
Network: copt.Network,
|
Network: copt.Network,
|
||||||
Level: copt.Level,
|
Level: copt.Level,
|
||||||
Opt: copt.Opt,
|
Opt: copt.Opt,
|
||||||
|
|||||||
@@ -291,3 +291,76 @@ func TestHeaderCustomUDPBuildRejectsExprWithoutArgs(t *testing.T) {
|
|||||||
t.Fatalf("expected transform arg rejection, got %v", err)
|
t.Fatalf("expected transform arg rejection, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestXDriveStreamConfig(t *testing.T) {
|
||||||
|
config := new(StreamConfig)
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"method": "xdrive",
|
||||||
|
"xdriveSettings": {
|
||||||
|
"remoteFolder": "/tmp/xdrive",
|
||||||
|
"service": "local"
|
||||||
|
}
|
||||||
|
}`), config); err != nil {
|
||||||
|
t.Fatalf("Unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
built, err := config.Build()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Build: %v", err)
|
||||||
|
}
|
||||||
|
if built.ProtocolName != "xdrive" {
|
||||||
|
t.Fatalf("ProtocolName is %q, want %q", built.ProtocolName, "xdrive")
|
||||||
|
}
|
||||||
|
if len(built.TransportSettings) != 1 || built.TransportSettings[0].ProtocolName != "xdrive" {
|
||||||
|
t.Fatalf("TransportSettings is %v, want a single xdrive entry", built.TransportSettings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXDriveRejectsUnknownService(t *testing.T) {
|
||||||
|
config := new(XDriveConfig)
|
||||||
|
if err := json.Unmarshal([]byte(`{"remoteFolder": "/tmp/xdrive", "service": "Dropbox"}`), config); err != nil {
|
||||||
|
t.Fatalf("Unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := config.Build(); err == nil {
|
||||||
|
t.Fatal("Build accepted an unsupported service")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXDriveTemplateStreamConfig(t *testing.T) {
|
||||||
|
config := new(StreamConfig)
|
||||||
|
if err := json.Unmarshal([]byte(`{
|
||||||
|
"method": "xdrive",
|
||||||
|
"xdriveSettings": {
|
||||||
|
"remoteFolder": "folder",
|
||||||
|
"service": "template",
|
||||||
|
"secrets": ["user", "pass"],
|
||||||
|
"template": {
|
||||||
|
"flatten": true,
|
||||||
|
"auth": {"type": "basic", "username": "{secret0}", "password": "{secret1}"},
|
||||||
|
"put": {"method": "PUT", "url": "https://dav.example/{folder}/{name}"},
|
||||||
|
"get": {"method": "GET", "url": "https://dav.example/{folder}/{name}"},
|
||||||
|
"delete": {"method": "DELETE", "url": "https://dav.example/{folder}/{name}"},
|
||||||
|
"list": {"method": "PROPFIND", "url": "https://dav.example/{folder}/", "namesRegex": "<d:href>/folder/([^<]+)</d:href>"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`), config); err != nil {
|
||||||
|
t.Fatalf("Unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
built, err := config.Build()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Build: %v", err)
|
||||||
|
}
|
||||||
|
if built.ProtocolName != "xdrive" {
|
||||||
|
t.Fatalf("ProtocolName is %q, want xdrive", built.ProtocolName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXDriveTemplateNeedsTemplate(t *testing.T) {
|
||||||
|
config := new(XDriveConfig)
|
||||||
|
if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil {
|
||||||
|
t.Fatalf("Unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := config.Build(); err == nil {
|
||||||
|
t.Fatal("Build accepted a template service without a template")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/proxy/tun"
|
"github.com/xtls/xray-core/proxy/tun"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
@@ -20,6 +24,8 @@ type TunConfig struct {
|
|||||||
UserLevel uint32 `json:"userLevel"`
|
UserLevel uint32 `json:"userLevel"`
|
||||||
AutoSystemRoutingTable []string `json:"autoSystemRoutingTable"`
|
AutoSystemRoutingTable []string `json:"autoSystemRoutingTable"`
|
||||||
AutoOutboundsInterface *string `json:"autoOutboundsInterface"`
|
AutoOutboundsInterface *string `json:"autoOutboundsInterface"`
|
||||||
|
AutoSystemDnsToGateway bool `json:"autoSystemDnsToGateway"`
|
||||||
|
AutoSystemWfpBlockLeak []string `json:"autoSystemWfpBlockLeak"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *TunConfig) Build() (proto.Message, error) {
|
func (v *TunConfig) Build() (proto.Message, error) {
|
||||||
@@ -31,6 +37,32 @@ func (v *TunConfig) Build() (proto.Message, error) {
|
|||||||
DNS: v.DNS,
|
DNS: v.DNS,
|
||||||
UserLevel: v.UserLevel,
|
UserLevel: v.UserLevel,
|
||||||
AutoSystemRoutingTable: v.AutoSystemRoutingTable,
|
AutoSystemRoutingTable: v.AutoSystemRoutingTable,
|
||||||
|
AutoSystemDnsToGateway: v.AutoSystemDnsToGateway,
|
||||||
|
}
|
||||||
|
for _, leak := range v.AutoSystemWfpBlockLeak {
|
||||||
|
switch leak := strings.ToLower(leak); leak {
|
||||||
|
case "dns", "misconfigtun":
|
||||||
|
config.AutoSystemWfpBlockLeak = append(config.AutoSystemWfpBlockLeak, leak)
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unknown autoSystemWfpBlockLeak value: ", leak)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Each option needs other settings on the system it takes effect on: the
|
||||||
|
// filters go along with the routes of autoSystemRoutingTable, "dns" lets
|
||||||
|
// DNS through the TUN only, and autoSystemDnsToGateway points the system
|
||||||
|
// DNS at the gateway.
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
if len(config.AutoSystemWfpBlockLeak) > 0 && len(v.AutoSystemRoutingTable) == 0 {
|
||||||
|
return nil, errors.New("autoSystemWfpBlockLeak needs autoSystemRoutingTable to be set")
|
||||||
|
}
|
||||||
|
if slices.Contains(config.AutoSystemWfpBlockLeak, "dns") && len(v.DNS) == 0 {
|
||||||
|
return nil, errors.New(`autoSystemWfpBlockLeak "dns" needs dns to be set`)
|
||||||
|
}
|
||||||
|
case "linux":
|
||||||
|
if v.AutoSystemDnsToGateway && len(v.Gateway) == 0 {
|
||||||
|
return nil, errors.New("autoSystemDnsToGateway needs gateway to be set")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if v.AutoOutboundsInterface != nil {
|
if v.AutoOutboundsInterface != nil {
|
||||||
config.AutoOutboundsInterface = *v.AutoOutboundsInterface
|
config.AutoOutboundsInterface = *v.AutoOutboundsInterface
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package conf_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/xtls/xray-core/infra/conf"
|
||||||
|
"github.com/xtls/xray-core/proxy/tun"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTunConfigAutoSystem(t *testing.T) {
|
||||||
|
creator := func() Buildable {
|
||||||
|
return new(TunConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
runMultiTestCase(t, []TestCase{
|
||||||
|
{
|
||||||
|
Input: `{"name": "xray0"}`,
|
||||||
|
Parser: loadJSON(creator),
|
||||||
|
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: `{"name": "xray0", "gateway": ["10.0.0.1/24"], "autoSystemDnsToGateway": true}`,
|
||||||
|
Parser: loadJSON(creator),
|
||||||
|
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, Gateway: []string{"10.0.0.1/24"}, AutoSystemDnsToGateway: true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns", "misconfigtun"]}`,
|
||||||
|
Parser: loadJSON(creator),
|
||||||
|
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"dns", "misconfigtun"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["DNS"]}`,
|
||||||
|
Parser: loadJSON(creator),
|
||||||
|
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"dns"}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTunConfigAutoSystemNeeds checks that an option is rejected without the
|
||||||
|
// setting it needs, only on the system it takes effect on.
|
||||||
|
func TestTunConfigAutoSystemNeeds(t *testing.T) {
|
||||||
|
for _, c := range []struct {
|
||||||
|
input string
|
||||||
|
goos string // where it is rejected
|
||||||
|
}{
|
||||||
|
{`{"name": "xray0", "autoSystemWfpBlockLeak": ["misconfigtun"]}`, "windows"},
|
||||||
|
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["misconfigtun"]}`, ""},
|
||||||
|
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns"]}`, "windows"},
|
||||||
|
{`{"name": "xray0", "autoSystemDnsToGateway": true}`, "linux"},
|
||||||
|
} {
|
||||||
|
config := new(TunConfig)
|
||||||
|
if err := json.Unmarshal([]byte(c.input), config); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := config.Build(); (err != nil) != (runtime.GOOS == c.goos) {
|
||||||
|
t.Errorf("%s on %s: error = %v", c.input, runtime.GOOS, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTunConfigAutoSystemWfpBlockLeakUnknown(t *testing.T) {
|
||||||
|
config := new(TunConfig)
|
||||||
|
if err := json.Unmarshal([]byte(`{"name": "xray0", "autoSystemWfpBlockLeak": ["dns", "ip"]}`), config); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := config.Build(); err == nil {
|
||||||
|
t.Error("an unknown autoSystemWfpBlockLeak value was accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -312,6 +312,9 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) {
|
|||||||
if err := json.Unmarshal(rawUser, account); err != nil {
|
if err := json.Unmarshal(rawUser, account); err != nil {
|
||||||
return nil, errors.New(`VLESS users: invalid user`).Base(err)
|
return nil, errors.New(`VLESS users: invalid user`).Base(err)
|
||||||
}
|
}
|
||||||
|
// validateOutboundTransportSecurity needs to see these
|
||||||
|
c.Encryption = account.Encryption
|
||||||
|
c.Address = rec.Address
|
||||||
if account.Reverse != nil { // may not be reached: error json unmarshal
|
if account.Reverse != nil { // may not be reached: error json unmarshal
|
||||||
return nil, errors.New(`VLESS users: please use simplified outbound's config style to use "reverse"`)
|
return nil, errors.New(`VLESS users: please use simplified outbound's config style to use "reverse"`)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-23
@@ -59,14 +59,13 @@ func (c *WireGuardPeerConfig) Build() (*wireguard.PeerConfig, error) {
|
|||||||
type WireGuardConfig struct {
|
type WireGuardConfig struct {
|
||||||
IsClient bool `json:""`
|
IsClient bool `json:""`
|
||||||
|
|
||||||
NoKernelTun bool `json:"noKernelTun"`
|
NoKernelTun bool `json:"noKernelTun"`
|
||||||
SecretKey string `json:"secretKey"`
|
SecretKey string `json:"secretKey"`
|
||||||
Address []string `json:"address"`
|
Address []string `json:"address"`
|
||||||
Peers []*WireGuardPeerConfig `json:"peers"`
|
Peers []*WireGuardPeerConfig `json:"peers"`
|
||||||
MTU int32 `json:"mtu"`
|
MTU int32 `json:"mtu"`
|
||||||
Reserved []byte `json:"reserved"`
|
Reserved []byte `json:"reserved"`
|
||||||
DomainStrategy string `json:"domainStrategy"`
|
DNS []string `json:"remoteDNS"`
|
||||||
DNS []string `json:"remoteDNS"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WireGuardConfig) Build() (proto.Message, error) {
|
func (c *WireGuardConfig) Build() (proto.Message, error) {
|
||||||
@@ -125,21 +124,6 @@ func (c *WireGuardConfig) Build() (proto.Message, error) {
|
|||||||
}
|
}
|
||||||
config.Reserved = c.Reserved
|
config.Reserved = c.Reserved
|
||||||
|
|
||||||
switch strings.ToLower(c.DomainStrategy) {
|
|
||||||
case "forceip", "":
|
|
||||||
config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP
|
|
||||||
case "forceipv4":
|
|
||||||
config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP4
|
|
||||||
case "forceipv6":
|
|
||||||
config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP6
|
|
||||||
case "forceipv4v6":
|
|
||||||
config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP46
|
|
||||||
case "forceipv6v4":
|
|
||||||
config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP64
|
|
||||||
default:
|
|
||||||
return nil, errors.New("unsupported domain strategy: ", c.DomainStrategy)
|
|
||||||
}
|
|
||||||
|
|
||||||
config.IsClient = c.IsClient
|
config.IsClient = c.IsClient
|
||||||
config.NoKernelTun = c.NoKernelTun
|
config.NoKernelTun = c.NoKernelTun
|
||||||
config.DNS = c.DNS
|
config.DNS = c.DNS
|
||||||
|
|||||||
+46
-35
@@ -15,6 +15,8 @@ import (
|
|||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/common/serial"
|
"github.com/xtls/xray-core/common/serial"
|
||||||
core "github.com/xtls/xray-core/core"
|
core "github.com/xtls/xray-core/core"
|
||||||
|
"github.com/xtls/xray-core/proxy/freedom"
|
||||||
|
"github.com/xtls/xray-core/proxy/masque"
|
||||||
"github.com/xtls/xray-core/transport/internet"
|
"github.com/xtls/xray-core/transport/internet"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ var (
|
|||||||
"trojan": func() interface{} { return new(TrojanServerConfig) },
|
"trojan": func() interface{} { return new(TrojanServerConfig) },
|
||||||
"wireguard": func() interface{} { return &WireGuardConfig{IsClient: false} },
|
"wireguard": func() interface{} { return &WireGuardConfig{IsClient: false} },
|
||||||
"hysteria": func() interface{} { return new(HysteriaServerConfig) },
|
"hysteria": func() interface{} { return new(HysteriaServerConfig) },
|
||||||
|
"masque": func() interface{} { return new(MasqueServerConfig) },
|
||||||
"tun": func() interface{} { return new(TunConfig) },
|
"tun": func() interface{} { return new(TunConfig) },
|
||||||
}, "protocol", "settings")
|
}, "protocol", "settings")
|
||||||
|
|
||||||
@@ -47,6 +50,7 @@ var (
|
|||||||
"vmess": func() interface{} { return new(VMessOutboundConfig) },
|
"vmess": func() interface{} { return new(VMessOutboundConfig) },
|
||||||
"trojan": func() interface{} { return new(TrojanClientConfig) },
|
"trojan": func() interface{} { return new(TrojanClientConfig) },
|
||||||
"hysteria": func() interface{} { return new(HysteriaClientConfig) },
|
"hysteria": func() interface{} { return new(HysteriaClientConfig) },
|
||||||
|
"masque": func() interface{} { return new(MasqueClientConfig) },
|
||||||
"dns": func() interface{} { return new(DNSOutboundConfig) },
|
"dns": func() interface{} { return new(DNSOutboundConfig) },
|
||||||
"wireguard": func() interface{} { return &WireGuardConfig{IsClient: true} },
|
"wireguard": func() interface{} { return &WireGuardConfig{IsClient: true} },
|
||||||
}, "protocol", "settings")
|
}, "protocol", "settings")
|
||||||
@@ -202,6 +206,9 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("failed to build inbound handler for protocol ", c.Protocol).Base(err)
|
return nil, errors.New("failed to build inbound handler for protocol ", c.Protocol).Base(err)
|
||||||
}
|
}
|
||||||
|
if _, ok := ts.(*masque.ServerConfig); !ok && receiverSettings.StreamSettings != nil && receiverSettings.StreamSettings.ProtocolName == "masque" {
|
||||||
|
return nil, errors.New("the masque transport can only be used by the masque inbound")
|
||||||
|
}
|
||||||
|
|
||||||
return &core.InboundHandlerConfig{
|
return &core.InboundHandlerConfig{
|
||||||
Tag: c.Tag,
|
Tag: c.Tag,
|
||||||
@@ -216,21 +223,11 @@ type OutboundDetourConfig struct {
|
|||||||
Tag string `json:"tag"`
|
Tag string `json:"tag"`
|
||||||
Settings *json.RawMessage `json:"settings"`
|
Settings *json.RawMessage `json:"settings"`
|
||||||
StreamSetting *StreamConfig `json:"streamSettings"`
|
StreamSetting *StreamConfig `json:"streamSettings"`
|
||||||
ProxySettings *ProxyConfig `json:"proxySettings"`
|
ProxySettings *json.RawMessage `json:"proxySettings"`
|
||||||
MuxSettings *MuxConfig `json:"mux"`
|
MuxSettings *MuxConfig `json:"mux"`
|
||||||
TargetStrategy string `json:"targetStrategy"`
|
TargetStrategy string `json:"targetStrategy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *OutboundDetourConfig) checkChainProxyConfig() error {
|
|
||||||
if c.StreamSetting == nil || c.ProxySettings == nil || c.StreamSetting.SocketSettings == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(c.ProxySettings.Tag) > 0 && len(c.StreamSetting.SocketSettings.DialerProxy) > 0 {
|
|
||||||
return errors.New("proxySettings.tag is conflicted with sockopt.dialerProxy").AtWarning()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func requiresTransportSecurity(address *Address) bool {
|
func requiresTransportSecurity(address *Address) bool {
|
||||||
if address == nil || address.Address == nil {
|
if address == nil || address.Address == nil {
|
||||||
return false
|
return false
|
||||||
@@ -251,7 +248,7 @@ func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *pr
|
|||||||
if vlessCfg.Encryption != "" && vlessCfg.Encryption != "none" {
|
if vlessCfg.Encryption != "" && vlessCfg.Encryption != "none" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if requiresTransportSecurity(vlessCfg.Vnext[0].Address) {
|
if requiresTransportSecurity(vlessCfg.Address) {
|
||||||
return errors.New("vless without TLS or other encryption is prohibited unless the server address is a private IP or domain")
|
return errors.New("vless without TLS or other encryption is prohibited unless the server address is a private IP or domain")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,6 +264,10 @@ func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *pr
|
|||||||
|
|
||||||
// Build implements Buildable.
|
// Build implements Buildable.
|
||||||
func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||||
|
if c.ProxySettings != nil {
|
||||||
|
return nil, errors.PrintRemovedFeatureError(`outbound "proxySettings"`, `"streamSettings.sockopt.dialerProxy"`)
|
||||||
|
}
|
||||||
|
|
||||||
senderSettings := &proxyman.SenderConfig{}
|
senderSettings := &proxyman.SenderConfig{}
|
||||||
switch strings.ToLower(c.TargetStrategy) {
|
switch strings.ToLower(c.TargetStrategy) {
|
||||||
case "asis", "":
|
case "asis", "":
|
||||||
@@ -294,9 +295,6 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
|||||||
default:
|
default:
|
||||||
return nil, errors.New("unsupported target domain strategy: ", c.TargetStrategy)
|
return nil, errors.New("unsupported target domain strategy: ", c.TargetStrategy)
|
||||||
}
|
}
|
||||||
if err := c.checkChainProxyConfig(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.SendThrough != nil {
|
if c.SendThrough != nil {
|
||||||
address := ParseSendThough(c.SendThrough)
|
address := ParseSendThough(c.SendThrough)
|
||||||
@@ -322,26 +320,6 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
|||||||
senderSettings.StreamSettings = ss
|
senderSettings.StreamSettings = ss
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.ProxySettings != nil {
|
|
||||||
ps, err := c.ProxySettings.Build()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("invalid outbound detour proxy settings").Base(err)
|
|
||||||
}
|
|
||||||
if ps.TransportLayerProxy {
|
|
||||||
if senderSettings.StreamSettings != nil {
|
|
||||||
if senderSettings.StreamSettings.SocketSettings != nil {
|
|
||||||
senderSettings.StreamSettings.SocketSettings.DialerProxy = ps.Tag
|
|
||||||
} else {
|
|
||||||
senderSettings.StreamSettings.SocketSettings = &internet.SocketConfig{DialerProxy: ps.Tag}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
senderSettings.StreamSettings = &internet.StreamConfig{SocketSettings: &internet.SocketConfig{DialerProxy: ps.Tag}}
|
|
||||||
}
|
|
||||||
ps = nil
|
|
||||||
}
|
|
||||||
senderSettings.ProxySettings = ps
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.MuxSettings != nil {
|
if c.MuxSettings != nil {
|
||||||
ms, err := c.MuxSettings.Build()
|
ms, err := c.MuxSettings.Build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -366,6 +344,39 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, ok := ts.(*masque.ClientConfig); ok {
|
||||||
|
if ms := senderSettings.MultiplexSettings; ms != nil && ms.Enabled {
|
||||||
|
return nil, errors.New(`masque outbound does not support "mux"`)
|
||||||
|
}
|
||||||
|
} else if senderSettings.StreamSettings != nil && senderSettings.StreamSettings.ProtocolName == "masque" {
|
||||||
|
return nil, errors.New("the masque transport can only be used by the masque outbound")
|
||||||
|
}
|
||||||
|
|
||||||
|
if fc, ok := ts.(*freedom.Config); ok {
|
||||||
|
if senderSettings.StreamSettings != nil &&
|
||||||
|
senderSettings.StreamSettings.SocketSettings != nil &&
|
||||||
|
senderSettings.StreamSettings.SocketSettings.AddressPortStrategy != internet.AddressPortStrategy_None {
|
||||||
|
return nil, errors.New(`freedom outbound does not support "sockopt.addressPortStrategy"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
var strategy internet.DomainStrategy
|
||||||
|
if strategy = senderSettings.TargetStrategy; strategy != internet.DomainStrategy_AS_IS {
|
||||||
|
errors.LogWarning(context.Background(), `The "outbound.targetStrategy" setting is not supported directly by freedom and has been automatically migrated to "sockopt.domainStrategy" with no behavior change.`)
|
||||||
|
senderSettings.TargetStrategy = internet.DomainStrategy_AS_IS
|
||||||
|
} else if strategy = fc.DomainStrategy; strategy != internet.DomainStrategy_AS_IS {
|
||||||
|
errors.LogWarning(context.Background(), `The "freedom.domainStrategy" setting is deprecated and will be removed. For compatibility, its value has been automatically migrated to "sockopt.domainStrategy". Please update your config before removal.`)
|
||||||
|
}
|
||||||
|
if strategy != internet.DomainStrategy_AS_IS {
|
||||||
|
if senderSettings.StreamSettings == nil {
|
||||||
|
senderSettings.StreamSettings = &internet.StreamConfig{}
|
||||||
|
}
|
||||||
|
if senderSettings.StreamSettings.SocketSettings == nil {
|
||||||
|
senderSettings.StreamSettings.SocketSettings = &internet.SocketConfig{}
|
||||||
|
}
|
||||||
|
senderSettings.StreamSettings.SocketSettings.DomainStrategy = strategy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return &core.OutboundHandlerConfig{
|
return &core.OutboundHandlerConfig{
|
||||||
SenderSettings: serial.ToTypedMessage(senderSettings),
|
SenderSettings: serial.ToTypedMessage(senderSettings),
|
||||||
Tag: c.Tag,
|
Tag: c.Tag,
|
||||||
|
|||||||
+273
-139
@@ -1,15 +1,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"bytes"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/build"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"mvdan.cc/gofumpt/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -23,101 +26,27 @@ var (
|
|||||||
isFormat bool
|
isFormat bool
|
||||||
)
|
)
|
||||||
|
|
||||||
// envFile returns the name of the Go environment configuration file.
|
func getModuleInfo(pwd string) (modPath, langVersion string, err error) {
|
||||||
// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166
|
data, err := os.ReadFile(filepath.Join(pwd, "go.mod"))
|
||||||
func envFile() (string, error) {
|
|
||||||
if file := os.Getenv("GOENV"); file != "" {
|
|
||||||
if file == "off" {
|
|
||||||
return "", errors.New("GOENV=off")
|
|
||||||
}
|
|
||||||
return file, nil
|
|
||||||
}
|
|
||||||
dir, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
if dir == "" {
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
return "", errors.New("missing user-config dir")
|
fields := strings.Fields(line)
|
||||||
}
|
if len(fields) >= 2 {
|
||||||
return filepath.Join(dir, "go", "env"), nil
|
switch fields[0] {
|
||||||
}
|
case "module":
|
||||||
|
modPath = fields[1]
|
||||||
// GetRuntimeEnv returns the value of runtime environment variable,
|
case "go":
|
||||||
// that is set by running following command: `go env -w key=value`.
|
langVersion = "go" + strings.TrimPrefix(fields[1], "go")
|
||||||
func GetRuntimeEnv(key string) (string, error) {
|
|
||||||
file, err := envFile()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if file == "" {
|
|
||||||
return "", errors.New("missing runtime env file")
|
|
||||||
}
|
|
||||||
var data []byte
|
|
||||||
var runtimeEnv string
|
|
||||||
data, readErr := os.ReadFile(file)
|
|
||||||
if readErr != nil {
|
|
||||||
return "", readErr
|
|
||||||
}
|
|
||||||
envStrings := strings.Split(string(data), "\n")
|
|
||||||
for _, envItem := range envStrings {
|
|
||||||
envItem = strings.TrimSuffix(envItem, "\r")
|
|
||||||
envKeyValue := strings.Split(envItem, "=")
|
|
||||||
if len(envKeyValue) == 2 && strings.TrimSpace(envKeyValue[0]) == key {
|
|
||||||
runtimeEnv = strings.TrimSpace(envKeyValue[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return runtimeEnv, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty.
|
|
||||||
func GetGOBIN() string {
|
|
||||||
// The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command`
|
|
||||||
GOBIN := os.Getenv("GOBIN")
|
|
||||||
if GOBIN == "" {
|
|
||||||
var err error
|
|
||||||
// The one set by user by running `go env -w GOBIN=/path`
|
|
||||||
GOBIN, err = GetRuntimeEnv("GOBIN")
|
|
||||||
if err != nil {
|
|
||||||
// The default one that Golang uses
|
|
||||||
return filepath.Join(build.Default.GOPATH, "bin")
|
|
||||||
}
|
|
||||||
if GOBIN == "" {
|
|
||||||
return filepath.Join(build.Default.GOPATH, "bin")
|
|
||||||
}
|
|
||||||
return GOBIN
|
|
||||||
}
|
|
||||||
return GOBIN
|
|
||||||
}
|
|
||||||
|
|
||||||
func Run(binary string, args []string) ([]byte, error) {
|
|
||||||
cmd := exec.Command(binary, args...)
|
|
||||||
cmd.Env = append(cmd.Env, os.Environ()...)
|
|
||||||
output, cmdErr := cmd.CombinedOutput()
|
|
||||||
if cmdErr != nil {
|
|
||||||
return nil, cmdErr
|
|
||||||
}
|
|
||||||
return output, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func RunMany(binary string, args, files []string) bool {
|
|
||||||
fmt.Println("Processing with", binary, args, "...")
|
|
||||||
|
|
||||||
formatRequired := false
|
|
||||||
maxTasks := make(chan struct{}, runtime.NumCPU())
|
|
||||||
for _, file := range files {
|
|
||||||
maxTasks <- struct{}{}
|
|
||||||
go func(file string) {
|
|
||||||
output, err := Run(binary, append(args, file))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
} else if len(output) > 0 {
|
|
||||||
fmt.Println(string(output))
|
|
||||||
formatRequired = true
|
|
||||||
}
|
}
|
||||||
<-maxTasks
|
}
|
||||||
}(file)
|
|
||||||
}
|
}
|
||||||
return formatRequired
|
return modPath, langVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatGoSource(src []byte, opts format.Options) ([]byte, error) {
|
||||||
|
return format.Source(src, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -150,26 +79,76 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pwd := *directory
|
pwd := *directory
|
||||||
GOBIN := GetGOBIN()
|
modPath, langVersion, modErr := getModuleInfo(pwd)
|
||||||
binPath := os.Getenv("PATH")
|
if modErr != nil {
|
||||||
pathSlice := []string{pwd, GOBIN, binPath}
|
fmt.Println("Error reading go.mod:", modErr)
|
||||||
binPath = strings.Join(pathSlice, string(os.PathListSeparator))
|
|
||||||
os.Setenv("PATH", binPath)
|
|
||||||
|
|
||||||
suffix := ""
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
suffix = ".exe"
|
|
||||||
}
|
|
||||||
gofmt := "gofumpt" + suffix
|
|
||||||
|
|
||||||
if gofmtPath, err := exec.LookPath(gofmt); err != nil {
|
|
||||||
fmt.Println("Can not find", gofmt, "in system path or current working directory.")
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
} else {
|
}
|
||||||
gofmt = gofmtPath
|
opts := format.Options{
|
||||||
|
LangVersion: langVersion,
|
||||||
|
ModulePath: modPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFormat {
|
||||||
|
fmt.Println("Formatting Go source files...")
|
||||||
|
} else if isCheck {
|
||||||
|
fmt.Println("Checking files thar are not properly formatted...")
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs := make(chan string, runtime.NumCPU())
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var formatRequired atomic.Bool
|
||||||
|
var hasErrors atomic.Bool
|
||||||
|
|
||||||
|
for i := 0; i < runtime.NumCPU(); i++ {
|
||||||
|
wg.Go(func() {
|
||||||
|
for path := range jobs {
|
||||||
|
src, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error reading %s: %v\n", path, err)
|
||||||
|
hasErrors.Store(true)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted, err := formatGoSource(src, opts)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error formatting %s: %v\n", path, err)
|
||||||
|
hasErrors.Store(true)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(src, formatted) {
|
||||||
|
var diffText []byte
|
||||||
|
if isDryrun {
|
||||||
|
newName := filepath.ToSlash(path)
|
||||||
|
oldName := newName + ".orig"
|
||||||
|
diffText = diff(oldName, src, newName, formatted)
|
||||||
|
}
|
||||||
|
if isFormat {
|
||||||
|
info, statErr := os.Stat(path)
|
||||||
|
if statErr != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error stating %s: %v\n", path, statErr)
|
||||||
|
hasErrors.Store(true)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if writeErr := os.WriteFile(path, formatted, info.Mode().Perm()); writeErr != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", path, writeErr)
|
||||||
|
hasErrors.Store(true)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formatRequired.Store(true)
|
||||||
|
if isDryrun && len(diffText) > 0 {
|
||||||
|
fmt.Printf("%s\n%s", path, diffText)
|
||||||
|
} else {
|
||||||
|
fmt.Println(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
rawFilesSlice := make([]string, 0, 1000)
|
|
||||||
walkErr := filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error {
|
walkErr := filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
@@ -186,51 +165,206 @@ func main() {
|
|||||||
!strings.HasSuffix(filename, ".pb.go") &&
|
!strings.HasSuffix(filename, ".pb.go") &&
|
||||||
!strings.Contains(dir, filepath.Join("testing", "mocks")) &&
|
!strings.Contains(dir, filepath.Join("testing", "mocks")) &&
|
||||||
!strings.Contains(path, filepath.Join("main", "distro", "all", "all.go")) {
|
!strings.Contains(path, filepath.Join("main", "distro", "all", "all.go")) {
|
||||||
rawFilesSlice = append(rawFilesSlice, path)
|
jobs <- path
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
close(jobs)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
if walkErr != nil {
|
if walkErr != nil {
|
||||||
fmt.Println(walkErr)
|
fmt.Println(walkErr)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isFormat {
|
if hasErrors.Load() {
|
||||||
gofmtArgs := []string{
|
os.Exit(1)
|
||||||
"-l", "-e", "-w",
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Formatting Go source files...")
|
if isFormat {
|
||||||
RunMany(gofmt, gofmtArgs, rawFilesSlice)
|
if formatRequired.Load() {
|
||||||
fmt.Println("Do NOT forget to commit file changes.")
|
fmt.Println("Do NOT forget to commit file changes.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isCheck {
|
if isCheck {
|
||||||
gofmtListArgs := []string{
|
if formatRequired.Load() {
|
||||||
"-l", "-e",
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Checking files thar are not properly formatted...")
|
|
||||||
formatRequired := RunMany(gofmt, gofmtListArgs, rawFilesSlice)
|
|
||||||
if formatRequired {
|
|
||||||
fmt.Println("Format problem(s) found.")
|
fmt.Println("Format problem(s) found.")
|
||||||
}
|
fmt.Println("Please run 'go run ./infra/vformat/main.go' to format the Go source files.")
|
||||||
|
|
||||||
if isDryrun {
|
|
||||||
if formatRequired {
|
|
||||||
gofmtShowArgs := []string{
|
|
||||||
"-d", "-e",
|
|
||||||
}
|
|
||||||
RunMany(gofmt, gofmtShowArgs, rawFilesSlice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if formatRequired {
|
|
||||||
fmt.Println("Please run 'go install -v mvdan.cc/gofumpt@latest', then run 'go run ./infra/vformat/main.go' to format the Go source files.")
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("All Go source file format check has been passed.")
|
fmt.Println("All Go source file format check has been passed.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// diff algorithm copied from mvdan.cc/gofumpt/internal/govendor/diff
|
||||||
|
type pair struct{ x, y int }
|
||||||
|
|
||||||
|
func diff(oldName string, old []byte, newName string, new []byte) []byte {
|
||||||
|
if bytes.Equal(old, new) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
x := diffLines(old)
|
||||||
|
y := diffLines(new)
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
fmt.Fprintf(&out, "diff %s %s\n", oldName, newName)
|
||||||
|
fmt.Fprintf(&out, "--- %s\n", oldName)
|
||||||
|
fmt.Fprintf(&out, "+++ %s\n", newName)
|
||||||
|
|
||||||
|
var (
|
||||||
|
done pair
|
||||||
|
chunk pair
|
||||||
|
count pair
|
||||||
|
ctext []string
|
||||||
|
)
|
||||||
|
for _, m := range diffTgs(x, y) {
|
||||||
|
if m.x < done.x {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
start := m
|
||||||
|
for start.x > done.x && start.y > done.y && x[start.x-1] == y[start.y-1] {
|
||||||
|
start.x--
|
||||||
|
start.y--
|
||||||
|
}
|
||||||
|
end := m
|
||||||
|
for end.x < len(x) && end.y < len(y) && x[end.x] == y[end.y] {
|
||||||
|
end.x++
|
||||||
|
end.y++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range x[done.x:start.x] {
|
||||||
|
ctext = append(ctext, "-"+s)
|
||||||
|
count.x++
|
||||||
|
}
|
||||||
|
for _, s := range y[done.y:start.y] {
|
||||||
|
ctext = append(ctext, "+"+s)
|
||||||
|
count.y++
|
||||||
|
}
|
||||||
|
|
||||||
|
const C = 3
|
||||||
|
if (end.x < len(x) || end.y < len(y)) &&
|
||||||
|
(end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) {
|
||||||
|
for _, s := range x[start.x:end.x] {
|
||||||
|
ctext = append(ctext, " "+s)
|
||||||
|
count.x++
|
||||||
|
count.y++
|
||||||
|
}
|
||||||
|
done = end
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ctext) > 0 {
|
||||||
|
n := end.x - start.x
|
||||||
|
if n > C {
|
||||||
|
n = C
|
||||||
|
}
|
||||||
|
for _, s := range x[start.x : start.x+n] {
|
||||||
|
ctext = append(ctext, " "+s)
|
||||||
|
count.x++
|
||||||
|
count.y++
|
||||||
|
}
|
||||||
|
done = pair{start.x + n, start.y + n}
|
||||||
|
|
||||||
|
if count.x > 0 {
|
||||||
|
chunk.x++
|
||||||
|
}
|
||||||
|
if count.y > 0 {
|
||||||
|
chunk.y++
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y)
|
||||||
|
for _, s := range ctext {
|
||||||
|
out.WriteString(s)
|
||||||
|
}
|
||||||
|
count.x = 0
|
||||||
|
count.y = 0
|
||||||
|
ctext = ctext[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
if end.x >= len(x) && end.y >= len(y) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
chunk = pair{end.x - C, end.y - C}
|
||||||
|
for _, s := range x[chunk.x:end.x] {
|
||||||
|
ctext = append(ctext, " "+s)
|
||||||
|
count.x++
|
||||||
|
count.y++
|
||||||
|
}
|
||||||
|
done = end
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func diffLines(x []byte) []string {
|
||||||
|
l := strings.SplitAfter(string(x), "\n")
|
||||||
|
if l[len(l)-1] == "" {
|
||||||
|
l = l[:len(l)-1]
|
||||||
|
} else {
|
||||||
|
l[len(l)-1] += "\n\\ No newline at end of file\n"
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func diffTgs(x, y []string) []pair {
|
||||||
|
m := make(map[string]int)
|
||||||
|
for _, s := range x {
|
||||||
|
if c := m[s]; c > -2 {
|
||||||
|
m[s] = c - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range y {
|
||||||
|
if c := m[s]; c > -8 {
|
||||||
|
m[s] = c - 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var xi, yi, inv []int
|
||||||
|
for i, s := range y {
|
||||||
|
if m[s] == -5 {
|
||||||
|
m[s] = len(yi)
|
||||||
|
yi = append(yi, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, s := range x {
|
||||||
|
if j, ok := m[s]; ok && j >= 0 {
|
||||||
|
xi = append(xi, i)
|
||||||
|
inv = append(inv, j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
J := inv
|
||||||
|
n := len(xi)
|
||||||
|
T := make([]int, n)
|
||||||
|
L := make([]int, n)
|
||||||
|
for i := range T {
|
||||||
|
T[i] = n + 1
|
||||||
|
}
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
k := sort.Search(n, func(k int) bool {
|
||||||
|
return T[k] >= J[i]
|
||||||
|
})
|
||||||
|
T[k] = J[i]
|
||||||
|
L[i] = k + 1
|
||||||
|
}
|
||||||
|
k := 0
|
||||||
|
for _, v := range L {
|
||||||
|
if k < v {
|
||||||
|
k = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seq := make([]pair, 2+k)
|
||||||
|
seq[1+k] = pair{len(x), len(y)}
|
||||||
|
lastj := n
|
||||||
|
for i := n - 1; i >= 0; i-- {
|
||||||
|
if L[i] == k && J[i] < lastj {
|
||||||
|
seq[k] = pair{xi[i], yi[J[i]]}
|
||||||
|
k--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seq[0] = pair{0, 0}
|
||||||
|
return seq
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
"github.com/xtls/xray-core/core"
|
"github.com/xtls/xray-core/core"
|
||||||
"github.com/xtls/xray-core/infra/conf"
|
"github.com/xtls/xray-core/infra/conf"
|
||||||
"github.com/xtls/xray-core/infra/conf/serial"
|
"github.com/xtls/xray-core/infra/conf/serial"
|
||||||
|
"github.com/xtls/xray-core/proxy/hysteria"
|
||||||
|
"github.com/xtls/xray-core/proxy/masque"
|
||||||
"github.com/xtls/xray-core/proxy/shadowsocks"
|
"github.com/xtls/xray-core/proxy/shadowsocks"
|
||||||
"github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
"github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||||||
"github.com/xtls/xray-core/proxy/trojan"
|
"github.com/xtls/xray-core/proxy/trojan"
|
||||||
@@ -88,6 +90,10 @@ func extractInboundUsers(inb *core.InboundHandlerConfig) []*protocol.User {
|
|||||||
return ty.Users
|
return ty.Users
|
||||||
case *shadowsocks_2022.MultiUserServerConfig:
|
case *shadowsocks_2022.MultiUserServerConfig:
|
||||||
return ty.Users
|
return ty.Users
|
||||||
|
case *masque.ServerConfig:
|
||||||
|
return ty.Users
|
||||||
|
case *hysteria.ServerConfig:
|
||||||
|
return ty.Users
|
||||||
default:
|
default:
|
||||||
fmt.Println("unsupported inbound type")
|
fmt.Println("unsupported inbound type")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import (
|
|||||||
_ "github.com/xtls/xray-core/proxy/freedom"
|
_ "github.com/xtls/xray-core/proxy/freedom"
|
||||||
_ "github.com/xtls/xray-core/proxy/http"
|
_ "github.com/xtls/xray-core/proxy/http"
|
||||||
_ "github.com/xtls/xray-core/proxy/loopback"
|
_ "github.com/xtls/xray-core/proxy/loopback"
|
||||||
|
_ "github.com/xtls/xray-core/proxy/masque"
|
||||||
_ "github.com/xtls/xray-core/proxy/shadowsocks"
|
_ "github.com/xtls/xray-core/proxy/shadowsocks"
|
||||||
_ "github.com/xtls/xray-core/proxy/socks"
|
_ "github.com/xtls/xray-core/proxy/socks"
|
||||||
_ "github.com/xtls/xray-core/proxy/trojan"
|
_ "github.com/xtls/xray-core/proxy/trojan"
|
||||||
@@ -54,12 +55,14 @@ import (
|
|||||||
_ "github.com/xtls/xray-core/transport/internet/grpc"
|
_ "github.com/xtls/xray-core/transport/internet/grpc"
|
||||||
_ "github.com/xtls/xray-core/transport/internet/httpupgrade"
|
_ "github.com/xtls/xray-core/transport/internet/httpupgrade"
|
||||||
_ "github.com/xtls/xray-core/transport/internet/kcp"
|
_ "github.com/xtls/xray-core/transport/internet/kcp"
|
||||||
|
_ "github.com/xtls/xray-core/transport/internet/masque"
|
||||||
_ "github.com/xtls/xray-core/transport/internet/reality"
|
_ "github.com/xtls/xray-core/transport/internet/reality"
|
||||||
_ "github.com/xtls/xray-core/transport/internet/splithttp"
|
_ "github.com/xtls/xray-core/transport/internet/splithttp"
|
||||||
_ "github.com/xtls/xray-core/transport/internet/tcp"
|
_ "github.com/xtls/xray-core/transport/internet/tcp"
|
||||||
_ "github.com/xtls/xray-core/transport/internet/tls"
|
_ "github.com/xtls/xray-core/transport/internet/tls"
|
||||||
_ "github.com/xtls/xray-core/transport/internet/udp"
|
_ "github.com/xtls/xray-core/transport/internet/udp"
|
||||||
_ "github.com/xtls/xray-core/transport/internet/websocket"
|
_ "github.com/xtls/xray-core/transport/internet/websocket"
|
||||||
|
_ "github.com/xtls/xray-core/transport/internet/xdrive"
|
||||||
|
|
||||||
// Transport headers
|
// Transport headers
|
||||||
_ "github.com/xtls/xray-core/transport/internet/headers/http"
|
_ "github.com/xtls/xray-core/transport/internet/headers/http"
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
package blackhole
|
package blackhole
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
"github.com/xtls/xray-core/common/buf"
|
"github.com/xtls/xray-core/common/buf"
|
||||||
"github.com/xtls/xray-core/common/dice"
|
"github.com/xtls/xray-core/common/dice"
|
||||||
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/common/session"
|
"github.com/xtls/xray-core/common/session"
|
||||||
"github.com/xtls/xray-core/common/signal"
|
"github.com/xtls/xray-core/common/signal"
|
||||||
@@ -17,14 +20,34 @@ import (
|
|||||||
|
|
||||||
// Handler is an outbound connection that silently swallow the entire payload.
|
// Handler is an outbound connection that silently swallow the entire payload.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
response ResponseConfig
|
response []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var http403response = http.Response{
|
||||||
|
StatusCode: 403,
|
||||||
|
ProtoMajor: 1,
|
||||||
|
ProtoMinor: 1,
|
||||||
|
Header: http.Header{
|
||||||
|
"Connection": {"close"},
|
||||||
|
"Cache-Control": {"max-age=3600, public"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new blackhole handler.
|
// New creates a new blackhole handler.
|
||||||
func New(ctx context.Context, config *Config) (*Handler, error) {
|
func New(ctx context.Context, config *Config) (*Handler, error) {
|
||||||
response, err := config.GetInternalResponse()
|
response := []byte{}
|
||||||
if err != nil {
|
if config.Response != nil {
|
||||||
return nil, err
|
switch config.Response.Type {
|
||||||
|
case "", "none":
|
||||||
|
case "http":
|
||||||
|
var data bytes.Buffer
|
||||||
|
common.Must(http403response.Write(&data))
|
||||||
|
response = data.Bytes()
|
||||||
|
case "custom":
|
||||||
|
response = config.Response.CustomResponseData
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unknown blackhole response: " + config.Response.Type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &Handler{
|
return &Handler{
|
||||||
response: response,
|
response: response,
|
||||||
@@ -37,8 +60,10 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
|||||||
ob := outbounds[len(outbounds)-1]
|
ob := outbounds[len(outbounds)-1]
|
||||||
ob.Name = "blackhole"
|
ob.Name = "blackhole"
|
||||||
|
|
||||||
nBytes := h.response.WriteTo(link.Writer)
|
if len(h.response) > 0 {
|
||||||
if nBytes > 0 {
|
mbc := buf.MultiBufferContainer{}
|
||||||
|
common.Must2(mbc.Write(h.response))
|
||||||
|
link.Writer.WriteMultiBuffer(mbc.MultiBuffer)
|
||||||
// Sleep a little here to make sure the response is sent to client.
|
// Sleep a little here to make sure the response is sent to client.
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package blackhole_test
|
package blackhole_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
"github.com/xtls/xray-core/common/buf"
|
"github.com/xtls/xray-core/common/buf"
|
||||||
"github.com/xtls/xray-core/common/serial"
|
|
||||||
"github.com/xtls/xray-core/common/session"
|
"github.com/xtls/xray-core/common/session"
|
||||||
"github.com/xtls/xray-core/proxy/blackhole"
|
"github.com/xtls/xray-core/proxy/blackhole"
|
||||||
"github.com/xtls/xray-core/transport"
|
"github.com/xtls/xray-core/transport"
|
||||||
@@ -16,27 +19,58 @@ import (
|
|||||||
func TestBlackholeHTTPResponse(t *testing.T) {
|
func TestBlackholeHTTPResponse(t *testing.T) {
|
||||||
ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{}})
|
ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{}})
|
||||||
handler, err := blackhole.New(ctx, &blackhole.Config{
|
handler, err := blackhole.New(ctx, &blackhole.Config{
|
||||||
Response: serial.ToTypedMessage(&blackhole.HTTPResponse{}),
|
Response: &blackhole.Response{Type: "http"},
|
||||||
})
|
})
|
||||||
common.Must(err)
|
common.Must(err)
|
||||||
|
|
||||||
reader, writer := pipe.New(pipe.WithoutSizeLimit())
|
reader, writer := pipe.New(pipe.WithoutSizeLimit())
|
||||||
|
|
||||||
var mb buf.MultiBuffer
|
dataCh := make(chan buf.MultiBuffer, 1)
|
||||||
var rerr error
|
|
||||||
go func() {
|
go func() {
|
||||||
b, e := reader.ReadMultiBuffer()
|
mb := common.Must2(reader.ReadMultiBuffer())
|
||||||
mb = b
|
dataCh <- mb
|
||||||
rerr = e
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
link := transport.Link{
|
link := transport.Link{
|
||||||
Reader: reader,
|
Reader: reader,
|
||||||
Writer: writer,
|
Writer: writer,
|
||||||
}
|
}
|
||||||
common.Must(handler.Process(ctx, &link, nil))
|
common.Must(handler.Process(ctx, &link, nil))
|
||||||
|
mb := <-dataCh
|
||||||
|
data := make([]byte, mb.Len())
|
||||||
|
mb.Copy(data)
|
||||||
|
resp := common.Must2(http.ReadResponse(bufio.NewReader(bytes.NewBuffer(data)), nil))
|
||||||
|
if resp.StatusCode != 403 {
|
||||||
|
t.Errorf("expected 403 response, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlackholeCustomResponse(t *testing.T) {
|
||||||
|
ctx := session.ContextWithOutbounds(context.Background(), []*session.Outbound{{}})
|
||||||
|
// slightly bigger than a buffer
|
||||||
|
expected := make([]byte, buf.Size+1000)
|
||||||
|
if _, err := rand.Read(expected); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
handler, err := blackhole.New(ctx, &blackhole.Config{
|
||||||
|
Response: &blackhole.Response{
|
||||||
|
Type: "custom",
|
||||||
|
CustomResponseData: expected,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
common.Must(err)
|
||||||
|
|
||||||
|
reader, writer := pipe.New(pipe.WithoutSizeLimit())
|
||||||
|
var actual buf.MultiBuffer
|
||||||
|
var rerr error
|
||||||
|
go func() {
|
||||||
|
actual, rerr = reader.ReadMultiBuffer()
|
||||||
|
}()
|
||||||
|
|
||||||
|
link := transport.Link{Reader: reader, Writer: writer}
|
||||||
|
common.Must(handler.Process(ctx, &link, nil))
|
||||||
common.Must(rerr)
|
common.Must(rerr)
|
||||||
if mb.IsEmpty() {
|
|
||||||
t.Error("expect http response, but nothing")
|
if actual.String() != string(expected) {
|
||||||
|
t.Errorf("custom response mismatch")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
package blackhole
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/xtls/xray-core/common"
|
|
||||||
"github.com/xtls/xray-core/common/buf"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
http403response = `HTTP/1.1 403 Forbidden
|
|
||||||
Connection: close
|
|
||||||
Cache-Control: max-age=3600, public
|
|
||||||
Content-Length: 0
|
|
||||||
|
|
||||||
|
|
||||||
`
|
|
||||||
)
|
|
||||||
|
|
||||||
// ResponseConfig is the configuration for blackhole responses.
|
|
||||||
type ResponseConfig interface {
|
|
||||||
// WriteTo writes a predefined response to the specified buffer.
|
|
||||||
WriteTo(buf.Writer) int32
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteTo implements ResponseConfig.WriteTo().
|
|
||||||
func (*NoneResponse) WriteTo(buf.Writer) int32 { return 0 }
|
|
||||||
|
|
||||||
// WriteTo implements ResponseConfig.WriteTo().
|
|
||||||
func (*HTTPResponse) WriteTo(writer buf.Writer) int32 {
|
|
||||||
b := buf.New()
|
|
||||||
common.Must2(b.WriteString(http403response))
|
|
||||||
n := b.Len()
|
|
||||||
writer.WriteMultiBuffer(buf.MultiBuffer{b})
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInternalResponse converts response settings from proto to internal data structure.
|
|
||||||
func (c *Config) GetInternalResponse() (ResponseConfig, error) {
|
|
||||||
if c.GetResponse() == nil {
|
|
||||||
return new(NoneResponse), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
config, err := c.GetResponse().GetInstance()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return config.(ResponseConfig), nil
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
package blackhole
|
package blackhole
|
||||||
|
|
||||||
import (
|
import (
|
||||||
serial "github.com/xtls/xray-core/common/serial"
|
|
||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
@@ -22,26 +21,28 @@ const (
|
|||||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
)
|
)
|
||||||
|
|
||||||
type NoneResponse struct {
|
type Response struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
unknownFields protoimpl.UnknownFields
|
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
|
||||||
sizeCache protoimpl.SizeCache
|
CustomResponseData []byte `protobuf:"bytes,2,opt,name=custom_response_data,json=customResponseData,proto3" json:"custom_response_data,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *NoneResponse) Reset() {
|
func (x *Response) Reset() {
|
||||||
*x = NoneResponse{}
|
*x = Response{}
|
||||||
mi := &file_proxy_blackhole_config_proto_msgTypes[0]
|
mi := &file_proxy_blackhole_config_proto_msgTypes[0]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *NoneResponse) String() string {
|
func (x *Response) String() string {
|
||||||
return protoimpl.X.MessageStringOf(x)
|
return protoimpl.X.MessageStringOf(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*NoneResponse) ProtoMessage() {}
|
func (*Response) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *NoneResponse) ProtoReflect() protoreflect.Message {
|
func (x *Response) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proxy_blackhole_config_proto_msgTypes[0]
|
mi := &file_proxy_blackhole_config_proto_msgTypes[0]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
@@ -53,57 +54,35 @@ func (x *NoneResponse) ProtoReflect() protoreflect.Message {
|
|||||||
return mi.MessageOf(x)
|
return mi.MessageOf(x)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: Use NoneResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use Response.ProtoReflect.Descriptor instead.
|
||||||
func (*NoneResponse) Descriptor() ([]byte, []int) {
|
func (*Response) Descriptor() ([]byte, []int) {
|
||||||
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{0}
|
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{0}
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPResponse struct {
|
func (x *Response) GetType() string {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *HTTPResponse) Reset() {
|
|
||||||
*x = HTTPResponse{}
|
|
||||||
mi := &file_proxy_blackhole_config_proto_msgTypes[1]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *HTTPResponse) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*HTTPResponse) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *HTTPResponse) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_proxy_blackhole_config_proto_msgTypes[1]
|
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
return x.Type
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
}
|
||||||
return mi.MessageOf(x)
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: Use HTTPResponse.ProtoReflect.Descriptor instead.
|
func (x *Response) GetCustomResponseData() []byte {
|
||||||
func (*HTTPResponse) Descriptor() ([]byte, []int) {
|
if x != nil {
|
||||||
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{1}
|
return x.CustomResponseData
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Response *serial.TypedMessage `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"`
|
Response *Response `protobuf:"bytes,1,opt,name=response,proto3" json:"response,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Config) Reset() {
|
func (x *Config) Reset() {
|
||||||
*x = Config{}
|
*x = Config{}
|
||||||
mi := &file_proxy_blackhole_config_proto_msgTypes[2]
|
mi := &file_proxy_blackhole_config_proto_msgTypes[1]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
@@ -115,7 +94,7 @@ func (x *Config) String() string {
|
|||||||
func (*Config) ProtoMessage() {}
|
func (*Config) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *Config) ProtoReflect() protoreflect.Message {
|
func (x *Config) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_proxy_blackhole_config_proto_msgTypes[2]
|
mi := &file_proxy_blackhole_config_proto_msgTypes[1]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
@@ -128,10 +107,10 @@ func (x *Config) ProtoReflect() protoreflect.Message {
|
|||||||
|
|
||||||
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
||||||
func (*Config) Descriptor() ([]byte, []int) {
|
func (*Config) Descriptor() ([]byte, []int) {
|
||||||
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{2}
|
return file_proxy_blackhole_config_proto_rawDescGZIP(), []int{1}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Config) GetResponse() *serial.TypedMessage {
|
func (x *Config) GetResponse() *Response {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.Response
|
return x.Response
|
||||||
}
|
}
|
||||||
@@ -142,11 +121,12 @@ var File_proxy_blackhole_config_proto protoreflect.FileDescriptor
|
|||||||
|
|
||||||
const file_proxy_blackhole_config_proto_rawDesc = "" +
|
const file_proxy_blackhole_config_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\x1cproxy/blackhole/config.proto\x12\x14xray.proxy.blackhole\x1a!common/serial/typed_message.proto\"\x0e\n" +
|
"\x1cproxy/blackhole/config.proto\x12\x14xray.proxy.blackhole\"P\n" +
|
||||||
"\fNoneResponse\"\x0e\n" +
|
"\bResponse\x12\x12\n" +
|
||||||
"\fHTTPResponse\"F\n" +
|
"\x04type\x18\x01 \x01(\tR\x04type\x120\n" +
|
||||||
"\x06Config\x12<\n" +
|
"\x14custom_response_data\x18\x02 \x01(\fR\x12customResponseData\"D\n" +
|
||||||
"\bresponse\x18\x01 \x01(\v2 .xray.common.serial.TypedMessageR\bresponseB^\n" +
|
"\x06Config\x12:\n" +
|
||||||
|
"\bresponse\x18\x01 \x01(\v2\x1e.xray.proxy.blackhole.ResponseR\bresponseB^\n" +
|
||||||
"\x18com.xray.proxy.blackholeP\x01Z)github.com/xtls/xray-core/proxy/blackhole\xaa\x02\x14Xray.Proxy.Blackholeb\x06proto3"
|
"\x18com.xray.proxy.blackholeP\x01Z)github.com/xtls/xray-core/proxy/blackhole\xaa\x02\x14Xray.Proxy.Blackholeb\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -161,15 +141,13 @@ func file_proxy_blackhole_config_proto_rawDescGZIP() []byte {
|
|||||||
return file_proxy_blackhole_config_proto_rawDescData
|
return file_proxy_blackhole_config_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_proxy_blackhole_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
var file_proxy_blackhole_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||||
var file_proxy_blackhole_config_proto_goTypes = []any{
|
var file_proxy_blackhole_config_proto_goTypes = []any{
|
||||||
(*NoneResponse)(nil), // 0: xray.proxy.blackhole.NoneResponse
|
(*Response)(nil), // 0: xray.proxy.blackhole.Response
|
||||||
(*HTTPResponse)(nil), // 1: xray.proxy.blackhole.HTTPResponse
|
(*Config)(nil), // 1: xray.proxy.blackhole.Config
|
||||||
(*Config)(nil), // 2: xray.proxy.blackhole.Config
|
|
||||||
(*serial.TypedMessage)(nil), // 3: xray.common.serial.TypedMessage
|
|
||||||
}
|
}
|
||||||
var file_proxy_blackhole_config_proto_depIdxs = []int32{
|
var file_proxy_blackhole_config_proto_depIdxs = []int32{
|
||||||
3, // 0: xray.proxy.blackhole.Config.response:type_name -> xray.common.serial.TypedMessage
|
0, // 0: xray.proxy.blackhole.Config.response:type_name -> xray.proxy.blackhole.Response
|
||||||
1, // [1:1] is the sub-list for method output_type
|
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 method input_type
|
||||||
1, // [1:1] is the sub-list for extension type_name
|
1, // [1:1] is the sub-list for extension type_name
|
||||||
@@ -188,7 +166,7 @@ func file_proxy_blackhole_config_proto_init() {
|
|||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_blackhole_config_proto_rawDesc), len(file_proxy_blackhole_config_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_blackhole_config_proto_rawDesc), len(file_proxy_blackhole_config_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 3,
|
NumMessages: 2,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 0,
|
NumServices: 0,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ option go_package = "github.com/xtls/xray-core/proxy/blackhole";
|
|||||||
option java_package = "com.xray.proxy.blackhole";
|
option java_package = "com.xray.proxy.blackhole";
|
||||||
option java_multiple_files = true;
|
option java_multiple_files = true;
|
||||||
|
|
||||||
import "common/serial/typed_message.proto";
|
message Response {
|
||||||
|
string type = 1;
|
||||||
message NoneResponse {}
|
bytes custom_response_data = 2;
|
||||||
|
}
|
||||||
message HTTPResponse {}
|
|
||||||
|
|
||||||
message Config {
|
message Config {
|
||||||
xray.common.serial.TypedMessage response = 1;
|
Response response = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,19 @@
|
|||||||
package blackhole_test
|
package blackhole_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"context"
|
||||||
"net/http"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
"github.com/xtls/xray-core/common/buf"
|
"github.com/xtls/xray-core/proxy/blackhole"
|
||||||
. "github.com/xtls/xray-core/proxy/blackhole"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHTTPResponse(t *testing.T) {
|
func TestHTTPResponse(t *testing.T) {
|
||||||
buffer := buf.New()
|
handler, err := blackhole.New(context.Background(), &blackhole.Config{
|
||||||
|
Response: &blackhole.Response{Type: "http"},
|
||||||
httpResponse := new(HTTPResponse)
|
})
|
||||||
httpResponse.WriteTo(buf.NewWriter(buffer))
|
|
||||||
|
|
||||||
reader := bufio.NewReader(buffer)
|
|
||||||
response, err := http.ReadResponse(reader, nil)
|
|
||||||
common.Must(err)
|
common.Must(err)
|
||||||
|
if handler == nil {
|
||||||
if response.StatusCode != 403 {
|
t.Error("expected HTTP response handler")
|
||||||
t.Error("expected status code 403, but got ", response.StatusCode)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-106
@@ -53,6 +53,10 @@ func reloadEnvSettings() error {
|
|||||||
func init() {
|
func init() {
|
||||||
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
common.Must(common.RegisterConfig((*Config)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||||
h := new(Handler)
|
h := new(Handler)
|
||||||
|
if streamSettings, ok := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig); ok && streamSettings.SocketSettings != nil {
|
||||||
|
h.resolveStrategy = streamSettings.SocketSettings.DomainStrategy
|
||||||
|
h.usesDialerProxy = len(streamSettings.SocketSettings.DialerProxy) > 0
|
||||||
|
}
|
||||||
if err := core.RequireFeatures(ctx, func(pm policy.Manager) error {
|
if err := core.RequireFeatures(ctx, func(pm policy.Manager) error {
|
||||||
return h.Init(config.(*Config), pm)
|
return h.Init(config.(*Config), pm)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -89,9 +93,11 @@ type FinalRule struct {
|
|||||||
|
|
||||||
// Handler handles Freedom connections.
|
// Handler handles Freedom connections.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
policyManager policy.Manager
|
policyManager policy.Manager
|
||||||
config *Config
|
config *Config
|
||||||
finalRules []*FinalRule
|
finalRules []*FinalRule
|
||||||
|
resolveStrategy internet.DomainStrategy
|
||||||
|
usesDialerProxy bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildFinalRule(config *FinalRuleConfig) (*FinalRule, error) {
|
func buildFinalRule(config *FinalRuleConfig) (*FinalRule, error) {
|
||||||
@@ -168,22 +174,6 @@ func getDefaultFinalRule(inbound *session.Inbound) *FinalRule {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) shouldResolveDomainBeforeFinalRules(dialDest net.Destination, defaultRule *FinalRule) bool {
|
|
||||||
if !dialDest.Address.Family().IsDomain() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if len(h.finalRules) > 0 {
|
|
||||||
rule := h.finalRules[0]
|
|
||||||
if rule.action == RuleAction_Allow && rule.network[dialDest.Network] && len(rule.port) == 0 && rule.ip == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if defaultRule != nil || len(h.finalRules) > 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) matchFinalRule(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) *FinalRule {
|
func (h *Handler) matchFinalRule(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) *FinalRule {
|
||||||
for _, rule := range h.finalRules {
|
for _, rule := range h.finalRules {
|
||||||
if rule.Apply(network, address, port) {
|
if rule.Apply(network, address, port) {
|
||||||
@@ -196,17 +186,16 @@ func (h *Handler) matchFinalRule(network net.Network, address net.Address, port
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) applyFinalRules(network net.Network, address net.Address, port net.Port, defaultRule *FinalRule) RuleAction {
|
|
||||||
if rule := h.matchFinalRule(network, address, port, defaultRule); rule != nil {
|
|
||||||
return rule.action
|
|
||||||
}
|
|
||||||
return RuleAction_Allow
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init initializes the Handler with necessary parameters.
|
// Init initializes the Handler with necessary parameters.
|
||||||
func (h *Handler) Init(config *Config, pm policy.Manager) error {
|
func (h *Handler) Init(config *Config, pm policy.Manager) error {
|
||||||
h.config = config
|
h.config = config
|
||||||
h.policyManager = pm
|
h.policyManager = pm
|
||||||
|
if h.usesDialerProxy { // freedom is not the final outbound, final rules do not apply
|
||||||
|
if len(config.FinalRules) > 0 {
|
||||||
|
errors.LogWarning(context.Background(), `The "finalRules" setting is ignored when "sockopt.dialerProxy" is set, since freedom is not the final outbound.`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
h.finalRules = make([]*FinalRule, 0, len(config.FinalRules))
|
h.finalRules = make([]*FinalRule, 0, len(config.FinalRules))
|
||||||
for _, rc := range config.FinalRules {
|
for _, rc := range config.FinalRules {
|
||||||
rule, err := buildFinalRule(rc)
|
rule, err := buildFinalRule(rc)
|
||||||
@@ -237,6 +226,20 @@ func (h *Handler) blockDelay(rule *FinalRule) time.Duration {
|
|||||||
return time.Duration(min+uint64(dice.Roll(int(span+1)))) * time.Second
|
return time.Duration(min+uint64(dice.Roll(int(span+1)))) * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) blackhole(ctx context.Context, input buf.Reader, output buf.Writer, rule *FinalRule, dest *net.Destination) error {
|
||||||
|
delay := h.blockDelay(rule)
|
||||||
|
errors.LogInfo(ctx, "blocked target: ", *dest, ", blackholing connection for ", delay)
|
||||||
|
timer := time.AfterFunc(delay, func() {
|
||||||
|
common.Interrupt(input)
|
||||||
|
common.Interrupt(output)
|
||||||
|
errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *dest)
|
||||||
|
})
|
||||||
|
defer timer.Stop()
|
||||||
|
defer common.Close(output)
|
||||||
|
_ = buf.Copy(input, buf.Discard)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func isValidAddress(addr *net.IPOrDomain) bool {
|
func isValidAddress(addr *net.IPOrDomain) bool {
|
||||||
if addr == nil {
|
if addr == nil {
|
||||||
return false
|
return false
|
||||||
@@ -256,7 +259,10 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
|||||||
ob.Name = "freedom"
|
ob.Name = "freedom"
|
||||||
ob.CanSpliceCopy = 1
|
ob.CanSpliceCopy = 1
|
||||||
inbound := session.InboundFromContext(ctx)
|
inbound := session.InboundFromContext(ctx)
|
||||||
defaultRule := getDefaultFinalRule(inbound)
|
var defaultRule *FinalRule
|
||||||
|
if !h.usesDialerProxy { // freedom is not the final outbound, final rules do not apply (and the domain is not resolved)
|
||||||
|
defaultRule = getDefaultFinalRule(inbound)
|
||||||
|
}
|
||||||
|
|
||||||
destination := ob.Target
|
destination := ob.Target
|
||||||
origTargetAddr := ob.OriginalTarget.Address
|
origTargetAddr := ob.OriginalTarget.Address
|
||||||
@@ -284,61 +290,53 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
|||||||
var conn stat.Connection
|
var conn stat.Connection
|
||||||
var blockedDest *net.Destination
|
var blockedDest *net.Destination
|
||||||
var blockedRule *FinalRule
|
var blockedRule *FinalRule
|
||||||
firstResolve := true
|
|
||||||
err := retry.ExponentialBackoff(5, 100).On(func() error {
|
err := retry.ExponentialBackoff(5, 100).On(func() error {
|
||||||
dialDest := destination
|
if destination.Address.Family().IsDomain() {
|
||||||
if h.config.DomainStrategy.HasStrategy() && dialDest.Address.Family().IsDomain() {
|
if defaultRule != nil || len(h.finalRules) > 0 {
|
||||||
strategy := h.config.DomainStrategy
|
if strategy := h.resolveStrategy; strategy.HasStrategy() {
|
||||||
if destination.Network == net.Network_UDP && origTargetAddr != nil && outGateway == nil {
|
ips, err := internet.LookupForIP(destination.Address.Domain(), strategy, outGateway)
|
||||||
strategy = strategy.GetDynamicStrategy(origTargetAddr.Family())
|
if err != nil { // non-force may still dial with system DNS
|
||||||
}
|
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", destination.Address.Domain())
|
||||||
ips, err := internet.LookupForIP(dialDest.Address.Domain(), strategy, outGateway)
|
if strategy.ForceIP() {
|
||||||
if err != nil {
|
return err // retry
|
||||||
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", dialDest.Address.Domain())
|
}
|
||||||
if h.config.DomainStrategy.ForceIP() || h.shouldResolveDomainBeforeFinalRules(dialDest, defaultRule) {
|
}
|
||||||
return err
|
for _, ip := range ips {
|
||||||
|
if addr := net.IPAddress(ip); addr != nil {
|
||||||
|
if rule := h.matchFinalRule(destination.Network, addr, destination.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||||
|
blockedDest = &destination
|
||||||
|
blockedDest.Address = addr
|
||||||
|
blockedRule = rule
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addrs, err := net.DefaultResolver.LookupIPAddr(ctx, destination.Address.Domain())
|
||||||
|
if err != nil { // dialer may retry DNS
|
||||||
|
errors.LogInfoInner(ctx, err, "failed to get IP address for domain ", destination.Address.Domain())
|
||||||
|
}
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if ipAddr := net.IPAddress(addr.IP); ipAddr != nil {
|
||||||
|
if rule := h.matchFinalRule(destination.Network, ipAddr, destination.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||||
|
blockedDest = &destination
|
||||||
|
blockedDest.Address = ipAddr
|
||||||
|
blockedRule = rule
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
dialDest = net.Destination{
|
|
||||||
Network: dialDest.Network,
|
|
||||||
Address: net.IPAddress(ips[dice.Roll(len(ips))]),
|
|
||||||
Port: dialDest.Port,
|
|
||||||
}
|
|
||||||
errors.LogInfo(ctx, "dialing to ", dialDest)
|
|
||||||
}
|
}
|
||||||
} else if h.shouldResolveDomainBeforeFinalRules(dialDest, defaultRule) { // asis + domain + hasrules
|
} else {
|
||||||
domain := dialDest.Address.Domain()
|
if rule := h.matchFinalRule(destination.Network, destination.Address, destination.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||||
var ips []net.IP
|
blockedDest = &destination
|
||||||
if firstResolve {
|
blockedRule = rule
|
||||||
firstResolve = false
|
return nil
|
||||||
supportIPv4, supportIPv6 := utils.CheckRoutes()
|
|
||||||
if supportIPv4 {
|
|
||||||
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip4", domain)
|
|
||||||
}
|
|
||||||
if len(ips) == 0 && supportIPv6 {
|
|
||||||
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip6", domain)
|
|
||||||
}
|
|
||||||
if len(ips) == 0 {
|
|
||||||
return errors.New("failed to get IP address for domain ", domain)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ips, _ = net.DefaultResolver.LookupIP(ctx, "ip", domain)
|
|
||||||
}
|
}
|
||||||
if len(ips) == 0 { // SRV/TXT, lookup failed
|
|
||||||
return errors.New("failed to get IP address for domain ", domain)
|
|
||||||
}
|
|
||||||
if addr := net.IPAddress(ips[dice.Roll(len(ips))]); addr != nil {
|
|
||||||
dialDest.Address = addr
|
|
||||||
errors.LogInfo(ctx, "dialing to ", dialDest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if rule := h.matchFinalRule(dialDest.Network, dialDest.Address, dialDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
|
||||||
blockedDest = &dialDest
|
|
||||||
blockedRule = rule
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rawConn, err := dialer.Dial(ctx, dialDest)
|
rawConn, err := dialer.Dial(ctx, destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -350,20 +348,17 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
|||||||
return errors.New("failed to open connection to ", destination).Base(err)
|
return errors.New("failed to open connection to ", destination).Base(err)
|
||||||
}
|
}
|
||||||
if blockedDest != nil {
|
if blockedDest != nil {
|
||||||
delay := h.blockDelay(blockedRule)
|
return h.blackhole(ctx, input, output, blockedRule, blockedDest)
|
||||||
errors.LogInfo(ctx, "blocked target: ", *blockedDest, ", blackholing connection for ", delay)
|
|
||||||
timer := time.AfterFunc(delay, func() {
|
|
||||||
common.Interrupt(input)
|
|
||||||
common.Interrupt(output)
|
|
||||||
errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *blockedDest)
|
|
||||||
})
|
|
||||||
defer timer.Stop()
|
|
||||||
defer common.Close(output)
|
|
||||||
if err := buf.Copy(input, buf.Discard); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
if destination.Address.Family().IsDomain() && (defaultRule != nil || len(h.finalRules) > 0) {
|
||||||
|
// pre-check may fail or dialer may select another IP
|
||||||
|
remoteDest := net.DestinationFromAddr(conn.RemoteAddr())
|
||||||
|
if rule := h.matchFinalRule(remoteDest.Network, remoteDest.Address, remoteDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||||
|
conn.Close()
|
||||||
|
return h.blackhole(ctx, input, output, rule, &remoteDest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 {
|
if h.config.ProxyProtocol > 0 && h.config.ProxyProtocol <= 2 {
|
||||||
version := byte(h.config.ProxyProtocol)
|
version := byte(h.config.ProxyProtocol)
|
||||||
srcAddr := inbound.Source.RawNetAddr()
|
srcAddr := inbound.Source.RawNetAddr()
|
||||||
@@ -408,7 +403,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
|||||||
writer = buf.NewWriter(conn)
|
writer = buf.NewWriter(conn)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
writer = NewPacketWriter(conn, h, defaultRule, UDPOverride, destination)
|
writer = NewPacketWriter(conn, h, defaultRule, UDPOverride, destination, outGateway)
|
||||||
if h.config.Noises != nil {
|
if h.config.Noises != nil {
|
||||||
errors.LogDebug(ctx, "NOISE", h.config.Noises)
|
errors.LogDebug(ctx, "NOISE", h.config.Noises)
|
||||||
writer = &NoisePacketWriter{
|
writer = &NoisePacketWriter{
|
||||||
@@ -472,7 +467,7 @@ func NewPacketReader(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverr
|
|||||||
if statConn != nil {
|
if statConn != nil {
|
||||||
counter = statConn.ReadCounter
|
counter = statConn.ReadCounter
|
||||||
}
|
}
|
||||||
if c, ok := iConn.(*internet.PacketConnWrapper); ok {
|
if c, ok := iConn.(*net.PacketConnWrapper); ok {
|
||||||
isOverridden := false
|
isOverridden := false
|
||||||
if UDPOverride.Address != nil || UDPOverride.Port != 0 {
|
if UDPOverride.Address != nil || UDPOverride.Port != 0 {
|
||||||
isOverridden = true
|
isOverridden = true
|
||||||
@@ -492,7 +487,7 @@ func NewPacketReader(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverr
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PacketReader struct {
|
type PacketReader struct {
|
||||||
*internet.PacketConnWrapper
|
*net.PacketConnWrapper
|
||||||
stats.Counter
|
stats.Counter
|
||||||
Handler *Handler
|
Handler *Handler
|
||||||
DefaultRule *FinalRule
|
DefaultRule *FinalRule
|
||||||
@@ -512,7 +507,7 @@ func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
|||||||
}
|
}
|
||||||
udpAddr := d.(*net.UDPAddr)
|
udpAddr := d.(*net.UDPAddr)
|
||||||
sourceAddr := net.IPAddress(udpAddr.IP)
|
sourceAddr := net.IPAddress(udpAddr.IP)
|
||||||
if r.Handler.applyFinalRules(net.Network_UDP, sourceAddr, net.Port(udpAddr.Port), r.DefaultRule) == RuleAction_Block {
|
if rule := r.Handler.matchFinalRule(net.Network_UDP, sourceAddr, net.Port(udpAddr.Port), r.DefaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
b.Resize(0, int32(n))
|
b.Resize(0, int32(n))
|
||||||
@@ -537,7 +532,7 @@ func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DialDest means the dial target used in the dialer when creating conn
|
// DialDest means the dial target used in the dialer when creating conn
|
||||||
func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverride net.Destination, DialDest net.Destination) buf.Writer {
|
func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverride net.Destination, DialDest net.Destination, outGateway net.Address) buf.Writer {
|
||||||
iConn := conn
|
iConn := conn
|
||||||
statConn, ok := iConn.(*stat.CounterConnection)
|
statConn, ok := iConn.(*stat.CounterConnection)
|
||||||
if ok {
|
if ok {
|
||||||
@@ -547,7 +542,7 @@ func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverr
|
|||||||
if statConn != nil {
|
if statConn != nil {
|
||||||
counter = statConn.WriteCounter
|
counter = statConn.WriteCounter
|
||||||
}
|
}
|
||||||
if c, ok := iConn.(*internet.PacketConnWrapper); ok {
|
if c, ok := iConn.(*net.PacketConnWrapper); ok {
|
||||||
// If DialDest is a domain, it will be resolved in dialer
|
// If DialDest is a domain, it will be resolved in dialer
|
||||||
// check this behavior and add it to map
|
// check this behavior and add it to map
|
||||||
resolvedUDPAddr := utils.NewTypedSyncMap[string, net.Address]()
|
resolvedUDPAddr := utils.NewTypedSyncMap[string, net.Address]()
|
||||||
@@ -561,15 +556,14 @@ func NewPacketWriter(conn net.Conn, h *Handler, defaultRule *FinalRule, UDPOverr
|
|||||||
DefaultRule: defaultRule,
|
DefaultRule: defaultRule,
|
||||||
UDPOverride: UDPOverride,
|
UDPOverride: UDPOverride,
|
||||||
ResolvedUDPAddr: resolvedUDPAddr,
|
ResolvedUDPAddr: resolvedUDPAddr,
|
||||||
LocalAddr: net.DestinationFromAddr(conn.LocalAddr()).Address,
|
OutGateway: outGateway,
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return &buf.SequentialWriter{Writer: conn}
|
return &buf.SequentialWriter{Writer: conn}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PacketWriter struct {
|
type PacketWriter struct {
|
||||||
*internet.PacketConnWrapper
|
*net.PacketConnWrapper
|
||||||
stats.Counter
|
stats.Counter
|
||||||
*Handler
|
*Handler
|
||||||
DefaultRule *FinalRule
|
DefaultRule *FinalRule
|
||||||
@@ -580,7 +574,7 @@ type PacketWriter struct {
|
|||||||
// Resulting in these packets being sent to many different IPs randomly
|
// Resulting in these packets being sent to many different IPs randomly
|
||||||
// So, cache and keep the resolve result
|
// So, cache and keep the resolve result
|
||||||
ResolvedUDPAddr *utils.TypedSyncMap[string, net.Address]
|
ResolvedUDPAddr *utils.TypedSyncMap[string, net.Address]
|
||||||
LocalAddr net.Address
|
OutGateway net.Address
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||||
@@ -603,21 +597,21 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
|||||||
if ip, ok := w.ResolvedUDPAddr.Load(b.UDP.Address.Domain()); ok {
|
if ip, ok := w.ResolvedUDPAddr.Load(b.UDP.Address.Domain()); ok {
|
||||||
b.UDP.Address = ip
|
b.UDP.Address = ip
|
||||||
} else {
|
} else {
|
||||||
ShouldUseSystemResolver := true
|
shouldUseSystemResolver := true
|
||||||
if w.Handler.config.DomainStrategy.HasStrategy() {
|
if strategy := w.Handler.resolveStrategy; strategy.HasStrategy() {
|
||||||
ips, err := internet.LookupForIP(b.UDP.Address.Domain(), w.Handler.config.DomainStrategy, w.LocalAddr)
|
ips, err := internet.LookupForIP(b.UDP.Address.Domain(), strategy, w.OutGateway)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// drop packet if resolve failed when forceIP
|
// drop packet if resolve failed when forceIP
|
||||||
if w.Handler.config.DomainStrategy.ForceIP() {
|
if strategy.ForceIP() {
|
||||||
b.Release()
|
b.Release()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ip = net.IPAddress(ips[dice.Roll(len(ips))])
|
ip = net.IPAddress(ips[dice.Roll(len(ips))])
|
||||||
ShouldUseSystemResolver = false
|
shouldUseSystemResolver = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ShouldUseSystemResolver {
|
if shouldUseSystemResolver {
|
||||||
udpAddr, err := net.ResolveUDPAddr("udp", b.UDP.NetAddr())
|
udpAddr, err := net.ResolveUDPAddr("udp", b.UDP.NetAddr())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Release()
|
b.Release()
|
||||||
@@ -631,7 +625,7 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if w.applyFinalRules(net.Network_UDP, b.UDP.Address, b.UDP.Port, w.DefaultRule) == RuleAction_Block {
|
if rule := w.matchFinalRule(net.Network_UDP, b.UDP.Address, b.UDP.Port, w.DefaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||||
b.Release()
|
b.Release()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,11 +115,7 @@ Start:
|
|||||||
|
|
||||||
request, err := http.ReadRequest(reader)
|
request, err := http.ReadRequest(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
trace := errors.New("failed to read http request").Base(err)
|
return errors.New("failed to read http request").Base(err)
|
||||||
if errors.Cause(err) != io.EOF && !isTimeout(errors.Cause(err)) {
|
|
||||||
trace.AtWarning()
|
|
||||||
}
|
|
||||||
return trace
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(s.config.Accounts) > 0 {
|
if len(s.config.Accounts) > 0 {
|
||||||
@@ -147,7 +143,7 @@ Start:
|
|||||||
}
|
}
|
||||||
dest, err := http_proto.ParseHost(host, defaultPort)
|
dest, err := http_proto.ParseHost(host, defaultPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("malformed proxy host: ", host).AtWarning().Base(err)
|
return errors.New("malformed proxy host: ", host).Base(err)
|
||||||
}
|
}
|
||||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||||
From: conn.RemoteAddr(),
|
From: conn.RemoteAddr(),
|
||||||
@@ -262,7 +258,7 @@ func (s *Server) handlePlainHTTP(ctx context.Context, request *http.Request, wri
|
|||||||
requestWriter := buf.NewBufferedWriter(link.Writer)
|
requestWriter := buf.NewBufferedWriter(link.Writer)
|
||||||
common.Must(requestWriter.SetBuffered(false))
|
common.Must(requestWriter.SetBuffered(false))
|
||||||
if err := request.Write(requestWriter); err != nil {
|
if err := request.Write(requestWriter); err != nil {
|
||||||
return errors.New("failed to write whole request").Base(err).AtWarning()
|
return errors.New("failed to write whole request").Base(err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -299,7 +295,7 @@ func (s *Server) handlePlainHTTP(ctx context.Context, request *http.Request, wri
|
|||||||
response.Header.Set("Proxy-Connection", "close")
|
response.Header.Set("Proxy-Connection", "close")
|
||||||
}
|
}
|
||||||
if err := response.Write(writer); err != nil {
|
if err := response.Write(writer); err != nil {
|
||||||
return errors.New("failed to write response").Base(err).AtWarning()
|
return errors.New("failed to write response").Base(err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -332,7 +328,7 @@ func readResponseAndHandle100Continue(r *bufio.Reader, req *http.Request, writer
|
|||||||
return nil, errors.New("failed to read http 1xx response").Base(err)
|
return nil, errors.New("failed to read http 1xx response").Base(err)
|
||||||
}
|
}
|
||||||
ResponseHeader1xx = append(ResponseHeader1xx, data...)
|
ResponseHeader1xx = append(ResponseHeader1xx, data...)
|
||||||
if bytes.Equal(ResponseHeader1xx[len(ResponseHeader1xx)-4:], []byte{'\r', '\n', '\r', '\n'}) {
|
if len(ResponseHeader1xx) >= 4 && bytes.Equal(ResponseHeader1xx[len(ResponseHeader1xx)-4:], []byte{'\r', '\n', '\r', '\n'}) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if len(ResponseHeader1xx) > 1024 {
|
if len(ResponseHeader1xx) > 1024 {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A malformed upstream response containing a bare '\n' before the real
|
||||||
|
// status line used to crash readResponseAndHandle100Continue: the first
|
||||||
|
// ReadSlice('\n') returns fewer than 4 bytes, and slicing
|
||||||
|
// ResponseHeader1xx[len(ResponseHeader1xx)-4:] panicked with a negative
|
||||||
|
// index instead of returning an error.
|
||||||
|
func TestReadResponseAndHandle100ContinueDoesNotPanicOnEarlyNewline(t *testing.T) {
|
||||||
|
payload := "X\nHTTP/1.1 100 Continue\r\n\r\n" + strings.Repeat("A", 40)
|
||||||
|
r := bufio.NewReader(bytes.NewReader([]byte(payload)))
|
||||||
|
req, err := http.NewRequest("GET", "http://example.com/", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must not panic; a parse error for the garbage trailing bytes is fine.
|
||||||
|
_, _ = readResponseAndHandle100Continue(r, req, io.Discard)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadResponseAndHandle100ContinueForwardsAndParsesFinalResponse(t *testing.T) {
|
||||||
|
payload := "HTTP/1.1 100 Continue\r\n\r\n" +
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
|
||||||
|
r := bufio.NewReader(bytes.NewReader([]byte(payload)))
|
||||||
|
req, err := http.NewRequest("GET", "http://example.com/", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var forwarded bytes.Buffer
|
||||||
|
resp, err := readResponseAndHandle100Continue(r, req, &forwarded)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
t.Fatalf("expected status 200, got %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
if !strings.Contains(forwarded.String(), "100 Continue") {
|
||||||
|
t.Fatalf("expected 1xx response to be forwarded, got %q", forwarded.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,7 +62,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
|
|||||||
|
|
||||||
conn, err := dialer.Dial(hysteria.ContextWithDatagram(ctx, target.Network == net.Network_UDP), c.server.Destination)
|
conn, err := dialer.Dial(hysteria.ContextWithDatagram(ctx, target.Network == net.Network_UDP), c.server.Destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to find an available destination").AtWarning().Base(err)
|
return errors.New("failed to find an available destination").Base(err)
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
errors.LogInfo(ctx, "tunneling request to ", target, " via ", target.Network, ":", c.server.Destination.NetAddr())
|
errors.LogInfo(ctx, "tunneling request to ", target, " via ", target.Network, ":", c.server.Destination.NetAddr())
|
||||||
@@ -236,14 +236,14 @@ type UDPReader struct {
|
|||||||
|
|
||||||
func (r *UDPReader) ReadFrom(p []byte) (n int, addr *net.Destination, err error) {
|
func (r *UDPReader) ReadFrom(p []byte) (n int, addr *net.Destination, err error) {
|
||||||
for {
|
for {
|
||||||
var buf [hysteria.MaxDatagramFrameSize]byte
|
var packet [1500]byte
|
||||||
|
|
||||||
n, err := r.reader.Read(buf[:])
|
n, err := r.reader.Read(packet[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
msg, err := ParseUDPMessage(buf[:n])
|
msg, err := ParseUDPMessage(packet[:n])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
|
|||||||
for _, user := range config.Users {
|
for _, user := range config.Users {
|
||||||
u, err := user.ToMemoryUser()
|
u, err := user.ToMemoryUser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("failed to get hysteria user").Base(err).AtError()
|
return nil, errors.New("failed to get hysteria user").Base(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validator.Add(u); err != nil {
|
if err := validator.Add(u); err != nil {
|
||||||
return nil, errors.New("failed to add user").Base(err).AtError()
|
return nil, errors.New("failed to add user").Base(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func (l *Loopback) init(config *Config, dispatcherInstance routing.Dispatcher) e
|
|||||||
if config.Sniffing.GetEnabled() {
|
if config.Sniffing.GetEnabled() {
|
||||||
request, err := proxyman.BuildSniffingRequest(config.Sniffing)
|
request, err := proxyman.BuildSniffingRequest(config.Sniffing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to build loopback sniffing request").Base(err).AtError()
|
return errors.New("failed to build loopback sniffing request").Base(err)
|
||||||
}
|
}
|
||||||
l.sniffingRequest = request
|
l.sniffingRequest = request
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package masque
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (a *Account) AsAccount() (protocol.Account, error) {
|
||||||
|
return &MemoryAccount{Password: a.Password}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemoryAccount struct {
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *MemoryAccount) Equals(other protocol.Account) bool {
|
||||||
|
b, ok := other.(*MemoryAccount)
|
||||||
|
return ok && a.Password == b.Password
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *MemoryAccount) ToProto() proto.Message {
|
||||||
|
return &Account{Password: a.Password}
|
||||||
|
}
|
||||||
|
|
||||||
|
type validator struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
users map[string]*protocol.MemoryUser
|
||||||
|
}
|
||||||
|
|
||||||
|
func newValidator() *validator {
|
||||||
|
return &validator{users: make(map[string]*protocol.MemoryUser)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validator) add(user *protocol.MemoryUser) error {
|
||||||
|
account, ok := user.Account.(*MemoryAccount)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("not a MASQUE account")
|
||||||
|
}
|
||||||
|
if user.Email == "" || strings.Contains(user.Email, ":") {
|
||||||
|
return errors.New("invalid email ", user.Email)
|
||||||
|
}
|
||||||
|
if account.Password == "" {
|
||||||
|
return errors.New("empty password for ", user.Email)
|
||||||
|
}
|
||||||
|
email := strings.ToLower(user.Email)
|
||||||
|
v.mu.Lock()
|
||||||
|
defer v.mu.Unlock()
|
||||||
|
if _, found := v.users[email]; found {
|
||||||
|
return errors.New("user ", user.Email, " already exists")
|
||||||
|
}
|
||||||
|
v.users[email] = user
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validator) delByEmail(email string) (*protocol.MemoryUser, error) {
|
||||||
|
key := strings.ToLower(email)
|
||||||
|
v.mu.Lock()
|
||||||
|
defer v.mu.Unlock()
|
||||||
|
user, found := v.users[key]
|
||||||
|
if !found {
|
||||||
|
return nil, errors.New("user ", email, " not found")
|
||||||
|
}
|
||||||
|
delete(v.users, key)
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validator) contains(user *protocol.MemoryUser) bool {
|
||||||
|
v.mu.RLock()
|
||||||
|
defer v.mu.RUnlock()
|
||||||
|
return v.users[strings.ToLower(user.Email)] == user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validator) get(email, password string) *protocol.MemoryUser {
|
||||||
|
v.mu.RLock()
|
||||||
|
user := v.users[strings.ToLower(email)]
|
||||||
|
v.mu.RUnlock()
|
||||||
|
if user == nil || subtle.ConstantTimeCompare([]byte(user.Account.(*MemoryAccount).Password), []byte(password)) != 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validator) getByEmail(email string) *protocol.MemoryUser {
|
||||||
|
v.mu.RLock()
|
||||||
|
defer v.mu.RUnlock()
|
||||||
|
return v.users[strings.ToLower(email)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validator) getAll() []*protocol.MemoryUser {
|
||||||
|
v.mu.RLock()
|
||||||
|
defer v.mu.RUnlock()
|
||||||
|
users := make([]*protocol.MemoryUser, 0, len(v.users))
|
||||||
|
for _, user := range v.users {
|
||||||
|
users = append(users, user)
|
||||||
|
}
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *validator) count() int64 {
|
||||||
|
v.mu.RLock()
|
||||||
|
defer v.mu.RUnlock()
|
||||||
|
return int64(len(v.users))
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package masque
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidator(t *testing.T) {
|
||||||
|
v := newValidator()
|
||||||
|
user := &protocol.MemoryUser{Email: "U@example.com", Account: &MemoryAccount{Password: "p"}}
|
||||||
|
require.NoError(t, v.add(user))
|
||||||
|
for _, u := range []*protocol.MemoryUser{
|
||||||
|
{Email: "u@example.com", Account: &MemoryAccount{Password: "other"}},
|
||||||
|
{Account: &MemoryAccount{Password: "p"}},
|
||||||
|
{Email: "a:b", Account: &MemoryAccount{Password: "p"}},
|
||||||
|
{Email: "b@example.com", Account: &MemoryAccount{}},
|
||||||
|
} {
|
||||||
|
require.Error(t, v.add(u), u.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, user, v.get("u@example.com", "p"))
|
||||||
|
require.Equal(t, user, v.get("U@EXAMPLE.COM", "p"))
|
||||||
|
require.Nil(t, v.get("u@example.com", "x"))
|
||||||
|
require.Nil(t, v.get("x@example.com", "p"))
|
||||||
|
require.Nil(t, v.get("", ""))
|
||||||
|
require.Equal(t, user, v.getByEmail("u@example.com"))
|
||||||
|
require.Equal(t, []*protocol.MemoryUser{user}, v.getAll())
|
||||||
|
require.Equal(t, int64(1), v.count())
|
||||||
|
|
||||||
|
require.True(t, v.contains(user))
|
||||||
|
removed, err := v.delByEmail("u@EXAMPLE.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, user, removed)
|
||||||
|
_, err = v.delByEmail("u@example.com")
|
||||||
|
require.Error(t, err)
|
||||||
|
require.False(t, v.contains(user))
|
||||||
|
require.Nil(t, v.get("u@example.com", "p"))
|
||||||
|
require.Zero(t, v.count())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccount(t *testing.T) {
|
||||||
|
account, err := (&Account{Password: "p"}).AsAccount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, account.Equals(&MemoryAccount{Password: "p"}))
|
||||||
|
require.False(t, account.Equals(&MemoryAccount{Password: "x"}))
|
||||||
|
require.Equal(t, &Account{Password: "p"}, account.ToProto())
|
||||||
|
}
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
package masque
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
go_errors "errors"
|
||||||
|
"io"
|
||||||
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.zx2c4.com/wireguard/tun"
|
||||||
|
|
||||||
|
"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/protocol"
|
||||||
|
"github.com/xtls/xray-core/common/session"
|
||||||
|
"github.com/xtls/xray-core/common/signal"
|
||||||
|
"github.com/xtls/xray-core/common/task"
|
||||||
|
"github.com/xtls/xray-core/core"
|
||||||
|
"github.com/xtls/xray-core/features/policy"
|
||||||
|
"github.com/xtls/xray-core/proxy/wireguard"
|
||||||
|
"github.com/xtls/xray-core/transport"
|
||||||
|
"github.com/xtls/xray-core/transport/internet"
|
||||||
|
"github.com/xtls/xray-core/transport/internet/masque"
|
||||||
|
"github.com/xtls/xray-core/transport/internet/stat"
|
||||||
|
"github.com/xtls/xray-core/transport/internet/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
establishTimeout = 10 * time.Second
|
||||||
|
retryInterval = time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
server *protocol.ServerSpec
|
||||||
|
policyManager policy.Manager
|
||||||
|
remoteDNS []netip.Addr
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
tunnel atomic.Pointer[tunnel]
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
lastErr error
|
||||||
|
lastErrAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) {
|
||||||
|
v := core.MustFromContext(ctx)
|
||||||
|
p := v.GetFeature(policy.ManagerType()).(policy.Manager)
|
||||||
|
|
||||||
|
streamSettings := session.StreamSettingsFromContext(ctx).(*internet.MemoryStreamConfig)
|
||||||
|
if _, ok := streamSettings.ProtocolSettings.(*masque.Config); !ok {
|
||||||
|
return nil, errors.New("not masque transport")
|
||||||
|
}
|
||||||
|
if tls.ConfigFromStreamSettings(streamSettings) == nil {
|
||||||
|
return nil, errors.New(`MASQUE requires "security": "tls"`)
|
||||||
|
}
|
||||||
|
if config.Server == nil {
|
||||||
|
return nil, errors.New(`no target server found`)
|
||||||
|
}
|
||||||
|
server, err := protocol.NewServerSpecFromPB(config.Server)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("failed to get server spec").Base(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dns := config.RemoteDns
|
||||||
|
if len(dns) == 0 {
|
||||||
|
dns = []string{"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"}
|
||||||
|
}
|
||||||
|
remoteDNS := make([]netip.Addr, 0, len(dns))
|
||||||
|
for _, s := range dns {
|
||||||
|
addr, err := netip.ParseAddr(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("invalid remote DNS server ", s).Base(err)
|
||||||
|
}
|
||||||
|
remoteDNS = append(remoteDNS, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Client{
|
||||||
|
server: server,
|
||||||
|
policyManager: p,
|
||||||
|
remoteDNS: remoteDNS,
|
||||||
|
}
|
||||||
|
c.ctx, c.cancel = context.WithCancel(context.Background())
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error {
|
||||||
|
outbounds := session.OutboundsFromContext(ctx)
|
||||||
|
ob := outbounds[len(outbounds)-1]
|
||||||
|
if !ob.Target.IsValid() {
|
||||||
|
return errors.New("target not specified")
|
||||||
|
}
|
||||||
|
ob.Name = "masque"
|
||||||
|
ob.CanSpliceCopy = 3
|
||||||
|
|
||||||
|
t, err := c.getTunnel(ctx, dialer)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to establish CONNECT-IP tunnel").Base(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var newCtx context.Context
|
||||||
|
var newCancel context.CancelFunc
|
||||||
|
if session.TimeoutOnlyFromContext(ctx) {
|
||||||
|
newCtx, newCancel = context.WithCancel(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionPolicy := c.policyManager.ForLevel(0)
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
timer := signal.CancelAfterInactivity(ctx, func() {
|
||||||
|
cancel()
|
||||||
|
if newCancel != nil {
|
||||||
|
newCancel()
|
||||||
|
}
|
||||||
|
}, sessionPolicy.Timeouts.ConnectionIdle)
|
||||||
|
|
||||||
|
if newCtx != nil {
|
||||||
|
ctx = newCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
var reader buf.Reader
|
||||||
|
var writer buf.Writer
|
||||||
|
|
||||||
|
switch ob.Target.Network {
|
||||||
|
case net.Network_TCP:
|
||||||
|
var conn net.Conn
|
||||||
|
var err error
|
||||||
|
if sessionPolicy.Timeouts.Handshake != 0 {
|
||||||
|
timeoutCtx, timeoutCancel := context.WithTimeout(ctx, sessionPolicy.Timeouts.Handshake)
|
||||||
|
conn, err = t.tnet.DialContext(timeoutCtx, "tcp", ob.Target.NetAddr())
|
||||||
|
timeoutCancel()
|
||||||
|
} else {
|
||||||
|
conn, err = t.tnet.Dial("tcp", ob.Target.NetAddr())
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to create TCP connection").Base(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
reader = buf.NewReader(conn)
|
||||||
|
writer = buf.NewWriter(conn)
|
||||||
|
case net.Network_UDP:
|
||||||
|
conn, err := t.tnet.Dial("udp", ob.Target.NetAddr())
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("failed to create UDP connection").Base(err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
uc := &wireguard.UDPConnClient{
|
||||||
|
PacketConn: conn.(*net.PacketConnWrapper).PacketConn,
|
||||||
|
Dest: conn.RemoteAddr().(*net.UDPAddr),
|
||||||
|
}
|
||||||
|
reader = uc
|
||||||
|
writer = uc
|
||||||
|
default:
|
||||||
|
panic(ob.Target.Network)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestFunc := func() error {
|
||||||
|
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
|
||||||
|
return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer))
|
||||||
|
}
|
||||||
|
|
||||||
|
responseFunc := func() error {
|
||||||
|
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||||
|
return buf.Copy(reader, link.Writer, buf.UpdateActivity(timer))
|
||||||
|
}
|
||||||
|
|
||||||
|
responseDonePost := task.OnSuccess(responseFunc, task.Close(link.Writer))
|
||||||
|
if err := task.Run(ctx, requestFunc, responseDonePost); err != nil {
|
||||||
|
common.Interrupt(link.Reader)
|
||||||
|
common.Interrupt(link.Writer)
|
||||||
|
return errors.New("connection ends").Base(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getTunnel(ctx context.Context, dialer internet.Dialer) (*tunnel, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
if c.ctx.Err() != nil {
|
||||||
|
return nil, errors.New("closed")
|
||||||
|
}
|
||||||
|
if t := c.tunnel.Load(); t != nil {
|
||||||
|
select {
|
||||||
|
case <-t.done:
|
||||||
|
default:
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if c.lastErr != nil && time.Since(c.lastErrAt) < retryInterval {
|
||||||
|
return nil, c.lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := c.establish(ctx, dialer)
|
||||||
|
if err != nil {
|
||||||
|
c.lastErr, c.lastErrAt = err, time.Now()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.lastErr = nil
|
||||||
|
c.tunnel.Store(t)
|
||||||
|
if c.ctx.Err() != nil {
|
||||||
|
if c.tunnel.CompareAndSwap(t, nil) {
|
||||||
|
t.close()
|
||||||
|
}
|
||||||
|
return nil, errors.New("closed")
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) establish(ctx context.Context, dialer internet.Dialer) (*tunnel, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), establishTimeout)
|
||||||
|
defer cancel()
|
||||||
|
defer context.AfterFunc(c.ctx, cancel)()
|
||||||
|
conn, err := dialer.Dial(ctx, c.server.Destination)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mconn, ok := stat.TryUnwrapStatsConn(conn).(*masque.Conn)
|
||||||
|
if !ok {
|
||||||
|
conn.Close()
|
||||||
|
return nil, errors.New("not a CONNECT-IP connection")
|
||||||
|
}
|
||||||
|
t, err := newTunnel(conn, mconn.LocalAddrs(), c.remoteDNS)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
errors.LogInfo(ctx, "MASQUE: tunnel established from ", mconn.LocalAddrs())
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
c.cancel()
|
||||||
|
if t := c.tunnel.Swap(nil); t != nil {
|
||||||
|
t.close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type tunnel struct {
|
||||||
|
conn stat.Connection
|
||||||
|
dev tun.Device
|
||||||
|
tnet *wireguard.Net
|
||||||
|
done chan struct{}
|
||||||
|
closeOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTunnel(conn stat.Connection, local []netip.Addr, remoteDNS []netip.Addr) (*tunnel, error) {
|
||||||
|
var dns []netip.Addr
|
||||||
|
for _, addr := range remoteDNS {
|
||||||
|
if slices.ContainsFunc(local, func(l netip.Addr) bool { return l.Is4() == addr.Is4() }) {
|
||||||
|
dns = append(dns, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(dns) == 0 {
|
||||||
|
errors.LogWarning(context.Background(), "MASQUE: no remote DNS server is reachable from the assigned addresses ", local, ", domain names will fail to resolve")
|
||||||
|
dns = remoteDNS
|
||||||
|
}
|
||||||
|
|
||||||
|
dev, tnet, _, err := wireguard.CreateNetTUN(local, dns, masque.MinPacketSize, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t := &tunnel{
|
||||||
|
conn: conn,
|
||||||
|
dev: dev,
|
||||||
|
tnet: tnet,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
go t.readFromTunnel()
|
||||||
|
go t.writeToTunnel()
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tunnel) readFromTunnel() {
|
||||||
|
defer t.close()
|
||||||
|
b := make([]byte, buf.Size)
|
||||||
|
for {
|
||||||
|
n, err := t.conn.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
if go_errors.Is(err, io.ErrShortBuffer) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
errors.LogInfoInner(context.Background(), err, "MASQUE: tunnel closed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.dev.Write([][]byte{b[:n]}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tunnel) writeToTunnel() {
|
||||||
|
bufs := [][]byte{make([]byte, masque.MinPacketSize)}
|
||||||
|
sizes := []int{0}
|
||||||
|
for {
|
||||||
|
if _, err := t.dev.Read(bufs, sizes, 0); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := t.conn.Write(bufs[0][:sizes[0]]); err != nil {
|
||||||
|
var ptb *masque.PacketTooBigError
|
||||||
|
if go_errors.As(err, &ptb) {
|
||||||
|
go t.dev.Write([][]byte{ptb.ICMP}, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tunnel) close() {
|
||||||
|
t.closeOnce.Do(func() {
|
||||||
|
close(t.done)
|
||||||
|
t.conn.Close()
|
||||||
|
t.dev.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||||
|
return NewClient(ctx, config.(*ClientConfig))
|
||||||
|
}))
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user