mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-02 13:56:39 +00:00
Compare commits
21
Commits
lua
...
windows-tun-fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
747b153333 | ||
|
|
94cd83ccb4 | ||
|
|
1c225a8041 | ||
|
|
3a6bdb19ba | ||
|
|
de02da553a | ||
|
|
4ec4fb8aab | ||
|
|
63de6135cb | ||
|
|
edd916b08e | ||
|
|
eb29a4e3de | ||
|
|
2db099b34b | ||
|
|
7780db9bbe | ||
|
|
2953d44734 | ||
|
|
7b8ade3ec5 | ||
|
|
5dda894e29 | ||
|
|
47a2c2ffdc | ||
|
|
7a018833ec | ||
|
|
65e853ed84 | ||
|
|
3519dfecbd | ||
|
|
df261e4479 | ||
|
|
61cad5ec8b | ||
|
|
a642a190ed |
@@ -23,7 +23,7 @@ func newFakeDNSSniffer(ctx context.Context) (protocolSnifferWithMetadata, error)
|
||||
}
|
||||
|
||||
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{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() {
|
||||
ips = append(ips, addr.IP())
|
||||
} 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
|
||||
|
||||
@@ -212,6 +212,28 @@ func (s *DNS) IsOwnLink(ctx context.Context) bool {
|
||||
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.
|
||||
func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, error) {
|
||||
// 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
|
||||
h, err := parser.Start(payload)
|
||||
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 {
|
||||
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()
|
||||
|
||||
@@ -58,7 +58,7 @@ func NewFakeDNSHolder() (*Holder, error) {
|
||||
var err error
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -80,13 +80,13 @@ func (fkdns *Holder) initialize(ipPoolCidr string, lruSize int) error {
|
||||
var err error
|
||||
|
||||
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()
|
||||
rooms := bits - ones
|
||||
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.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
|
||||
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.
|
||||
@@ -102,7 +102,7 @@ func NewClient(
|
||||
// Create a new server for each client for now
|
||||
server, err := NewServer(ctx, ns.Address.AsDestination(), dispatcher, disableCache, serveStale, serveExpiredTTL, clientIP)
|
||||
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)
|
||||
@@ -113,7 +113,7 @@ func NewClient(
|
||||
if len(ns.ExpectedIp) > 0 {
|
||||
expectedMatcher, err = geodata.IPReg.BuildIPMatcher(ns.ExpectedIp)
|
||||
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 {
|
||||
unexpectedMatcher, err = geodata.IPReg.BuildIPMatcher(ns.UnexpectedIp)
|
||||
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) {
|
||||
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
|
||||
@@ -39,7 +39,7 @@ func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, opt dns.IPOp
|
||||
|
||||
netIP, err := toNetIP(ips)
|
||||
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)
|
||||
|
||||
+6
-2
@@ -89,10 +89,10 @@ func (g *Instance) startInternal() error {
|
||||
g.active = true
|
||||
|
||||
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 {
|
||||
return errors.New("failed to initialize error logger").Base(err).AtWarning()
|
||||
return errors.New("failed to initialize error logger").Base(err)
|
||||
}
|
||||
|
||||
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().
|
||||
func (g *Instance) Close() error {
|
||||
errors.LogDebug(context.Background(), "Logger closing")
|
||||
|
||||
@@ -66,7 +66,7 @@ func NewAlwaysOnInboundHandler(ctx context.Context, tag string, receiverConfig *
|
||||
}
|
||||
mss, err := internet.ToMemoryStreamConfig(receiverConfig.StreamSettings)
|
||||
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})
|
||||
|
||||
@@ -165,7 +165,7 @@ func NewHandler(ctx context.Context, config *core.InboundHandlerConfig) (inbound
|
||||
|
||||
receiverSettings, ok := rawReceiverSettings.(*proxyman.ReceiverConfig)
|
||||
if !ok {
|
||||
return nil, errors.New("not a ReceiverConfig").AtError()
|
||||
return nil, errors.New("not a ReceiverConfig")
|
||||
}
|
||||
|
||||
streamSettings := receiverSettings.StreamSettings
|
||||
|
||||
@@ -142,7 +142,7 @@ func (w *tcpWorker) Start() error {
|
||||
go w.callback(conn)
|
||||
})
|
||||
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
|
||||
return nil
|
||||
@@ -528,7 +528,7 @@ func (w *dsWorker) Start() error {
|
||||
go w.callback(conn)
|
||||
})
|
||||
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
|
||||
return nil
|
||||
|
||||
@@ -87,7 +87,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
|
||||
h.senderSettings = s
|
||||
mss, err := internet.ToMemoryStreamConfig(s.StreamSettings)
|
||||
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
|
||||
default:
|
||||
@@ -217,7 +217,7 @@ func (h *Handler) Dispatch(ctx context.Context, link *transport.Link) {
|
||||
if ob.Target.Network == net.Network_UDP && ob.Target.Port == 443 {
|
||||
switch h.udp443 {
|
||||
case "reject":
|
||||
test(errors.New("XUDP rejected UDP/443 traffic").AtInfo())
|
||||
test(errors.New("XUDP rejected UDP/443 traffic"))
|
||||
return
|
||||
case "skip":
|
||||
goto out
|
||||
|
||||
@@ -68,13 +68,13 @@ func (p *Portal) HandleConnection(ctx context.Context, link *transport.Link) err
|
||||
outbounds := session.OutboundsFromContext(ctx)
|
||||
ob := outbounds[len(outbounds)-1]
|
||||
if ob == nil {
|
||||
return errors.New("outbound metadata not found").AtError()
|
||||
return errors.New("outbound metadata not found")
|
||||
}
|
||||
|
||||
if isDomain(ob.Target, p.domain) {
|
||||
muxClient, err := mux.NewClientWorker(*link, mux.ClientStrategy{})
|
||||
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)
|
||||
|
||||
@@ -115,7 +115,7 @@ func (rr *RoutingRule) BuildCondition() (Condition, error) {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -145,7 +145,7 @@ func (br *BalancingRule) Build(ohm outbound.Manager, dispatcher routing.Dispatch
|
||||
}
|
||||
s, ok := i.(*StrategyLeastLoadConfig)
|
||||
if !ok {
|
||||
return nil, errors.New("not a StrategyLeastLoadConfig").AtError()
|
||||
return nil, errors.New("not a StrategyLeastLoadConfig")
|
||||
}
|
||||
leastLoadStrategy := NewLeastLoadStrategy(s)
|
||||
return &Balancer{
|
||||
|
||||
+13
-65
@@ -18,17 +18,12 @@ type hasInnerError interface {
|
||||
Unwrap() error
|
||||
}
|
||||
|
||||
type hasSeverity interface {
|
||||
Severity() log.Severity
|
||||
}
|
||||
|
||||
// Error is an error object with underlying error.
|
||||
type Error struct {
|
||||
prefix []interface{}
|
||||
message []interface{}
|
||||
caller string
|
||||
inner error
|
||||
severity log.Severity
|
||||
prefix []interface{}
|
||||
message []interface{}
|
||||
caller string
|
||||
inner error
|
||||
}
|
||||
|
||||
// Error implements error.Error().
|
||||
@@ -69,46 +64,6 @@ func (err *Error) Base(e error) *Error {
|
||||
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.
|
||||
func (err *Error) String() string {
|
||||
return err.Error()
|
||||
@@ -132,9 +87,8 @@ func New(msg ...interface{}) *Error {
|
||||
details = details[:i]
|
||||
}
|
||||
return &Error{
|
||||
message: msg,
|
||||
severity: log.Severity_Info,
|
||||
caller: details,
|
||||
message: msg,
|
||||
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{}) {
|
||||
if log.GetSeverity() < severity {
|
||||
return
|
||||
}
|
||||
pc, _, _, _ := runtime.Caller(2)
|
||||
details := runtime.FuncForPC(pc).Name()
|
||||
if len(details) >= trim {
|
||||
@@ -181,10 +138,9 @@ func doLog(ctx context.Context, inner error, severity log.Severity, msg ...inter
|
||||
details = details[:i]
|
||||
}
|
||||
err := &Error{
|
||||
message: msg,
|
||||
severity: severity,
|
||||
caller: details,
|
||||
inner: inner,
|
||||
message: msg,
|
||||
caller: details,
|
||||
inner: inner,
|
||||
}
|
||||
if ctx != nil && ctx != context.Background() {
|
||||
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{
|
||||
Severity: GetSeverity(err),
|
||||
Severity: severity,
|
||||
Content: err,
|
||||
})
|
||||
}
|
||||
@@ -217,11 +173,3 @@ L:
|
||||
}
|
||||
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/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/log"
|
||||
)
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
err := New("TestError")
|
||||
if v := GetSeverity(err); v != log.Severity_Info {
|
||||
t.Error("severity: ", v)
|
||||
if v := err.Error(); !strings.Contains(v, "TestError") {
|
||||
t.Error("error: ", v)
|
||||
}
|
||||
|
||||
err = New("TestError2").Base(io.EOF)
|
||||
if v := GetSeverity(err); v != log.Severity_Info {
|
||||
t.Error("severity: ", v)
|
||||
if v := err.Error(); !strings.Contains(v, "EOF") {
|
||||
t.Error("error: ", v)
|
||||
}
|
||||
|
||||
err = New("TestError3").Base(io.EOF).AtWarning()
|
||||
if v := GetSeverity(err); v != log.Severity_Warning {
|
||||
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)
|
||||
}
|
||||
err = New("TestError3").Base(io.EOF)
|
||||
err = New("TestError4").Base(err)
|
||||
if v := err.Error(); !strings.Contains(v, "EOF") {
|
||||
t.Error("error: ", v)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"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
|
||||
|
||||
func benchmarkMatcherType(b *testing.B, t Type, ctor func() MatcherGroup) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package strmatcher
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"math/bits"
|
||||
"runtime"
|
||||
"sort"
|
||||
@@ -38,19 +40,21 @@ type mphRuleInfo struct {
|
||||
// MphMatcherGroup is an implementation of MatcherGroup.
|
||||
// It implements Rabin-Karp algorithm and minimal perfect hash table for Full and Domain matcher.
|
||||
type MphMatcherGroup struct {
|
||||
rules []string // RuleIdx -> pattern string, index 0 reserved for failed lookup
|
||||
values [][]uint32 // RuleIdx -> registered matcher values for the pattern (Full Matcher takes precedence)
|
||||
level0 []uint32 // RollingHash & Mask -> seed for Memhash
|
||||
level0Mask uint32 // Mask restricting RollingHash to 0 ~ len(level0)
|
||||
level1 []uint32 // Memhash<seed> & Mask -> stored index for rules
|
||||
level1Mask uint32 // Mask for restricting Memhash<seed> to 0 ~ len(level1)
|
||||
ruleInfos *map[string]mphRuleInfo
|
||||
patterns string // All rule patterns concatenated
|
||||
patternOffs []uint32 // RuleIdx -> patterns[patternOffs[i]:patternOffs[i+1]], index 0 reserved for failed lookup
|
||||
values []uint32 // All registered matcher values concatenated
|
||||
valueOffs []uint32 // RuleIdx -> values[valueOffs[i]:valueOffs[i+1]] (Full Matcher takes precedence)
|
||||
level0 []uint32 // RollingHash & Mask -> seed for Memhash
|
||||
level0Mask uint32 // Mask restricting RollingHash to 0 ~ len(level0)
|
||||
level1 []uint32 // Memhash<seed> & Mask -> stored index for rules
|
||||
level1Mask uint32 // Mask for restricting Memhash<seed> to 0 ~ len(level1)
|
||||
rules []string // RuleIdx -> pattern string, only used for building
|
||||
ruleInfos *map[string]mphRuleInfo
|
||||
}
|
||||
|
||||
func NewMphMatcherGroup() *MphMatcherGroup {
|
||||
return &MphMatcherGroup{
|
||||
rules: []string{""},
|
||||
values: [][]uint32{nil},
|
||||
level0: nil,
|
||||
level0Mask: 0,
|
||||
level1: nil,
|
||||
@@ -78,7 +82,6 @@ func (g *MphMatcherGroup) addPattern(suffixHash uint32, suffixPattern string, pa
|
||||
if !found {
|
||||
info = mphRuleInfo{rollingHash: RollingHash(suffixHash, pattern)}
|
||||
g.rules = append(g.rules, fullPattern)
|
||||
g.values = append(g.values, nil)
|
||||
}
|
||||
info.matchers[matcherType] = append(info.matchers[matcherType], value)
|
||||
(*g.ruleInfos)[fullPattern] = info
|
||||
@@ -94,14 +97,30 @@ func (g *MphMatcherGroup) Build() error {
|
||||
g.level1 = make([]uint32, nextPow2(ruleCount))
|
||||
g.level1Mask = uint32(len(g.level1) - 1)
|
||||
|
||||
// Flatten patterns and values so the built group has no per-rule objects
|
||||
valueCount := 0
|
||||
for _, ruleInfo := range *g.ruleInfos {
|
||||
valueCount += len(ruleInfo.matchers[Full]) + len(ruleInfo.matchers[Domain])
|
||||
}
|
||||
g.patterns = strings.Join(g.rules, "")
|
||||
if uint64(len(g.patterns)) > math.MaxUint32 || uint64(valueCount) > math.MaxUint32 {
|
||||
return errors.New("too many rules for MphMatcherGroup")
|
||||
}
|
||||
g.patternOffs = make([]uint32, len(g.rules)+1)
|
||||
g.values = make([]uint32, 0, valueCount)
|
||||
g.valueOffs = make([]uint32, len(g.rules)+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.patternOffs[ruleIdx+1] = g.patternOffs[ruleIdx] + uint32(len(g.rules[ruleIdx]))
|
||||
g.values = append(append(g.values, ruleInfo.matchers[Full]...), ruleInfo.matchers[Domain]...)
|
||||
g.valueOffs[ruleIdx+1] = uint32(len(g.values))
|
||||
}
|
||||
g.rules = nil
|
||||
g.ruleInfos = nil // Set ruleInfos nil to release memory
|
||||
runtime.GC() // peak mem
|
||||
|
||||
@@ -121,7 +140,7 @@ func (g *MphMatcherGroup) Build() error {
|
||||
seed := uint32(0)
|
||||
for len(hashedBucket) != len(bucket) {
|
||||
for _, ruleIdx := range bucket {
|
||||
memHash := MemHash(seed, g.rules[ruleIdx]) & g.level1Mask
|
||||
memHash := MemHash(seed, g.pattern(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
|
||||
@@ -141,12 +160,26 @@ func (g *MphMatcherGroup) Build() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *MphMatcherGroup) pattern(ruleIdx uint32) string {
|
||||
return g.patterns[g.patternOffs[ruleIdx]:g.patternOffs[ruleIdx+1]]
|
||||
}
|
||||
|
||||
// valuesOf caps the capacity, so appending to a Match result can't overwrite the next rule's values.
|
||||
func (g *MphMatcherGroup) valuesOf(ruleIdx uint32) []uint32 {
|
||||
start, end := g.valueOffs[ruleIdx], g.valueOffs[ruleIdx+1]
|
||||
return g.values[start:end:end]
|
||||
}
|
||||
|
||||
// Lookup searches for input in minimal perfect hash table and returns its index. 0 indicates not found.
|
||||
func (g *MphMatcherGroup) Lookup(rollingHash uint32, input string) uint32 {
|
||||
i0 := rollingHash & g.level0Mask
|
||||
seed := g.level0[i0]
|
||||
i1 := MemHash(seed, input) & g.level1Mask
|
||||
if n := g.level1[i1]; g.rules[n] == input {
|
||||
n := g.level1[i1]
|
||||
// Build only puts valid rule indices in level1, so n+1 < len(patternOffs) and the span is inside patterns.
|
||||
// Skip the bounds checks, they made this hot path measurably slower than indexing a []string
|
||||
offs := (*[2]uint32)(unsafe.Add(unsafe.Pointer(unsafe.SliceData(g.patternOffs)), uintptr(n)*4))
|
||||
if start := offs[0]; int(offs[1]-start) == len(input) && unsafe.String((*byte)(unsafe.Add(unsafe.Pointer(unsafe.StringData(g.patterns)), start)), len(input)) == input {
|
||||
return n
|
||||
}
|
||||
return 0
|
||||
@@ -160,12 +193,12 @@ func (g *MphMatcherGroup) Match(input string) []uint32 {
|
||||
hash = hash*PrimeRK + uint32(input[i])
|
||||
if input[i] == '.' {
|
||||
if mphIdx := g.Lookup(hash, input[i:]); mphIdx != 0 {
|
||||
matches = append(matches, g.values[mphIdx])
|
||||
matches = append(matches, g.valuesOf(mphIdx))
|
||||
}
|
||||
}
|
||||
}
|
||||
if mphIdx := g.Lookup(hash, input); mphIdx != 0 {
|
||||
matches = append(matches, g.values[mphIdx])
|
||||
matches = append(matches, g.valuesOf(mphIdx))
|
||||
}
|
||||
return CompositeMatchesReverse(matches)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package strmatcher_test
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
@@ -276,3 +278,63 @@ func TestEmptyMphMatcherGroup(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
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]...)
|
||||
}
|
||||
if m := g.Match(input); !slices.Equal(m, 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package strmatcher
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"regexp/syntax"
|
||||
"slices"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
@@ -73,7 +74,43 @@ func (m SubstrMatcher) Match(s string) bool {
|
||||
|
||||
// RegexMatcher is an implementation of Matcher.
|
||||
type RegexMatcher struct {
|
||||
pattern *regexp.Regexp
|
||||
pattern *regexp.Regexp
|
||||
literals []string // every match contains all of them, longest first
|
||||
}
|
||||
|
||||
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) })
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -89,6 +126,11 @@ func (m *RegexMatcher) String() string {
|
||||
}
|
||||
|
||||
func (m *RegexMatcher) Match(s string) bool {
|
||||
for _, l := range m.literals {
|
||||
if !strings.Contains(s, l) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return m.pattern.MatchString(s)
|
||||
}
|
||||
|
||||
@@ -102,11 +144,7 @@ func (t Type) New(pattern string) (Matcher, error) {
|
||||
case Domain:
|
||||
return DomainMatcher(pattern), nil
|
||||
case Regex: // 1. regex matching is case-sensitive
|
||||
regex, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &RegexMatcher{pattern: regex}, nil
|
||||
return newRegexMatcher(pattern)
|
||||
default:
|
||||
return nil, errors.New("unknown matcher type")
|
||||
}
|
||||
@@ -135,11 +173,7 @@ func (t Type) NewDomainPattern(pattern string) (Matcher, error) {
|
||||
}
|
||||
return DomainMatcher(pattern), nil
|
||||
case Regex: // Regex's charset not in LDH subset
|
||||
regex, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &RegexMatcher{pattern: regex}, nil
|
||||
return newRegexMatcher(pattern)
|
||||
default:
|
||||
return nil, errors.New("unknown matcher type")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package strmatcher
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
f.Fuzz(func(t *testing.T, pattern, s string) {
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
m, _ := newRegexMatcher(pattern)
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
+21
-25
@@ -1,7 +1,7 @@
|
||||
package log // import "github.com/xtls/xray-core/common/log"
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
)
|
||||
@@ -29,36 +29,32 @@ func (m *GeneralMessage) String() string {
|
||||
|
||||
// Record writes a message into log stream.
|
||||
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.
|
||||
func RegisterHandler(handler Handler) {
|
||||
if handler == nil {
|
||||
panic("Log handler is nil")
|
||||
}
|
||||
logHandler.Set(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
|
||||
logHandler.Store(&handler)
|
||||
}
|
||||
|
||||
@@ -68,6 +68,10 @@ func (l *serverityLogger) Handle(msg Message) {
|
||||
}
|
||||
}
|
||||
|
||||
func (l *serverityLogger) Severity() Severity {
|
||||
return l.logLevel
|
||||
}
|
||||
|
||||
func (l *generalLogger) run() {
|
||||
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 {
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ func (f *FrameMetadata) Unmarshal(reader io.Reader, readSourceAndLocal bool) err
|
||||
return err
|
||||
}
|
||||
if metaLen > 512 {
|
||||
return errors.New("invalid metalen ", metaLen).AtError()
|
||||
return errors.New("invalid metalen ", metaLen)
|
||||
}
|
||||
|
||||
b := buf.New()
|
||||
|
||||
@@ -351,7 +351,7 @@ func (w *ServerWorker) handleFrame(ctx context.Context, reader *buf.BufferedRead
|
||||
err = w.handleStatusKeep(&meta, reader)
|
||||
default:
|
||||
status := meta.SessionStatus
|
||||
return errors.New("unknown status: ", status).AtError()
|
||||
return errors.New("unknown status: ", status)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
func (u *User) GetTypedAccount() (Account, error) {
|
||||
if u.GetAccount() == nil {
|
||||
return nil, errors.New("Account is missing").AtWarning()
|
||||
return nil, errors.New("Account is missing")
|
||||
}
|
||||
|
||||
rawAccount, err := u.Account.GetInstance()
|
||||
|
||||
@@ -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 {
|
||||
configType := reflect.TypeOf(config)
|
||||
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
|
||||
return nil
|
||||
@@ -27,7 +27,7 @@ func CreateObject(ctx context.Context, config interface{}) (interface{}, error)
|
||||
configType := reflect.TypeOf(config)
|
||||
creator, found := typeCreatorRegistry[configType]
|
||||
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)
|
||||
}
|
||||
|
||||
+4
-4
@@ -125,7 +125,7 @@ func LoadConfig(formatName string, input interface{}) (*Config, error) {
|
||||
}
|
||||
|
||||
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" {
|
||||
@@ -142,7 +142,7 @@ func LoadConfig(formatName string, input interface{}) (*Config, error) {
|
||||
if len(v) == 1 {
|
||||
return configLoaderByName["protobuf"].Loader(v)
|
||||
} 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 {
|
||||
return f.Loader(v)
|
||||
} 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) {
|
||||
|
||||
@@ -13,7 +13,7 @@ type FakeDNSEngine interface {
|
||||
|
||||
var (
|
||||
FakeIPv4Pool = "198.18.0.0/15"
|
||||
FakeIPv6Pool = "fc00::/18"
|
||||
FakeIPv6Pool = "2001:2::/48"
|
||||
)
|
||||
|
||||
type FakeDNSEngineRev0 interface {
|
||||
|
||||
@@ -97,6 +97,9 @@ func New() *Client {
|
||||
r := &net.Resolver{
|
||||
PreferGo: true,
|
||||
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)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -18,8 +18,6 @@ require (
|
||||
github.com/pires/go-proxyproto v0.15.0
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sagernet/sing v0.5.1
|
||||
github.com/sagernet/sing-shadowsocks v0.2.7
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
github.com/xtls/reality v0.0.0-20260908062103-8cdf7bf9c7f0
|
||||
|
||||
@@ -70,10 +70,6 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
|
||||
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/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
|
||||
+2
-2
@@ -97,7 +97,7 @@ func (v *HTTPClientConfig) Build() (proto.Message, error) {
|
||||
user.Email = v.Email
|
||||
} else {
|
||||
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)
|
||||
@@ -106,7 +106,7 @@ func (v *HTTPClientConfig) Build() (proto.Message, error) {
|
||||
account.Password = v.Password
|
||||
} else {
|
||||
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())
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ func RegisterConfigureFilePostProcessingStage(name string, stage ConfigureFilePo
|
||||
func PostProcessConfigureFile(conf *Config) error {
|
||||
for k, v := range configureFilePostProcessingStages {
|
||||
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
|
||||
|
||||
@@ -13,7 +13,7 @@ type ConfigCreatorCache map[string]ConfigCreator
|
||||
|
||||
func (v ConfigCreatorCache) RegisterCreator(id string, creator ConfigCreator) error {
|
||||
if _, found := v[id]; found {
|
||||
return errors.New(id, " already registered.").AtError()
|
||||
return errors.New(id, " already registered.")
|
||||
}
|
||||
|
||||
v[id] = creator
|
||||
@@ -61,7 +61,7 @@ func (v *JSONConfigLoader) Load(raw []byte) (interface{}, string, error) {
|
||||
}
|
||||
rawID, found := obj[v.idKey]
|
||||
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
|
||||
if err := json.Unmarshal(rawID, &id); err != nil {
|
||||
|
||||
@@ -2,9 +2,11 @@ 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"
|
||||
)
|
||||
@@ -35,3 +37,67 @@ func (c *MasqueClientConfig) Build() (proto.Message, error) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ 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"
|
||||
)
|
||||
|
||||
@@ -37,6 +40,14 @@ func TestMasqueConfig(t *testing.T) {
|
||||
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{
|
||||
@@ -47,6 +58,8 @@ func TestMasqueConfig(t *testing.T) {
|
||||
`{"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)
|
||||
@@ -83,3 +96,93 @@ func TestMasqueOutboundConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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) {
|
||||
|
||||
+37
-56
@@ -3,8 +3,6 @@ package conf
|
||||
import (
|
||||
"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/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
@@ -55,7 +53,7 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
|
||||
v.Users = v.Clients
|
||||
}
|
||||
|
||||
if C.Contains(shadowaead_2022.List, v.Cipher) {
|
||||
if _, err := shadowsocks_2022.GetCipherMethod(v.Cipher); err == nil {
|
||||
return buildShadowsocks2022(v)
|
||||
}
|
||||
|
||||
@@ -111,12 +109,14 @@ func (v *ShadowsocksServerConfig) Build() (proto.Message, error) {
|
||||
}
|
||||
|
||||
func buildShadowsocks2022(v *ShadowsocksServerConfig) (proto.Message, error) {
|
||||
v.Cipher = strings.ToLower(v.Cipher)
|
||||
if len(v.Users) == 0 {
|
||||
config := new(shadowsocks_2022.ServerConfig)
|
||||
config.Method = v.Cipher
|
||||
config.Key = v.Password
|
||||
config.Network = v.NetworkList.Build()
|
||||
config.Email = v.Email
|
||||
config.Level = int32(v.Level)
|
||||
return config, nil
|
||||
}
|
||||
|
||||
@@ -171,6 +171,7 @@ func buildShadowsocks2022(v *ShadowsocksServerConfig) (proto.Message, error) {
|
||||
Email: user.Email,
|
||||
Address: user.Address.Build(),
|
||||
Port: uint32(user.Port),
|
||||
Level: int32(user.Level),
|
||||
})
|
||||
}
|
||||
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`)
|
||||
}
|
||||
|
||||
if len(v.Servers) == 1 {
|
||||
server := v.Servers[0]
|
||||
if C.Contains(shadowaead_2022.List, server.Cipher) {
|
||||
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.")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
server := v.Servers[0]
|
||||
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.")
|
||||
}
|
||||
|
||||
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)
|
||||
for _, server := range v.Servers {
|
||||
if C.Contains(shadowaead_2022.List, server.Cipher) {
|
||||
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 := &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
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
+2
-3
@@ -44,7 +44,6 @@ func (v *SocksServerConfig) Build() (proto.Message, error) {
|
||||
case AuthMethodUserPass:
|
||||
config.AuthType = socks.AuthType_PASSWORD
|
||||
default:
|
||||
// errors.New("unknown socks auth method: ", v.AuthMethod, ". Default to noauth.").AtWarning().WriteToLog()
|
||||
config.AuthType = socks.AuthType_NO_AUTH
|
||||
}
|
||||
|
||||
@@ -115,7 +114,7 @@ func (v *SocksClientConfig) Build() (proto.Message, error) {
|
||||
user.Email = v.Email
|
||||
} else {
|
||||
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)
|
||||
@@ -124,7 +123,7 @@ func (v *SocksClientConfig) Build() (proto.Message, error) {
|
||||
account.Password = v.Password
|
||||
} else {
|
||||
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())
|
||||
|
||||
@@ -942,12 +942,19 @@ func (c *UDPHop) Build() (proto.Message, error) {
|
||||
}
|
||||
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(c.Interval.From),
|
||||
IntervalMax: int64(c.Interval.To),
|
||||
IntervalMin: int64(interval.From),
|
||||
IntervalMax: int64(interval.To),
|
||||
RemoteIPs: remoteIPs,
|
||||
RemotePorts: c.RemotePorts.Build().Ports(),
|
||||
}, nil
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"maps"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"sort"
|
||||
@@ -124,7 +126,7 @@ func (v *AuthenticatorRequest) Build() (*http.RequestConfig, error) {
|
||||
for _, key := range headerNames {
|
||||
value := v.Headers[key]
|
||||
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{
|
||||
Name: key,
|
||||
@@ -192,7 +194,7 @@ func (v *AuthenticatorResponse) Build() (*http.ResponseConfig, error) {
|
||||
for _, key := range headerNames {
|
||||
value := v.Headers[key]
|
||||
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{
|
||||
Name: key,
|
||||
@@ -242,11 +244,11 @@ func (c *TCPConfig) Build() (proto.Message, error) {
|
||||
if len(c.HeaderConfig) > 0 {
|
||||
headerConfig, _, err := tcpHeaderLoader.Load(c.HeaderConfig)
|
||||
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()
|
||||
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)
|
||||
}
|
||||
@@ -791,6 +793,8 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -819,12 +823,27 @@ func (c *MasqueConfig) Build() (proto.Message, error) {
|
||||
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: c.Headers,
|
||||
Headers: headers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,12 @@ import (
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/proxy/tun"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
@@ -20,6 +24,8 @@ type TunConfig struct {
|
||||
UserLevel uint32 `json:"userLevel"`
|
||||
AutoSystemRoutingTable []string `json:"autoSystemRoutingTable"`
|
||||
AutoOutboundsInterface *string `json:"autoOutboundsInterface"`
|
||||
AutoSystemDnsToGateway bool `json:"autoSystemDnsToGateway"`
|
||||
AutoSystemWfpBlockLeak []string `json:"autoSystemWfpBlockLeak"`
|
||||
}
|
||||
|
||||
func (v *TunConfig) Build() (proto.Message, error) {
|
||||
@@ -31,6 +37,32 @@ func (v *TunConfig) Build() (proto.Message, error) {
|
||||
DNS: v.DNS,
|
||||
UserLevel: v.UserLevel,
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ var (
|
||||
"trojan": func() interface{} { return new(TrojanServerConfig) },
|
||||
"wireguard": func() interface{} { return &WireGuardConfig{IsClient: false} },
|
||||
"hysteria": func() interface{} { return new(HysteriaServerConfig) },
|
||||
"masque": func() interface{} { return new(MasqueServerConfig) },
|
||||
"tun": func() interface{} { return new(TunConfig) },
|
||||
}, "protocol", "settings")
|
||||
|
||||
@@ -205,6 +206,9 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
|
||||
if err != nil {
|
||||
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{
|
||||
Tag: c.Tag,
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/infra/conf"
|
||||
"github.com/xtls/xray-core/infra/conf/serial"
|
||||
"github.com/xtls/xray-core/proxy/masque"
|
||||
"github.com/xtls/xray-core/proxy/shadowsocks"
|
||||
"github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||||
"github.com/xtls/xray-core/proxy/trojan"
|
||||
@@ -88,6 +89,8 @@ func extractInboundUsers(inb *core.InboundHandlerConfig) []*protocol.User {
|
||||
return ty.Users
|
||||
case *shadowsocks_2022.MultiUserServerConfig:
|
||||
return ty.Users
|
||||
case *masque.ServerConfig:
|
||||
return ty.Users
|
||||
default:
|
||||
fmt.Println("unsupported inbound type")
|
||||
}
|
||||
|
||||
@@ -115,11 +115,7 @@ Start:
|
||||
|
||||
request, err := http.ReadRequest(reader)
|
||||
if err != nil {
|
||||
trace := errors.New("failed to read http request").Base(err)
|
||||
if errors.Cause(err) != io.EOF && !isTimeout(errors.Cause(err)) {
|
||||
trace.AtWarning()
|
||||
}
|
||||
return trace
|
||||
return errors.New("failed to read http request").Base(err)
|
||||
}
|
||||
|
||||
if len(s.config.Accounts) > 0 {
|
||||
@@ -147,7 +143,7 @@ Start:
|
||||
}
|
||||
dest, err := http_proto.ParseHost(host, defaultPort)
|
||||
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{
|
||||
From: conn.RemoteAddr(),
|
||||
@@ -262,7 +258,7 @@ func (s *Server) handlePlainHTTP(ctx context.Context, request *http.Request, wri
|
||||
requestWriter := buf.NewBufferedWriter(link.Writer)
|
||||
common.Must(requestWriter.SetBuffered(false))
|
||||
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
|
||||
}
|
||||
@@ -299,7 +295,7 @@ func (s *Server) handlePlainHTTP(ctx context.Context, request *http.Request, wri
|
||||
response.Header.Set("Proxy-Connection", "close")
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
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()
|
||||
errors.LogInfo(ctx, "tunneling request to ", target, " via ", target.Network, ":", c.server.Destination.NetAddr())
|
||||
|
||||
@@ -40,11 +40,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
|
||||
for _, user := range config.Users {
|
||||
u, err := user.ToMemoryUser()
|
||||
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 {
|
||||
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() {
|
||||
request, err := proxyman.BuildSniffingRequest(config.Sniffing)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
+125
-11
@@ -74,15 +74,125 @@ func (x *ClientConfig) GetRemoteDns() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Account) Reset() {
|
||||
*x = Account{}
|
||||
mi := &file_proxy_masque_config_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Account) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Account) ProtoMessage() {}
|
||||
|
||||
func (x *Account) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proxy_masque_config_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Account.ProtoReflect.Descriptor instead.
|
||||
func (*Account) Descriptor() ([]byte, []int) {
|
||||
return file_proxy_masque_config_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *Account) GetPassword() string {
|
||||
if x != nil {
|
||||
return x.Password
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Users []*protocol.User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"`
|
||||
Address []string `protobuf:"bytes,2,rep,name=address,proto3" json:"address,omitempty"`
|
||||
Mtu uint32 `protobuf:"varint,3,opt,name=mtu,proto3" json:"mtu,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ServerConfig) Reset() {
|
||||
*x = ServerConfig{}
|
||||
mi := &file_proxy_masque_config_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ServerConfig) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ServerConfig) ProtoMessage() {}
|
||||
|
||||
func (x *ServerConfig) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proxy_masque_config_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ServerConfig.ProtoReflect.Descriptor instead.
|
||||
func (*ServerConfig) Descriptor() ([]byte, []int) {
|
||||
return file_proxy_masque_config_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *ServerConfig) GetUsers() []*protocol.User {
|
||||
if x != nil {
|
||||
return x.Users
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ServerConfig) GetAddress() []string {
|
||||
if x != nil {
|
||||
return x.Address
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ServerConfig) GetMtu() uint32 {
|
||||
if x != nil {
|
||||
return x.Mtu
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var File_proxy_masque_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proxy_masque_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x19proxy/masque/config.proto\x12\x11xray.proxy.masque\x1a!common/protocol/server_spec.proto\"k\n" +
|
||||
"\x19proxy/masque/config.proto\x12\x11xray.proxy.masque\x1a!common/protocol/server_spec.proto\x1a\x1acommon/protocol/user.proto\"k\n" +
|
||||
"\fClientConfig\x12<\n" +
|
||||
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\x12\x1d\n" +
|
||||
"\n" +
|
||||
"remote_dns\x18\x02 \x03(\tR\tremoteDnsBU\n" +
|
||||
"remote_dns\x18\x02 \x03(\tR\tremoteDns\"%\n" +
|
||||
"\aAccount\x12\x1a\n" +
|
||||
"\bpassword\x18\x01 \x01(\tR\bpassword\"l\n" +
|
||||
"\fServerConfig\x120\n" +
|
||||
"\x05users\x18\x01 \x03(\v2\x1a.xray.common.protocol.UserR\x05users\x12\x18\n" +
|
||||
"\aaddress\x18\x02 \x03(\tR\aaddress\x12\x10\n" +
|
||||
"\x03mtu\x18\x03 \x01(\rR\x03mtuBU\n" +
|
||||
"\x15com.xray.proxy.masqueP\x01Z&github.com/xtls/xray-core/proxy/masque\xaa\x02\x11Xray.Proxy.Masqueb\x06proto3"
|
||||
|
||||
var (
|
||||
@@ -97,18 +207,22 @@ func file_proxy_masque_config_proto_rawDescGZIP() []byte {
|
||||
return file_proxy_masque_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_proxy_masque_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_proxy_masque_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
||||
var file_proxy_masque_config_proto_goTypes = []any{
|
||||
(*ClientConfig)(nil), // 0: xray.proxy.masque.ClientConfig
|
||||
(*protocol.ServerEndpoint)(nil), // 1: xray.common.protocol.ServerEndpoint
|
||||
(*Account)(nil), // 1: xray.proxy.masque.Account
|
||||
(*ServerConfig)(nil), // 2: xray.proxy.masque.ServerConfig
|
||||
(*protocol.ServerEndpoint)(nil), // 3: xray.common.protocol.ServerEndpoint
|
||||
(*protocol.User)(nil), // 4: xray.common.protocol.User
|
||||
}
|
||||
var file_proxy_masque_config_proto_depIdxs = []int32{
|
||||
1, // 0: xray.proxy.masque.ClientConfig.server:type_name -> xray.common.protocol.ServerEndpoint
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
3, // 0: xray.proxy.masque.ClientConfig.server:type_name -> xray.common.protocol.ServerEndpoint
|
||||
4, // 1: xray.proxy.masque.ServerConfig.users:type_name -> xray.common.protocol.User
|
||||
2, // [2:2] is the sub-list for method output_type
|
||||
2, // [2:2] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_proxy_masque_config_proto_init() }
|
||||
@@ -122,7 +236,7 @@ func file_proxy_masque_config_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_masque_config_proto_rawDesc), len(file_proxy_masque_config_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumMessages: 3,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@@ -7,8 +7,19 @@ option java_package = "com.xray.proxy.masque";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common/protocol/server_spec.proto";
|
||||
import "common/protocol/user.proto";
|
||||
|
||||
message ClientConfig {
|
||||
xray.common.protocol.ServerEndpoint server = 1;
|
||||
repeated string remote_dns = 2;
|
||||
}
|
||||
|
||||
message Account {
|
||||
string password = 1;
|
||||
}
|
||||
|
||||
message ServerConfig {
|
||||
repeated xray.common.protocol.User users = 1;
|
||||
repeated string address = 2;
|
||||
uint32 mtu = 3;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"sync"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
type addressPool struct {
|
||||
mu sync.Mutex
|
||||
prefix netip.Prefix
|
||||
server netip.Addr
|
||||
first netip.Addr
|
||||
last netip.Addr
|
||||
next netip.Addr
|
||||
used map[netip.Addr]struct{}
|
||||
}
|
||||
|
||||
func newAddressPool(address netip.Prefix) (*addressPool, error) {
|
||||
server := address.Addr()
|
||||
if server.Is4In6() || server.Zone() != "" {
|
||||
return nil, errors.New("invalid address ", address)
|
||||
}
|
||||
prefix := address.Masked()
|
||||
last := lastAddr(prefix)
|
||||
if server == prefix.Addr() || server.Is4() && server == last {
|
||||
return nil, errors.New("address ", address, " is not a host address")
|
||||
}
|
||||
if server.Is4() {
|
||||
last = last.Prev()
|
||||
}
|
||||
first := prefix.Addr().Next()
|
||||
if first == last {
|
||||
return nil, errors.New("address ", address, " leaves no addresses to assign")
|
||||
}
|
||||
return &addressPool{
|
||||
prefix: prefix,
|
||||
server: server,
|
||||
first: first,
|
||||
last: last,
|
||||
next: first,
|
||||
used: make(map[netip.Addr]struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func lastAddr(prefix netip.Prefix) netip.Addr {
|
||||
b := prefix.Addr().AsSlice()
|
||||
for i := prefix.Bits(); i < len(b)*8; i++ {
|
||||
b[i/8] |= 1 << (7 - i%8)
|
||||
}
|
||||
addr, _ := netip.AddrFromSlice(b)
|
||||
return addr
|
||||
}
|
||||
|
||||
func (p *addressPool) allocate() (netip.Addr, bool) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
for addr := p.next; ; {
|
||||
next := addr.Next()
|
||||
if addr == p.last {
|
||||
next = p.first
|
||||
}
|
||||
if _, found := p.used[addr]; !found && addr != p.server {
|
||||
p.used[addr] = struct{}{}
|
||||
p.next = next
|
||||
return addr, true
|
||||
}
|
||||
if next == p.next {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
addr = next
|
||||
}
|
||||
}
|
||||
|
||||
func (p *addressPool) release(addr netip.Addr) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
delete(p.used, addr)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func allocateAll(p *addressPool) []netip.Addr {
|
||||
var addrs []netip.Addr
|
||||
for {
|
||||
addr, ok := p.allocate()
|
||||
if !ok {
|
||||
return addrs
|
||||
}
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressPool(t *testing.T) {
|
||||
p, err := newAddressPool(netip.MustParsePrefix("10.0.0.1/29"))
|
||||
require.NoError(t, err)
|
||||
var want []netip.Addr
|
||||
for _, s := range []string{"10.0.0.2", "10.0.0.3", "10.0.0.4", "10.0.0.5", "10.0.0.6"} {
|
||||
want = append(want, netip.MustParseAddr(s))
|
||||
}
|
||||
require.Equal(t, want, allocateAll(p))
|
||||
|
||||
p.release(netip.MustParseAddr("10.0.0.4"))
|
||||
addr, ok := p.allocate()
|
||||
require.True(t, ok)
|
||||
require.Equal(t, netip.MustParseAddr("10.0.0.4"), addr)
|
||||
_, ok = p.allocate()
|
||||
require.False(t, ok)
|
||||
|
||||
p, err = newAddressPool(netip.MustParsePrefix("fd00::1/126"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("fd00::2"), netip.MustParseAddr("fd00::3")}, allocateAll(p))
|
||||
|
||||
p, err = newAddressPool(netip.MustParsePrefix("10.0.0.2/30"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("10.0.0.1")}, allocateAll(p))
|
||||
}
|
||||
|
||||
func TestAddressPoolRejects(t *testing.T) {
|
||||
for _, s := range []string{
|
||||
"10.0.0.0/24",
|
||||
"10.0.0.255/24",
|
||||
"10.0.0.1/31",
|
||||
"10.0.0.1/32",
|
||||
"fd00::1/127",
|
||||
"fd00::1/128",
|
||||
"::ffff:10.0.0.1/120",
|
||||
} {
|
||||
_, err := newAddressPool(netip.MustParsePrefix(s))
|
||||
require.Error(t, err, s)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,550 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"context"
|
||||
go_errors "errors"
|
||||
"io"
|
||||
stdnet "net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
c "github.com/xtls/xray-core/common/ctx"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/log"
|
||||
"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/core"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
"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/masque/connectip"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
)
|
||||
|
||||
const (
|
||||
authenticateHeader = `Basic realm="masque", charset="UTF-8"`
|
||||
tunnelQueueSize = 512
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
validator *validator
|
||||
dispatcher routing.Dispatcher
|
||||
ctx context.Context
|
||||
tag string
|
||||
sniffing session.SniffingRequest
|
||||
mtu int
|
||||
|
||||
dev tun.Device
|
||||
pools []*addressPool
|
||||
local []netip.Addr
|
||||
|
||||
mu sync.RWMutex
|
||||
tunnels map[netip.Addr]*serverTunnel
|
||||
closed bool
|
||||
started bool
|
||||
}
|
||||
|
||||
type serverTunnel struct {
|
||||
conn stat.Connection
|
||||
ipConn *connectip.Conn
|
||||
user *protocol.MemoryUser
|
||||
addrs []netip.Addr
|
||||
queue chan *buf.Buffer
|
||||
done chan struct{}
|
||||
|
||||
mu sync.Mutex
|
||||
conns map[net.Conn]struct{}
|
||||
}
|
||||
|
||||
func newServerTunnel(conn stat.Connection, user *protocol.MemoryUser) *serverTunnel {
|
||||
return &serverTunnel{
|
||||
conn: conn,
|
||||
user: user,
|
||||
queue: make(chan *buf.Buffer, tunnelQueueSize),
|
||||
done: make(chan struct{}),
|
||||
conns: make(map[net.Conn]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *serverTunnel) send(b *buf.Buffer) bool {
|
||||
select {
|
||||
case <-t.done:
|
||||
return false
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case t.queue <- b:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (t *serverTunnel) track(conn net.Conn) bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.conns == nil {
|
||||
return false
|
||||
}
|
||||
t.conns[conn] = struct{}{}
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *serverTunnel) untrack(conn net.Conn) {
|
||||
t.mu.Lock()
|
||||
delete(t.conns, conn)
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
func (t *serverTunnel) close() {
|
||||
t.mu.Lock()
|
||||
conns := t.conns
|
||||
if conns != nil {
|
||||
t.conns = nil
|
||||
close(t.done)
|
||||
}
|
||||
t.mu.Unlock()
|
||||
for conn := range conns {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
|
||||
v := core.MustFromContext(ctx)
|
||||
|
||||
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"`)
|
||||
}
|
||||
|
||||
users := newValidator()
|
||||
for _, user := range config.Users {
|
||||
u, err := user.ToMemoryUser()
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to get MASQUE user").Base(err)
|
||||
}
|
||||
if err := users.add(u); err != nil {
|
||||
return nil, errors.New("failed to add user").Base(err)
|
||||
}
|
||||
}
|
||||
|
||||
var pools []*addressPool
|
||||
var local []netip.Addr
|
||||
for _, s := range config.Address {
|
||||
prefix, err := netip.ParsePrefix(s)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid address ", s).Base(err)
|
||||
}
|
||||
if slices.ContainsFunc(local, func(addr netip.Addr) bool { return addr.Is4() == prefix.Addr().Is4() }) {
|
||||
return nil, errors.New("only one address per IP family is supported")
|
||||
}
|
||||
pool, err := newAddressPool(prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pools = append(pools, pool)
|
||||
local = append(local, prefix.Addr())
|
||||
}
|
||||
if len(pools) == 0 {
|
||||
return nil, errors.New("no address to assign")
|
||||
}
|
||||
|
||||
mtu := int(config.Mtu)
|
||||
if mtu == 0 {
|
||||
mtu = masque.MinPacketSize
|
||||
}
|
||||
dev, _, gstack, err := wireguard.CreateNetTUN(local, nil, mtu, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
validator: users,
|
||||
dispatcher: v.GetFeature(routing.DispatcherType()).(routing.Dispatcher),
|
||||
ctx: core.ToBackgroundDetachedContext(ctx),
|
||||
mtu: mtu,
|
||||
dev: dev,
|
||||
pools: pools,
|
||||
local: local,
|
||||
tunnels: make(map[netip.Addr]*serverTunnel),
|
||||
}
|
||||
if inbound := session.InboundFromContext(ctx); inbound != nil {
|
||||
s.tag = inbound.Tag
|
||||
}
|
||||
if content := session.ContentFromContext(ctx); content != nil {
|
||||
s.sniffing = content.SniffingRequest
|
||||
}
|
||||
wireguard.CreateForwarder(gstack, s.handleConnection)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.started || s.closed {
|
||||
return nil
|
||||
}
|
||||
s.started = true
|
||||
go s.readFromStack()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Close() error {
|
||||
s.mu.Lock()
|
||||
if s.closed {
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
s.closed = true
|
||||
var tunnels []*serverTunnel
|
||||
for _, t := range s.tunnels {
|
||||
if !slices.Contains(tunnels, t) {
|
||||
tunnels = append(tunnels, t)
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
for _, t := range tunnels {
|
||||
t.conn.Close()
|
||||
}
|
||||
return s.dev.Close()
|
||||
}
|
||||
|
||||
func (s *Server) AddUser(ctx context.Context, user *protocol.MemoryUser) error {
|
||||
return s.validator.add(user)
|
||||
}
|
||||
|
||||
func (s *Server) RemoveUser(ctx context.Context, email string) error {
|
||||
user, err := s.validator.delByEmail(email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.RLock()
|
||||
var conns []stat.Connection
|
||||
for _, t := range s.tunnels {
|
||||
if t.user == user && !slices.Contains(conns, t.conn) {
|
||||
conns = append(conns, t.conn)
|
||||
}
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
for _, conn := range conns {
|
||||
conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) GetUser(ctx context.Context, email string) *protocol.MemoryUser {
|
||||
return s.validator.getByEmail(email)
|
||||
}
|
||||
|
||||
func (s *Server) GetUsers(ctx context.Context) []*protocol.MemoryUser {
|
||||
return s.validator.getAll()
|
||||
}
|
||||
|
||||
func (s *Server) GetUsersCount(context.Context) int64 {
|
||||
return s.validator.count()
|
||||
}
|
||||
|
||||
func (s *Server) Network() []net.Network {
|
||||
return []net.Network{net.Network_TCP}
|
||||
}
|
||||
|
||||
func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error {
|
||||
sconn, ok := stat.TryUnwrapStatsConn(conn).(*masque.ServerConn)
|
||||
if !ok {
|
||||
return errors.New("not a MASQUE connection")
|
||||
}
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
inbound.Name = "masque"
|
||||
inbound.CanSpliceCopy = 3
|
||||
|
||||
name, pass, _ := sconn.Request().BasicAuth()
|
||||
user := s.validator.get(name, pass)
|
||||
if user == nil {
|
||||
sconn.Reject(http.StatusUnauthorized, http.Header{"WWW-Authenticate": {authenticateHeader}})
|
||||
log.Record(&log.AccessMessage{
|
||||
From: conn.RemoteAddr(),
|
||||
To: "",
|
||||
Status: log.AccessRejected,
|
||||
Reason: errors.New("invalid credentials"),
|
||||
})
|
||||
return errors.New("MASQUE: authentication failed for ", name)
|
||||
}
|
||||
inbound.User = user
|
||||
|
||||
t := newServerTunnel(conn, user)
|
||||
for _, pool := range s.pools {
|
||||
if addr, ok := pool.allocate(); ok {
|
||||
t.addrs = append(t.addrs, addr)
|
||||
}
|
||||
}
|
||||
defer s.release(t)
|
||||
if len(t.addrs) == 0 {
|
||||
sconn.Reject(http.StatusServiceUnavailable, nil)
|
||||
return errors.New("MASQUE: no address left to assign")
|
||||
}
|
||||
|
||||
ipConn, err := sconn.Accept()
|
||||
if err != nil {
|
||||
return errors.New("MASQUE: failed to accept the tunnel").Base(err)
|
||||
}
|
||||
t.ipConn = ipConn
|
||||
if !s.register(t) {
|
||||
return errors.New("MASQUE: server closed")
|
||||
}
|
||||
if !s.validator.contains(user) {
|
||||
return errors.New("MASQUE: user ", name, " was removed")
|
||||
}
|
||||
go s.writeToTunnel(t)
|
||||
|
||||
prefixes := make([]netip.Prefix, len(t.addrs))
|
||||
for i, addr := range t.addrs {
|
||||
prefixes[i] = netip.PrefixFrom(addr, addr.BitLen())
|
||||
}
|
||||
if err := ipConn.AssignAddresses(prefixes); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ipConn.AdvertiseRoute(fullRoutes(t.addrs)); err != nil {
|
||||
return err
|
||||
}
|
||||
go serveAddressRequests(t)
|
||||
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: conn.RemoteAddr(),
|
||||
To: "",
|
||||
Status: log.AccessAccepted,
|
||||
Email: user.Email,
|
||||
})
|
||||
errors.LogInfo(ctx, "MASQUE: tunnel from ", inbound.Source, " assigned ", t.addrs)
|
||||
return s.readFromTunnel(t)
|
||||
}
|
||||
|
||||
func fullRoutes(addrs []netip.Addr) []connectip.IPRoute {
|
||||
var routes []connectip.IPRoute
|
||||
if slices.ContainsFunc(addrs, netip.Addr.Is4) {
|
||||
routes = append(routes, connectip.IPRoute{StartIP: netip.IPv4Unspecified(), EndIP: netip.AddrFrom4([4]byte{255, 255, 255, 255})})
|
||||
}
|
||||
if slices.ContainsFunc(addrs, netip.Addr.Is6) {
|
||||
routes = append(routes, connectip.IPRoute{StartIP: netip.IPv6Unspecified(), EndIP: netip.AddrFrom16([16]byte{0: 0xff, 1: 0xff, 2: 0xff, 3: 0xff, 4: 0xff, 5: 0xff, 6: 0xff, 7: 0xff, 8: 0xff, 9: 0xff, 10: 0xff, 11: 0xff, 12: 0xff, 13: 0xff, 14: 0xff, 15: 0xff})})
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
func serveAddressRequests(t *serverTunnel) {
|
||||
for {
|
||||
req, err := t.ipConn.ReceiveAddressRequest(context.Background())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
assigned := make([]netip.Prefix, len(req.Prefixes))
|
||||
used := make(map[netip.Addr]bool)
|
||||
for i, requested := range req.Prefixes {
|
||||
for _, addr := range t.addrs {
|
||||
if addr.Is4() == requested.Addr().Is4() && !used[addr] {
|
||||
used[addr] = true
|
||||
assigned[i] = netip.PrefixFrom(addr, addr.BitLen())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
var additional []netip.Prefix
|
||||
for _, addr := range t.addrs {
|
||||
if !used[addr] {
|
||||
additional = append(additional, netip.PrefixFrom(addr, addr.BitLen()))
|
||||
}
|
||||
}
|
||||
if err := req.Respond(assigned, additional); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) register(t *serverTunnel) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.closed {
|
||||
return false
|
||||
}
|
||||
for _, addr := range t.addrs {
|
||||
s.tunnels[addr] = t
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) release(t *serverTunnel) {
|
||||
s.mu.Lock()
|
||||
for _, addr := range t.addrs {
|
||||
if s.tunnels[addr] == t {
|
||||
delete(s.tunnels, addr)
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
t.close()
|
||||
for _, addr := range t.addrs {
|
||||
for _, pool := range s.pools {
|
||||
if pool.prefix.Contains(addr) {
|
||||
pool.release(addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) lookup(addr netip.Addr) *serverTunnel {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.tunnels[addr]
|
||||
}
|
||||
|
||||
func (s *Server) inPool(addr netip.Addr) bool {
|
||||
return slices.ContainsFunc(s.pools, func(pool *addressPool) bool { return pool.prefix.Contains(addr) })
|
||||
}
|
||||
|
||||
func (s *Server) readFromTunnel(t *serverTunnel) error {
|
||||
b := make([]byte, 1<<16)
|
||||
for {
|
||||
n, err := t.conn.Read(b)
|
||||
if err != nil {
|
||||
if go_errors.Is(err, io.ErrShortBuffer) {
|
||||
continue
|
||||
}
|
||||
if go_errors.Is(err, stdnet.ErrClosed) || go_errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
dst, ok := packetDestination(b[:n])
|
||||
if !ok || dst.IsLinkLocalUnicast() || dst.IsMulticast() {
|
||||
continue
|
||||
}
|
||||
if other := s.lookup(dst); other != nil {
|
||||
if other != t {
|
||||
packet := buf.NewWithSize(int32(n))
|
||||
packet.Write(b[:n])
|
||||
if !other.send(packet) {
|
||||
packet.Release()
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if s.inPool(dst) && !slices.Contains(s.local, dst) {
|
||||
continue
|
||||
}
|
||||
s.dev.Write([][]byte{b[:n]}, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) readFromStack() {
|
||||
sizes := []int{0}
|
||||
var b *buf.Buffer
|
||||
for {
|
||||
if b == nil {
|
||||
b = buf.NewWithSize(int32(s.mtu))
|
||||
}
|
||||
b.Clear()
|
||||
if _, err := s.dev.Read([][]byte{b.Extend(int32(s.mtu))}, sizes, 0); err != nil {
|
||||
b.Release()
|
||||
return
|
||||
}
|
||||
b.Resize(0, int32(sizes[0]))
|
||||
dst, ok := packetDestination(b.Bytes())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if t := s.lookup(dst); t != nil && t.send(b) {
|
||||
b = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) writeToTunnel(t *serverTunnel) {
|
||||
for {
|
||||
select {
|
||||
case b := <-t.queue:
|
||||
_, err := t.conn.Write(b.Bytes())
|
||||
b.Release()
|
||||
if ptb, ok := go_errors.AsType[*masque.PacketTooBigError](err); ok {
|
||||
s.dev.Write([][]byte{ptb.ICMP}, 0)
|
||||
}
|
||||
case <-t.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func packetDestination(packet []byte) (netip.Addr, bool) {
|
||||
if len(packet) == 0 {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
switch packet[0] >> 4 {
|
||||
case 4:
|
||||
if len(packet) >= 20 {
|
||||
return netip.AddrFrom4([4]byte(packet[16:20])), true
|
||||
}
|
||||
case 6:
|
||||
if len(packet) >= 40 {
|
||||
return netip.AddrFrom16([16]byte(packet[24:40])), true
|
||||
}
|
||||
}
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
func (s *Server) handleConnection(conn net.Conn, dest net.Destination) {
|
||||
defer conn.Close()
|
||||
source := net.DestinationFromAddr(conn.RemoteAddr())
|
||||
addr, _ := netip.AddrFromSlice(source.Address.IP())
|
||||
t := s.lookup(addr.Unmap())
|
||||
if t == nil || !t.track(conn) {
|
||||
errors.LogInfo(s.ctx, "MASQUE: no tunnel for ", source, " to ", dest)
|
||||
return
|
||||
}
|
||||
defer t.untrack(conn)
|
||||
|
||||
ctx, cancel := context.WithCancel(s.ctx)
|
||||
defer cancel()
|
||||
ctx = c.ContextWithID(ctx, session.NewID())
|
||||
inbound := session.Inbound{
|
||||
Name: "masque",
|
||||
Tag: s.tag,
|
||||
CanSpliceCopy: 3,
|
||||
Source: source,
|
||||
User: t.user,
|
||||
}
|
||||
ctx = session.ContextWithInbound(ctx, &inbound)
|
||||
ctx = session.ContextWithContent(ctx, &session.Content{
|
||||
SniffingRequest: s.sniffing,
|
||||
})
|
||||
ctx = session.SubContextFromMuxInbound(ctx)
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: source,
|
||||
To: dest,
|
||||
Status: log.AccessAccepted,
|
||||
Email: t.user.Email,
|
||||
})
|
||||
errors.LogInfo(ctx, "processing from ", source, " to ", dest)
|
||||
|
||||
link := &transport.Link{
|
||||
Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)},
|
||||
Writer: buf.NewWriter(conn),
|
||||
}
|
||||
if err := s.dispatcher.DispatchLink(ctx, dest, link); err != nil {
|
||||
errors.LogError(ctx, errors.New("connection closed").Base(err))
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
|
||||
return NewServer(ctx, config.(*ServerConfig))
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
package masque
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/netip"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
)
|
||||
|
||||
type fakeTunnelConn struct {
|
||||
mu sync.Mutex
|
||||
reads chan []byte
|
||||
written [][]byte
|
||||
closed bool
|
||||
stall chan struct{}
|
||||
}
|
||||
|
||||
func newFakeTunnelConn() *fakeTunnelConn {
|
||||
return &fakeTunnelConn{reads: make(chan []byte, 16)}
|
||||
}
|
||||
|
||||
func (c *fakeTunnelConn) Read(b []byte) (int, error) {
|
||||
p, ok := <-c.reads
|
||||
if !ok {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return copy(b, p), nil
|
||||
}
|
||||
|
||||
func (c *fakeTunnelConn) Write(b []byte) (int, error) {
|
||||
if c.stall != nil {
|
||||
<-c.stall
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.written = append(c.written, bytes.Clone(b))
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (c *fakeTunnelConn) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if !c.closed {
|
||||
c.closed = true
|
||||
close(c.reads)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *fakeTunnelConn) packets() [][]byte {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.written
|
||||
}
|
||||
|
||||
func (c *fakeTunnelConn) isClosed() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.closed
|
||||
}
|
||||
|
||||
func (c *fakeTunnelConn) LocalAddr() net.Addr { return &net.TCPAddr{} }
|
||||
func (c *fakeTunnelConn) RemoteAddr() net.Addr { return &net.TCPAddr{} }
|
||||
func (c *fakeTunnelConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (c *fakeTunnelConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (c *fakeTunnelConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
type fakeDevice struct {
|
||||
mu sync.Mutex
|
||||
reads chan []byte
|
||||
written [][]byte
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (d *fakeDevice) File() *os.File { return nil }
|
||||
func (d *fakeDevice) MTU() (int, error) { return 1280, nil }
|
||||
func (d *fakeDevice) Name() (string, error) { return "fake", nil }
|
||||
func (d *fakeDevice) Events() <-chan tun.Event { return nil }
|
||||
func (d *fakeDevice) BatchSize() int { return 1 }
|
||||
|
||||
func (d *fakeDevice) Read(bufs [][]byte, sizes []int, offset int) (int, error) {
|
||||
p, ok := <-d.reads
|
||||
if !ok {
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
sizes[0] = copy(bufs[0][offset:], p)
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (d *fakeDevice) Write(bufs [][]byte, offset int) (int, error) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
for _, b := range bufs {
|
||||
d.written = append(d.written, bytes.Clone(b[offset:]))
|
||||
}
|
||||
return len(bufs), nil
|
||||
}
|
||||
|
||||
func (d *fakeDevice) Close() error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if !d.closed {
|
||||
d.closed = true
|
||||
close(d.reads)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *fakeDevice) packets() [][]byte {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
return d.written
|
||||
}
|
||||
|
||||
func ipPacket(src, dst string) []byte {
|
||||
s, d := netip.MustParseAddr(src), netip.MustParseAddr(dst)
|
||||
if s.Is4() {
|
||||
b := make([]byte, 20)
|
||||
b[0] = 0x45
|
||||
b[8] = 64
|
||||
copy(b[12:16], s.AsSlice())
|
||||
copy(b[16:20], d.AsSlice())
|
||||
return b
|
||||
}
|
||||
b := make([]byte, 40)
|
||||
b[0] = 0x60
|
||||
b[7] = 64
|
||||
copy(b[8:24], s.AsSlice())
|
||||
copy(b[24:40], d.AsSlice())
|
||||
return b
|
||||
}
|
||||
|
||||
func newTestServer(t *testing.T) (*Server, *fakeDevice) {
|
||||
t.Helper()
|
||||
pool4, err := newAddressPool(netip.MustParsePrefix("10.14.0.1/24"))
|
||||
require.NoError(t, err)
|
||||
pool6, err := newAddressPool(netip.MustParsePrefix("fd14::1/64"))
|
||||
require.NoError(t, err)
|
||||
dev := &fakeDevice{reads: make(chan []byte, tunnelQueueSize*2)}
|
||||
s := &Server{
|
||||
mtu: 1280,
|
||||
dev: dev,
|
||||
pools: []*addressPool{pool4, pool6},
|
||||
local: []netip.Addr{netip.MustParseAddr("10.14.0.1"), netip.MustParseAddr("fd14::1")},
|
||||
tunnels: make(map[netip.Addr]*serverTunnel),
|
||||
}
|
||||
return s, dev
|
||||
}
|
||||
|
||||
func addTunnel(t *testing.T, s *Server) (*serverTunnel, *fakeTunnelConn) {
|
||||
t.Helper()
|
||||
return addUserTunnel(t, s, &protocol.MemoryUser{})
|
||||
}
|
||||
|
||||
func addUserTunnel(t *testing.T, s *Server, user *protocol.MemoryUser) (*serverTunnel, *fakeTunnelConn) {
|
||||
t.Helper()
|
||||
conn := newFakeTunnelConn()
|
||||
tunnel := newServerTunnel(conn, user)
|
||||
for _, pool := range s.pools {
|
||||
addr, ok := pool.allocate()
|
||||
require.True(t, ok)
|
||||
tunnel.addrs = append(tunnel.addrs, addr)
|
||||
}
|
||||
require.True(t, s.register(tunnel))
|
||||
go s.writeToTunnel(tunnel)
|
||||
t.Cleanup(tunnel.close)
|
||||
return tunnel, conn
|
||||
}
|
||||
|
||||
func TestServerRoutesTunnelPackets(t *testing.T) {
|
||||
s, dev := newTestServer(t)
|
||||
a, aConn := addTunnel(t, s)
|
||||
b, bConn := addTunnel(t, s)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("10.14.0.2"), netip.MustParseAddr("fd14::2")}, a.addrs)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("10.14.0.3"), netip.MustParseAddr("fd14::3")}, b.addrs)
|
||||
|
||||
toB := ipPacket("10.14.0.2", "10.14.0.3")
|
||||
toB6 := ipPacket("fd14::2", "fd14::3")
|
||||
toServer := ipPacket("10.14.0.2", "10.14.0.1")
|
||||
toInternet := ipPacket("fd14::2", "2001:db8::1")
|
||||
for _, p := range [][]byte{
|
||||
toB,
|
||||
toB6,
|
||||
ipPacket("10.14.0.2", "10.14.0.9"),
|
||||
ipPacket("fd14::2", "fd14::99"),
|
||||
ipPacket("fd14::2", "fe80::1"),
|
||||
ipPacket("fd14::2", "ff02::1"),
|
||||
ipPacket("10.14.0.2", "224.0.0.251"),
|
||||
ipPacket("10.14.0.2", "10.14.0.2"),
|
||||
toServer,
|
||||
toInternet,
|
||||
} {
|
||||
aConn.reads <- p
|
||||
}
|
||||
aConn.Close()
|
||||
require.NoError(t, s.readFromTunnel(a))
|
||||
|
||||
require.Eventually(t, func() bool { return len(bConn.packets()) == 2 }, time.Second, time.Millisecond)
|
||||
require.Equal(t, [][]byte{toB, toB6}, bConn.packets())
|
||||
require.Equal(t, [][]byte{toServer, toInternet}, dev.packets())
|
||||
require.Empty(t, aConn.packets())
|
||||
}
|
||||
|
||||
func TestServerRoutesStackPackets(t *testing.T) {
|
||||
s, dev := newTestServer(t)
|
||||
_, aConn := addTunnel(t, s)
|
||||
_, bConn := addTunnel(t, s)
|
||||
require.NoError(t, s.Start())
|
||||
|
||||
toA := ipPacket("192.0.2.1", "10.14.0.2")
|
||||
toB := ipPacket("2001:db8::1", "fd14::3")
|
||||
dev.reads <- toA
|
||||
dev.reads <- ipPacket("192.0.2.1", "10.14.0.9")
|
||||
dev.reads <- toB
|
||||
require.Eventually(t, func() bool {
|
||||
return len(aConn.packets()) == 1 && len(bConn.packets()) == 1
|
||||
}, time.Second, time.Millisecond)
|
||||
require.Equal(t, [][]byte{toA}, aConn.packets())
|
||||
require.Equal(t, [][]byte{toB}, bConn.packets())
|
||||
|
||||
require.NoError(t, s.Close())
|
||||
require.True(t, aConn.isClosed())
|
||||
require.True(t, bConn.isClosed())
|
||||
require.False(t, s.register(&serverTunnel{}))
|
||||
}
|
||||
|
||||
func TestServerSlowTunnelDoesNotBlockOthers(t *testing.T) {
|
||||
s, dev := newTestServer(t)
|
||||
_, aConn := addTunnel(t, s)
|
||||
_, bConn := addTunnel(t, s)
|
||||
aConn.stall = make(chan struct{})
|
||||
defer close(aConn.stall)
|
||||
require.NoError(t, s.Start())
|
||||
defer s.Close()
|
||||
|
||||
for range tunnelQueueSize + 10 {
|
||||
dev.reads <- ipPacket("192.0.2.1", "10.14.0.2")
|
||||
}
|
||||
toB := ipPacket("192.0.2.1", "10.14.0.3")
|
||||
dev.reads <- toB
|
||||
require.Eventually(t, func() bool { return len(bConn.packets()) == 1 }, time.Second, time.Millisecond)
|
||||
require.Equal(t, [][]byte{toB}, bConn.packets())
|
||||
}
|
||||
|
||||
func TestServerClosesTunnelConnections(t *testing.T) {
|
||||
s, _ := newTestServer(t)
|
||||
a, _ := addTunnel(t, s)
|
||||
conn := newFakeTunnelConn()
|
||||
require.True(t, a.track(conn))
|
||||
other := newFakeTunnelConn()
|
||||
require.True(t, a.track(other))
|
||||
a.untrack(other)
|
||||
|
||||
s.release(a)
|
||||
require.True(t, conn.isClosed())
|
||||
require.False(t, other.isClosed())
|
||||
require.False(t, a.track(newFakeTunnelConn()))
|
||||
require.False(t, a.send(buf.New()))
|
||||
}
|
||||
|
||||
func TestServerReleasesAddresses(t *testing.T) {
|
||||
s, _ := newTestServer(t)
|
||||
a, _ := addTunnel(t, s)
|
||||
s.release(a)
|
||||
require.Nil(t, s.lookup(netip.MustParseAddr("10.14.0.2")))
|
||||
b, _ := addTunnel(t, s)
|
||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("10.14.0.3"), netip.MustParseAddr("fd14::3")}, b.addrs)
|
||||
for range 250 {
|
||||
addTunnel(t, s)
|
||||
}
|
||||
c, _ := addTunnel(t, s)
|
||||
require.Equal(t, netip.MustParseAddr("10.14.0.254"), c.addrs[0])
|
||||
addr, ok := s.pools[0].allocate()
|
||||
require.True(t, ok)
|
||||
require.Equal(t, netip.MustParseAddr("10.14.0.2"), addr)
|
||||
_, ok = s.pools[0].allocate()
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
func TestServerRemoveUserClosesTunnels(t *testing.T) {
|
||||
s, _ := newTestServer(t)
|
||||
s.validator = newValidator()
|
||||
alice := &protocol.MemoryUser{Email: "a@example.com", Account: &MemoryAccount{Password: "p"}}
|
||||
bob := &protocol.MemoryUser{Email: "b@example.com", Account: &MemoryAccount{Password: "p"}}
|
||||
require.NoError(t, s.AddUser(context.Background(), alice))
|
||||
require.NoError(t, s.AddUser(context.Background(), bob))
|
||||
_, aConn := addUserTunnel(t, s, alice)
|
||||
_, bConn := addUserTunnel(t, s, bob)
|
||||
|
||||
require.NoError(t, s.RemoveUser(context.Background(), "a@example.com"))
|
||||
require.True(t, aConn.isClosed())
|
||||
require.False(t, bConn.isClosed())
|
||||
require.Error(t, s.RemoveUser(context.Background(), "a@example.com"))
|
||||
require.Nil(t, s.validator.get("a@example.com", "p"))
|
||||
require.Equal(t, bob, s.validator.get("b@example.com", "p"))
|
||||
}
|
||||
|
||||
func TestPacketDestination(t *testing.T) {
|
||||
v4 := make([]byte, 20)
|
||||
v4[0] = 0x45
|
||||
copy(v4[16:20], []byte{192, 0, 2, 1})
|
||||
addr, ok := packetDestination(v4)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, netip.MustParseAddr("192.0.2.1"), addr)
|
||||
|
||||
v6 := make([]byte, 40)
|
||||
v6[0] = 0x60
|
||||
dst := netip.MustParseAddr("2001:db8::1").As16()
|
||||
copy(v6[24:40], dst[:])
|
||||
addr, ok = packetDestination(v6)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, netip.MustParseAddr("2001:db8::1"), addr)
|
||||
|
||||
for _, b := range [][]byte{nil, v4[:19], v6[:39], {0x50}} {
|
||||
_, ok = packetDestination(b)
|
||||
require.False(t, ok)
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
|
||||
return 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)
|
||||
}
|
||||
errors.LogInfo(ctx, "tunneling request to ", destination, " via ", network, ":", server.Destination.NetAddr())
|
||||
|
||||
@@ -124,7 +124,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
|
||||
}
|
||||
|
||||
if err = buf.CopyOnceTimeout(link.Reader, bodyWriter, time.Millisecond*100); err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout {
|
||||
return errors.New("failed to write A request payload").Base(err).AtWarning()
|
||||
return errors.New("failed to write A request payload").Base(err)
|
||||
}
|
||||
|
||||
if err := bufferedWriter.SetBuffered(false); err != nil {
|
||||
|
||||
@@ -98,7 +98,7 @@ func ReadTCPSession(validator *Validator, reader io.Reader) (*protocol.RequestHe
|
||||
iv := append([]byte(nil), buffer.BytesTo(ivLen)...)
|
||||
r, err = account.Cipher.NewDecryptionReader(account.Key, iv, reader)
|
||||
if err != nil {
|
||||
return nil, nil, drain.WithError(drainer, reader, errors.New("failed to initialize decoding stream").Base(err).AtError())
|
||||
return nil, nil, drain.WithError(drainer, reader, errors.New("failed to initialize decoding stream").Base(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func WriteTCPRequest(request *protocol.RequestHeader, writer io.Writer) (buf.Wri
|
||||
|
||||
w, err := account.Cipher.NewEncryptionWriter(account.Key, iv, writer)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to create encoding stream").Base(err).AtError()
|
||||
return nil, errors.New("failed to create encoding stream").Base(err)
|
||||
}
|
||||
|
||||
header := buf.New()
|
||||
|
||||
@@ -34,11 +34,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
|
||||
for _, user := range config.Users {
|
||||
u, err := user.ToMemoryUser()
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to get shadowsocks user").Base(err).AtError()
|
||||
return nil, errors.New("failed to get shadowsocks user").Base(err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ func (s *Server) handleUDPPayload(ctx context.Context, conn stat.Connection, dis
|
||||
func (s *Server) handleConnection(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher) error {
|
||||
sessionPolicy := s.policyManager.ForLevel(0)
|
||||
if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil {
|
||||
return errors.New("unable to set read deadline").Base(err).AtWarning()
|
||||
return errors.New("unable to set read deadline").Base(err)
|
||||
}
|
||||
|
||||
bufferedReader := buf.BufferedReader{Reader: buf.NewReader(conn)}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
type CipherMethod struct {
|
||||
Name string
|
||||
KeySaltLength int
|
||||
IsChaCha bool
|
||||
}
|
||||
|
||||
var methods = map[string]*CipherMethod{
|
||||
MethodAES128GCM: {Name: MethodAES128GCM, KeySaltLength: 16, IsChaCha: false},
|
||||
MethodAES256GCM: {Name: MethodAES256GCM, KeySaltLength: 32, IsChaCha: false},
|
||||
MethodChaCha20Poly1305: {Name: MethodChaCha20Poly1305, KeySaltLength: 32, IsChaCha: true},
|
||||
}
|
||||
|
||||
func GetCipherMethod(name string) (*CipherMethod, error) {
|
||||
name = strings.ToLower(name)
|
||||
if m, ok := methods[name]; ok {
|
||||
return m, nil
|
||||
}
|
||||
return nil, errors.New("unknown shadowsocks 2022 method")
|
||||
}
|
||||
|
||||
// NewAEAD creates standard stream AEAD cipher instance (AES-GCM or ChaCha20-Poly1305)
|
||||
func (m *CipherMethod) NewAEAD(key []byte) (cipher.AEAD, error) {
|
||||
if m.IsChaCha {
|
||||
return chacha20poly1305.New(key)
|
||||
}
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cipher.NewGCM(block)
|
||||
}
|
||||
|
||||
// NewBlock creates standard 16-byte block cipher for AES header encryption/decryption
|
||||
func (m *CipherMethod) NewBlock(key []byte) (cipher.Block, error) {
|
||||
return aes.NewCipher(key)
|
||||
}
|
||||
|
||||
// NewUDPCipher creates AEAD cipher for UDP packets (XChaCha20-Poly1305 with 24-byte nonce)
|
||||
func (m *CipherMethod) NewUDPCipher(key []byte) (cipher.AEAD, error) {
|
||||
if m.IsChaCha {
|
||||
return chacha20poly1305.NewX(key)
|
||||
}
|
||||
return nil, errors.New("shadowsocks-2022: udp separate AEAD cipher only available for chacha20 method")
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
@@ -8,26 +11,31 @@ import (
|
||||
|
||||
// MemoryAccount is an account type converted from Account.
|
||||
type MemoryAccount struct {
|
||||
Key string
|
||||
Key []byte
|
||||
}
|
||||
|
||||
// AsAccount implements protocol.AsAccount.
|
||||
func (u *Account) AsAccount() (protocol.Account, error) {
|
||||
keyStr := u.GetKey()
|
||||
raw, err := base64.StdEncoding.DecodeString(keyStr)
|
||||
if err != nil {
|
||||
raw = []byte(keyStr)
|
||||
}
|
||||
return &MemoryAccount{
|
||||
Key: u.GetKey(),
|
||||
Key: raw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Equals implements protocol.Account.Equals().
|
||||
func (a *MemoryAccount) Equals(another protocol.Account) bool {
|
||||
if account, ok := another.(*MemoryAccount); ok {
|
||||
return a.Key == account.Key
|
||||
return bytes.Equal(a.Key, account.Key)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *MemoryAccount) ToProto() proto.Message {
|
||||
return &Account{
|
||||
Key: a.Key,
|
||||
Key: base64.StdEncoding.EncodeToString(a.Key),
|
||||
}
|
||||
}
|
||||
|
||||
+208
-126
@@ -2,17 +2,11 @@ package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
shadowsocks "github.com/sagernet/sing-shadowsocks"
|
||||
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
|
||||
C "github.com/sagernet/sing/common"
|
||||
B "github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/antireplay"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/log"
|
||||
@@ -20,7 +14,10 @@ import (
|
||||
"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/singbridge"
|
||||
"github.com/xtls/xray-core/common/task"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/features/policy"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
)
|
||||
@@ -32,10 +29,13 @@ func init() {
|
||||
}
|
||||
|
||||
type Inbound struct {
|
||||
networks []net.Network
|
||||
service shadowsocks.Service
|
||||
email string
|
||||
level int
|
||||
networks []net.Network
|
||||
method *CipherMethod
|
||||
psk []byte
|
||||
user *protocol.MemoryUser
|
||||
saltFilter *antireplay.ReplayFilter[[32]byte]
|
||||
udpCodec *UDPServerCodec
|
||||
policyManager policy.Manager
|
||||
}
|
||||
|
||||
func NewServer(ctx context.Context, config *ServerConfig) (*Inbound, error) {
|
||||
@@ -46,20 +46,35 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Inbound, error) {
|
||||
net.Network_UDP,
|
||||
}
|
||||
}
|
||||
inbound := &Inbound{
|
||||
networks: networks,
|
||||
email: config.Email,
|
||||
level: int(config.Level),
|
||||
}
|
||||
if !C.Contains(shadowaead_2022.List, config.Method) {
|
||||
return nil, errors.New("unsupported method ", config.Method)
|
||||
}
|
||||
service, err := shadowaead_2022.NewServiceWithPassword(config.Method, config.Key, 500, inbound, nil)
|
||||
|
||||
method, err := GetCipherMethod(config.Method)
|
||||
if err != nil {
|
||||
return nil, errors.New("create service").Base(err)
|
||||
return nil, errors.New("unsupported method: ", config.Method).Base(err)
|
||||
}
|
||||
inbound.service = service
|
||||
return inbound, nil
|
||||
|
||||
psk, err := ParseKey(config.Key, method.KeySaltLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
udpCodec, err := NewUDPServerCodec(method, psk, 500*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := core.MustFromContext(ctx)
|
||||
return &Inbound{
|
||||
networks: networks,
|
||||
method: method,
|
||||
psk: psk,
|
||||
saltFilter: antireplay.NewMapFilter[[32]byte](60),
|
||||
user: &protocol.MemoryUser{
|
||||
Email: config.Email,
|
||||
Level: uint32(config.Level),
|
||||
},
|
||||
udpCodec: udpCodec,
|
||||
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *Inbound) Network() []net.Network {
|
||||
@@ -70,114 +85,181 @@ func (i *Inbound) Process(ctx context.Context, network net.Network, connection s
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
inbound.Name = "shadowsocks-2022"
|
||||
inbound.CanSpliceCopy = 3
|
||||
|
||||
var metadata M.Metadata
|
||||
if inbound.Source.IsValid() {
|
||||
metadata.Source = M.ParseSocksaddr(inbound.Source.NetAddr())
|
||||
}
|
||||
|
||||
ctx = session.ContextWithDispatcher(ctx, dispatcher)
|
||||
inbound.User = i.user
|
||||
|
||||
if network == net.Network_TCP {
|
||||
return singbridge.ReturnError(i.service.NewConnection(ctx, connection, metadata))
|
||||
} else {
|
||||
reader := buf.NewReader(connection)
|
||||
pc := &natPacketConn{connection}
|
||||
for {
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
return i.processTCP(ctx, connection, dispatcher)
|
||||
}
|
||||
return i.processUDP(ctx, connection, dispatcher)
|
||||
}
|
||||
|
||||
func (i *Inbound) processTCP(ctx context.Context, conn net.Conn, dispatcher routing.Dispatcher) error {
|
||||
defer conn.Close()
|
||||
|
||||
sessionPolicy := i.policyManager.ForLevel(0)
|
||||
if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil {
|
||||
return errors.New("unable to set read deadline").Base(err)
|
||||
}
|
||||
|
||||
var salt [32]byte
|
||||
saltSlice := salt[:i.method.KeySaltLength]
|
||||
if _, err := io.ReadFull(conn, saltSlice); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !i.saltFilter.Check(salt) {
|
||||
return ErrSaltNotUnique
|
||||
}
|
||||
|
||||
sessionKey := DeriveSessionSubKey(i.psk, saltSlice, i.method.KeySaltLength)
|
||||
aead, err := i.method.NewAEAD(sessionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reader := NewStreamReader(conn, aead)
|
||||
|
||||
reqHeader, err := ReadClientRequestHeader(conn, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
dest := reqHeader.Destination
|
||||
|
||||
writer, err := WriteTCPResponse(conn, i.method, i.psk, saltSlice, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: conn.RemoteAddr(),
|
||||
To: dest,
|
||||
Status: log.AccessAccepted,
|
||||
Email: i.user.Email,
|
||||
})
|
||||
|
||||
errors.LogInfo(ctx, "tunneling request to ", dest)
|
||||
|
||||
link, err := dispatcher.Dispatch(ctx, dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(reqHeader.EarlyData) > 0 {
|
||||
earlyBuf := buf.New()
|
||||
earlyBuf.Write(reqHeader.EarlyData)
|
||||
if err := link.Writer.WriteMultiBuffer(buf.MultiBuffer{earlyBuf}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
sessionPolicy = i.policyManager.ForLevel(uint32(i.user.Level))
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer)
|
||||
|
||||
requestDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
|
||||
return buf.Copy(reader, link.Writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||
return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
|
||||
return task.Run(ctx, requestDone, responseDoneAndCloseWriter)
|
||||
}
|
||||
|
||||
func (i *Inbound) processUDP(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher) error {
|
||||
udpConns := utils.NewTypedSyncMap[uint64, *udpConnEntry]()
|
||||
defer func() {
|
||||
udpConns.Range(func(key uint64, entry *udpConnEntry) bool {
|
||||
entry.timer.SetTimeout(0)
|
||||
return true
|
||||
})
|
||||
}()
|
||||
|
||||
reader := buf.NewReader(conn)
|
||||
for {
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, b := range mb {
|
||||
decoded, err := i.udpCodec.DecodePacket(b.Bytes())
|
||||
if err != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return singbridge.ReturnError(err)
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
for _, buffer := range mb {
|
||||
packet := B.As(buffer.Bytes()).ToOwned()
|
||||
buffer.Release()
|
||||
err = i.service.NewPacket(ctx, pc, packet, metadata)
|
||||
|
||||
entry, ok := udpConns.Load(decoded.SessionID)
|
||||
if !ok {
|
||||
sessCtx, cancel := context.WithCancel(ctx)
|
||||
sessCtx = log.ContextWithAccessMessage(sessCtx, &log.AccessMessage{
|
||||
From: conn.RemoteAddr(),
|
||||
To: decoded.Destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: i.user.Email,
|
||||
})
|
||||
|
||||
link, err := dispatcher.Dispatch(sessCtx, decoded.Destination)
|
||||
if err != nil {
|
||||
packet.Release()
|
||||
buf.ReleaseMulti(mb)
|
||||
return err
|
||||
cancel()
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
|
||||
newEntry := &udpConnEntry{
|
||||
link: link,
|
||||
cancel: cancel,
|
||||
}
|
||||
sessionPolicy := i.policyManager.ForLevel(uint32(i.user.Level))
|
||||
newEntry.timer = signal.CancelAfterInactivity(sessCtx, func() {
|
||||
udpConns.Delete(decoded.SessionID)
|
||||
common.Interrupt(link.Reader)
|
||||
common.Interrupt(link.Writer)
|
||||
cancel()
|
||||
}, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
|
||||
actual, loaded := udpConns.LoadOrStore(decoded.SessionID, newEntry)
|
||||
if loaded {
|
||||
// Another goroutine/packet beat us to storing, terminate our redundant link
|
||||
newEntry.timer.SetTimeout(0)
|
||||
entry = actual
|
||||
} else {
|
||||
entry = newEntry
|
||||
go func(sessID uint64, dest net.Destination, cEntry *udpConnEntry) {
|
||||
defer func() {
|
||||
cEntry.timer.SetTimeout(0)
|
||||
}()
|
||||
for {
|
||||
resMb, err := cEntry.link.Reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cEntry.timer.Update()
|
||||
for _, rb := range resMb {
|
||||
encPacket, err := i.udpCodec.EncodeServerPacket(sessID, dest, rb.Bytes())
|
||||
rb.Release()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_, _ = conn.Write(encPacket)
|
||||
}
|
||||
}
|
||||
}(decoded.SessionID, decoded.Destination, entry)
|
||||
}
|
||||
}
|
||||
|
||||
entry.timer.Update()
|
||||
payloadBuf := buf.New()
|
||||
payloadBuf.Write(decoded.Payload)
|
||||
b.Release()
|
||||
_ = entry.link.Writer.WriteMultiBuffer(buf.MultiBuffer{payloadBuf})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
inbound.User = &protocol.MemoryUser{
|
||||
Email: i.email,
|
||||
Level: uint32(i.level),
|
||||
}
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: metadata.Source,
|
||||
To: metadata.Destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: i.email,
|
||||
})
|
||||
errors.LogInfo(ctx, "tunnelling request to tcp:", metadata.Destination)
|
||||
dispatcher := session.DispatcherFromContext(ctx)
|
||||
destination, err := singbridge.ToDestination(metadata.Destination, net.Network_TCP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
link, err := dispatcher.Dispatch(ctx, destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return singbridge.CopyConn(ctx, nil, link, conn)
|
||||
}
|
||||
|
||||
func (i *Inbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
inbound.User = &protocol.MemoryUser{
|
||||
Email: i.email,
|
||||
Level: uint32(i.level),
|
||||
}
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: metadata.Source,
|
||||
To: metadata.Destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: i.email,
|
||||
})
|
||||
errors.LogInfo(ctx, "tunnelling request to udp:", metadata.Destination)
|
||||
dispatcher := session.DispatcherFromContext(ctx)
|
||||
destination, err := singbridge.ToDestination(metadata.Destination, net.Network_UDP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
link, err := dispatcher.Dispatch(ctx, destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outConn := &singbridge.PacketConnWrapper{
|
||||
Reader: link.Reader,
|
||||
Writer: link.Writer,
|
||||
Dest: destination,
|
||||
T: signal.CancelAfterInactivity(ctx, func() {
|
||||
common.Interrupt(link.Reader)
|
||||
}, 300*time.Second),
|
||||
}
|
||||
return bufio.CopyPacketConn(ctx, conn, outConn)
|
||||
}
|
||||
|
||||
func (i *Inbound) NewError(ctx context.Context, err error) {
|
||||
if E.IsClosed(err) {
|
||||
return
|
||||
}
|
||||
errors.LogWarning(ctx, err.Error())
|
||||
}
|
||||
|
||||
type natPacketConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (c *natPacketConn) ReadPacket(buffer *B.Buffer) (addr M.Socksaddr, err error) {
|
||||
_, err = buffer.ReadFrom(c)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *natPacketConn) WritePacket(buffer *B.Buffer, addr M.Socksaddr) error {
|
||||
_, err := buffer.WriteTo(c)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,21 +2,17 @@ package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"crypto/cipher"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
|
||||
C "github.com/sagernet/sing/common"
|
||||
A "github.com/sagernet/sing/common/auth"
|
||||
B "github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/antireplay"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/log"
|
||||
@@ -24,8 +20,11 @@ import (
|
||||
"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/singbridge"
|
||||
"github.com/xtls/xray-core/common/task"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
"github.com/xtls/xray-core/common/uuid"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/features/policy"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
)
|
||||
@@ -38,9 +37,16 @@ func init() {
|
||||
|
||||
type MultiUserInbound struct {
|
||||
sync.Mutex
|
||||
networks []net.Network
|
||||
users []*protocol.MemoryUser
|
||||
service *shadowaead_2022.MultiService[int]
|
||||
networks []net.Network
|
||||
method *CipherMethod
|
||||
masterPSK []byte
|
||||
usersByHash *utils.TypedSyncMap[[AESBlockSize]byte, *protocol.MemoryUser]
|
||||
usersByEmail *utils.TypedSyncMap[string, *protocol.MemoryUser]
|
||||
userCount atomic.Int64
|
||||
saltFilter *antireplay.ReplayFilter[[32]byte]
|
||||
udpSessions *UDPSessionManager
|
||||
udpMasterCipher cipher.Block
|
||||
policyManager policy.Manager
|
||||
}
|
||||
|
||||
func NewMultiServer(ctx context.Context, config *MultiUserServerConfig) (*MultiUserInbound, error) {
|
||||
@@ -51,138 +57,131 @@ func NewMultiServer(ctx context.Context, config *MultiUserServerConfig) (*MultiU
|
||||
net.Network_UDP,
|
||||
}
|
||||
}
|
||||
memUsers := []*protocol.MemoryUser{}
|
||||
for i, user := range config.Users {
|
||||
|
||||
method, err := GetCipherMethod(config.Method)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if method.IsChaCha {
|
||||
return nil, errors.New("shadowsocks 2022 multi-user: only aes methods are supported")
|
||||
}
|
||||
|
||||
masterPSK, err := ParseKey(config.Key, method.KeySaltLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
masterBlock, err := method.NewBlock(masterPSK)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := core.MustFromContext(ctx)
|
||||
i := &MultiUserInbound{
|
||||
networks: networks,
|
||||
method: method,
|
||||
masterPSK: masterPSK,
|
||||
usersByHash: utils.NewTypedSyncMap[[AESBlockSize]byte, *protocol.MemoryUser](),
|
||||
usersByEmail: utils.NewTypedSyncMap[string, *protocol.MemoryUser](),
|
||||
saltFilter: antireplay.NewMapFilter[[32]byte](60),
|
||||
udpSessions: NewUDPSessionManager(500 * time.Second),
|
||||
udpMasterCipher: masterBlock,
|
||||
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
|
||||
}
|
||||
|
||||
for idx, user := range config.Users {
|
||||
if user.Email == "" {
|
||||
u := uuid.New()
|
||||
user.Email = "unnamed-user-" + strconv.Itoa(i) + "-" + u.String()
|
||||
user.Email = "unnamed-user-" + strconv.Itoa(idx) + "-" + u.String()
|
||||
}
|
||||
u, err := user.ToMemoryUser()
|
||||
memUser, err := user.ToMemoryUser()
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to get shadowsocks user").Base(err).AtError()
|
||||
return nil, errors.New("failed to parse shadowsocks user").Base(err)
|
||||
}
|
||||
if err := i.AddUser(ctx, memUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
memUsers = append(memUsers, u)
|
||||
}
|
||||
|
||||
inbound := &MultiUserInbound{
|
||||
networks: networks,
|
||||
users: memUsers,
|
||||
}
|
||||
if config.Key == "" {
|
||||
return nil, errors.New("missing key")
|
||||
}
|
||||
psk, err := base64.StdEncoding.DecodeString(config.Key)
|
||||
if err != nil {
|
||||
return nil, errors.New("parse config").Base(err)
|
||||
}
|
||||
service, err := shadowaead_2022.NewMultiService[int](config.Method, psk, 500, inbound, nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("create service").Base(err)
|
||||
}
|
||||
err = service.UpdateUsersWithPasswords(
|
||||
C.MapIndexed(memUsers, func(index int, it *protocol.MemoryUser) int { return index }),
|
||||
C.Map(memUsers, func(it *protocol.MemoryUser) string { return it.Account.(*MemoryAccount).Key }),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.New("create service").Base(err)
|
||||
}
|
||||
|
||||
inbound.service = service
|
||||
return inbound, nil
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// AddUser implements proxy.UserManager.AddUser().
|
||||
// AddUser implements proxy.UserManager.AddUser()
|
||||
func (i *MultiUserInbound) AddUser(ctx context.Context, u *protocol.MemoryUser) error {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
var emailKey string
|
||||
if u.Email != "" {
|
||||
for idx := range i.users {
|
||||
if i.users[idx].Email == u.Email {
|
||||
return errors.New("User ", u.Email, " already exists.")
|
||||
}
|
||||
emailKey = strings.ToLower(u.Email)
|
||||
if _, exists := i.usersByEmail.Load(emailKey); exists {
|
||||
return errors.New("user ", u.Email, " already exists")
|
||||
}
|
||||
}
|
||||
i.users = append(i.users, u)
|
||||
|
||||
// sync to multi service
|
||||
// Considering implements shadowsocks2022 in xray-core may have better performance.
|
||||
i.service.UpdateUsersWithPasswords(
|
||||
C.MapIndexed(i.users, func(index int, it *protocol.MemoryUser) int { return index }),
|
||||
C.Map(i.users, func(it *protocol.MemoryUser) string { return it.Account.(*MemoryAccount).Key }),
|
||||
)
|
||||
memAcc, ok := u.Account.(*MemoryAccount)
|
||||
if !ok {
|
||||
return errors.New("missing or invalid user account")
|
||||
}
|
||||
|
||||
if len(memAcc.Key) != i.method.KeySaltLength {
|
||||
return ErrBadKey
|
||||
}
|
||||
|
||||
pskHash := DeriveUserPSKHash(memAcc.Key)
|
||||
i.usersByHash.Store(pskHash, u)
|
||||
if emailKey != "" {
|
||||
i.usersByEmail.Store(emailKey, u)
|
||||
}
|
||||
i.userCount.Add(1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveUser implements proxy.UserManager.RemoveUser().
|
||||
// RemoveUser implements proxy.UserManager.RemoveUser()
|
||||
func (i *MultiUserInbound) RemoveUser(ctx context.Context, email string) error {
|
||||
if email == "" {
|
||||
return errors.New("Email must not be empty.")
|
||||
return errors.New("email must not be empty")
|
||||
}
|
||||
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
idx := -1
|
||||
for ii, u := range i.users {
|
||||
if strings.EqualFold(u.Email, email) {
|
||||
idx = ii
|
||||
break
|
||||
}
|
||||
emailKey := strings.ToLower(email)
|
||||
u, loaded := i.usersByEmail.LoadAndDelete(emailKey)
|
||||
if !loaded {
|
||||
return errors.New("user ", email, " not found")
|
||||
}
|
||||
|
||||
if idx == -1 {
|
||||
return errors.New("User ", email, " not found.")
|
||||
}
|
||||
|
||||
ulen := len(i.users)
|
||||
|
||||
i.users[idx] = i.users[ulen-1]
|
||||
i.users[ulen-1] = nil
|
||||
i.users = i.users[:ulen-1]
|
||||
|
||||
// sync to multi service
|
||||
// Considering implements shadowsocks2022 in xray-core may have better performance.
|
||||
i.service.UpdateUsersWithPasswords(
|
||||
C.MapIndexed(i.users, func(index int, it *protocol.MemoryUser) int { return index }),
|
||||
C.Map(i.users, func(it *protocol.MemoryUser) string { return it.Account.(*MemoryAccount).Key }),
|
||||
)
|
||||
pskHash := DeriveUserPSKHash(u.Account.(*MemoryAccount).Key)
|
||||
i.usersByHash.Delete(pskHash)
|
||||
i.userCount.Add(-1)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUser implements proxy.UserManager.GetUser().
|
||||
// GetUser implements proxy.UserManager.GetUser()
|
||||
func (i *MultiUserInbound) GetUser(ctx context.Context, email string) *protocol.MemoryUser {
|
||||
if email == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
|
||||
for _, u := range i.users {
|
||||
if strings.EqualFold(u.Email, email) {
|
||||
return u
|
||||
}
|
||||
}
|
||||
return nil
|
||||
u, _ := i.usersByEmail.Load(strings.ToLower(email))
|
||||
return u
|
||||
}
|
||||
|
||||
// GetUsers implements proxy.UserManager.GetUsers().
|
||||
// GetUsers implements proxy.UserManager.GetUsers()
|
||||
func (i *MultiUserInbound) GetUsers(ctx context.Context) []*protocol.MemoryUser {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
dst := make([]*protocol.MemoryUser, len(i.users))
|
||||
copy(dst, i.users)
|
||||
return dst
|
||||
var users []*protocol.MemoryUser
|
||||
i.usersByEmail.Range(func(_ string, user *protocol.MemoryUser) bool {
|
||||
users = append(users, user)
|
||||
return true
|
||||
})
|
||||
return users
|
||||
}
|
||||
|
||||
// GetUsersCount implements proxy.UserManager.GetUsersCount().
|
||||
// GetUsersCount implements proxy.UserManager.GetUsersCount()
|
||||
func (i *MultiUserInbound) GetUsersCount(context.Context) int64 {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
return int64(len(i.users))
|
||||
return i.userCount.Load()
|
||||
}
|
||||
|
||||
func (i *MultiUserInbound) Network() []net.Network {
|
||||
@@ -194,97 +193,317 @@ func (i *MultiUserInbound) Process(ctx context.Context, network net.Network, con
|
||||
inbound.Name = "shadowsocks-2022-multi"
|
||||
inbound.CanSpliceCopy = 3
|
||||
|
||||
var metadata M.Metadata
|
||||
if inbound.Source.IsValid() {
|
||||
metadata.Source = M.ParseSocksaddr(inbound.Source.NetAddr())
|
||||
if network == net.Network_TCP {
|
||||
return i.processTCP(ctx, connection, dispatcher)
|
||||
}
|
||||
return i.processUDP(ctx, connection, dispatcher)
|
||||
}
|
||||
|
||||
func (i *MultiUserInbound) processTCP(ctx context.Context, conn net.Conn, dispatcher routing.Dispatcher) error {
|
||||
defer conn.Close()
|
||||
|
||||
sessionPolicy := i.policyManager.ForLevel(0)
|
||||
if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil {
|
||||
return errors.New("unable to set read deadline").Base(err)
|
||||
}
|
||||
|
||||
ctx = session.ContextWithDispatcher(ctx, dispatcher)
|
||||
// 1. Read Request Salt (16 or 32 bytes)
|
||||
var salt [32]byte
|
||||
saltSlice := salt[:i.method.KeySaltLength]
|
||||
if _, err := io.ReadFull(conn, saltSlice); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if network == net.Network_TCP {
|
||||
return singbridge.ReturnError(i.service.NewConnection(ctx, connection, metadata))
|
||||
} else {
|
||||
reader := buf.NewReader(connection)
|
||||
pc := &natPacketConn{connection}
|
||||
for {
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return singbridge.ReturnError(err)
|
||||
if !i.saltFilter.Check(salt) {
|
||||
return ErrSaltNotUnique
|
||||
}
|
||||
|
||||
// 2. Read Extended Identity Header (16 bytes)
|
||||
var eih [AESBlockSize]byte
|
||||
if _, err := io.ReadFull(conn, eih[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Decrypt EIH with IdentitySubKey derived from masterPSK and salt
|
||||
identitySubkey := DeriveIdentitySubKey(i.masterPSK, saltSlice, i.method.KeySaltLength)
|
||||
block, err := i.method.NewBlock(identitySubkey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var decryptedHash [AESBlockSize]byte
|
||||
block.Decrypt(decryptedHash[:], eih[:])
|
||||
|
||||
// Lookup user
|
||||
user, ok := i.usersByHash.Load(decryptedHash)
|
||||
if !ok || user == nil {
|
||||
return ErrInvalidRequest
|
||||
}
|
||||
userPSK := user.Account.(*MemoryAccount).Key
|
||||
|
||||
// 3. Derive Session Subkey using matched user's PSK
|
||||
sessionKey := DeriveSessionSubKey(userPSK, saltSlice, i.method.KeySaltLength)
|
||||
aead, err := i.method.NewAEAD(sessionKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reader := NewStreamReader(conn, aead)
|
||||
|
||||
// 4 & 5. Read Client Request Header
|
||||
reqHeader, err := ReadClientRequestHeader(conn, reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
dest := reqHeader.Destination
|
||||
|
||||
// 6. Send Server Response Handshake
|
||||
writer, err := WriteTCPResponse(conn, i.method, userPSK, saltSlice, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 7. Dispatch Connection to Xray routing with matched User
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
inbound.User = user
|
||||
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: conn.RemoteAddr(),
|
||||
To: dest,
|
||||
Status: log.AccessAccepted,
|
||||
Email: user.Email,
|
||||
})
|
||||
|
||||
errors.LogInfo(ctx, "tunneling request to ", dest, " for user ", user.Email)
|
||||
|
||||
link, err := dispatcher.Dispatch(ctx, dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(reqHeader.EarlyData) > 0 {
|
||||
earlyBuf := buf.New()
|
||||
earlyBuf.Write(reqHeader.EarlyData)
|
||||
if err := link.Writer.WriteMultiBuffer(buf.MultiBuffer{earlyBuf}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
sessionPolicy = i.policyManager.ForLevel(user.Level)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer)
|
||||
|
||||
requestDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
|
||||
return buf.Copy(reader, link.Writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||
return buf.Copy(link.Reader, writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
|
||||
return task.Run(ctx, requestDone, responseDoneAndCloseWriter)
|
||||
}
|
||||
|
||||
func (i *MultiUserInbound) processUDP(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher) error {
|
||||
udpConns := utils.NewTypedSyncMap[uint64, *udpConnEntry]()
|
||||
defer func() {
|
||||
udpConns.Range(func(key uint64, entry *udpConnEntry) bool {
|
||||
entry.timer.SetTimeout(0)
|
||||
return true
|
||||
})
|
||||
}()
|
||||
|
||||
reader := buf.NewReader(conn)
|
||||
for {
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, b := range mb {
|
||||
// In multi-user UDP:
|
||||
// Packet header is 16 bytes: Encrypted(SessionID + PacketID)
|
||||
// Followed by 16 bytes EIH
|
||||
packetBytes := b.Bytes()
|
||||
if len(packetBytes) < 32+1+8+2 {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
for _, buffer := range mb {
|
||||
packet := B.As(buffer.Bytes()).ToOwned()
|
||||
buffer.Release()
|
||||
err = i.service.NewPacket(ctx, pc, packet, metadata)
|
||||
|
||||
var rawHeader [16]byte
|
||||
i.udpMasterCipher.Decrypt(rawHeader[:], packetBytes[:16])
|
||||
|
||||
sessionID := binary.BigEndian.Uint64(rawHeader[:8])
|
||||
packetID := binary.BigEndian.Uint64(rawHeader[8:16])
|
||||
|
||||
// Replay protection & session lookup
|
||||
sessionItem := i.udpSessions.GetOrCreate(sessionID)
|
||||
|
||||
sessionItem.Lock()
|
||||
if !sessionItem.Window.Check(packetID) {
|
||||
sessionItem.Unlock()
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
|
||||
var userPSK []byte
|
||||
var currentUser *protocol.MemoryUser
|
||||
if sessionItem.User != nil {
|
||||
currentUser = sessionItem.User
|
||||
userPSK = sessionItem.UserPSK
|
||||
sessionItem.Unlock()
|
||||
} else {
|
||||
sessionItem.Unlock()
|
||||
// Decrypt EIH
|
||||
identitySubkey := DeriveIdentitySubKey(i.masterPSK, rawHeader[:8], i.method.KeySaltLength)
|
||||
idBlock, err := i.method.NewBlock(identitySubkey)
|
||||
if err != nil {
|
||||
packet.Release()
|
||||
buf.ReleaseMulti(mb)
|
||||
return err
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
|
||||
var decryptedHash [16]byte
|
||||
idBlock.Decrypt(decryptedHash[:], packetBytes[16:32])
|
||||
|
||||
user, ok := i.usersByHash.Load(decryptedHash)
|
||||
if !ok || user == nil {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
currentUser = user
|
||||
userPSK = user.Account.(*MemoryAccount).Key
|
||||
|
||||
sessionItem.Lock()
|
||||
sessionItem.User = user
|
||||
sessionItem.UserPSK = userPSK
|
||||
sessionItem.Unlock()
|
||||
}
|
||||
|
||||
// Decrypt Body (with AEAD caching per session)
|
||||
bodyAead := sessionItem.GetRemoteCipher()
|
||||
if bodyAead == nil {
|
||||
bodyKey := DeriveSessionSubKey(userPSK, rawHeader[:8], i.method.KeySaltLength)
|
||||
var err error
|
||||
bodyAead, err = i.method.NewAEAD(bodyKey)
|
||||
if err != nil {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
sessionItem.SetRemoteCipher(bodyAead)
|
||||
}
|
||||
|
||||
bodyNonce := rawHeader[4:16]
|
||||
bodyCipher := packetBytes[32:]
|
||||
bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil)
|
||||
b.Release()
|
||||
if err != nil || len(bodyPlain) < 1+8+2 {
|
||||
continue
|
||||
}
|
||||
|
||||
sessionItem.Lock()
|
||||
sessionItem.Window.Add(packetID)
|
||||
sessionItem.Unlock()
|
||||
|
||||
if bodyPlain[0] != HeaderTypeClient {
|
||||
continue
|
||||
}
|
||||
epoch := binary.BigEndian.Uint64(bodyPlain[1:9])
|
||||
diff := time.Now().Unix() - int64(epoch)
|
||||
if diff < -30 || diff > 30 {
|
||||
continue
|
||||
}
|
||||
|
||||
paddingLen := int(binary.BigEndian.Uint16(bodyPlain[9:11]))
|
||||
offset := 11 + paddingLen
|
||||
if len(bodyPlain) < offset {
|
||||
continue
|
||||
}
|
||||
|
||||
dest, addrLen, err := parseAddressPort(bodyPlain[offset:])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
payload := bodyPlain[offset+addrLen:]
|
||||
|
||||
entry, ok := udpConns.Load(sessionID)
|
||||
if !ok {
|
||||
sessCtx, cancel := context.WithCancel(ctx)
|
||||
inbound := session.InboundFromContext(sessCtx)
|
||||
inbound.User = currentUser
|
||||
|
||||
sessCtx = log.ContextWithAccessMessage(sessCtx, &log.AccessMessage{
|
||||
From: conn.RemoteAddr(),
|
||||
To: dest,
|
||||
Status: log.AccessAccepted,
|
||||
Email: currentUser.Email,
|
||||
})
|
||||
|
||||
link, err := dispatcher.Dispatch(sessCtx, dest)
|
||||
if err != nil {
|
||||
cancel()
|
||||
continue
|
||||
}
|
||||
|
||||
newEntry := &udpConnEntry{
|
||||
link: link,
|
||||
cancel: cancel,
|
||||
}
|
||||
sessionPolicy := i.policyManager.ForLevel(currentUser.Level)
|
||||
newEntry.timer = signal.CancelAfterInactivity(sessCtx, func() {
|
||||
udpConns.Delete(sessionID)
|
||||
common.Interrupt(link.Reader)
|
||||
common.Interrupt(link.Writer)
|
||||
cancel()
|
||||
}, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
|
||||
actual, loaded := udpConns.LoadOrStore(sessionID, newEntry)
|
||||
if loaded {
|
||||
newEntry.timer.SetTimeout(0)
|
||||
entry = actual
|
||||
} else {
|
||||
entry = newEntry
|
||||
go func(sessID uint64, uPSK []byte, d net.Destination, cEntry *udpConnEntry) {
|
||||
defer func() {
|
||||
cEntry.timer.SetTimeout(0)
|
||||
}()
|
||||
for {
|
||||
resMb, err := cEntry.link.Reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cEntry.timer.Update()
|
||||
for _, rb := range resMb {
|
||||
encPacket, err := i.encodeServerUDPPacket(sessID, uPSK, d, rb.Bytes())
|
||||
rb.Release()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_, _ = conn.Write(encPacket)
|
||||
}
|
||||
}
|
||||
}(sessionID, userPSK, dest, entry)
|
||||
}
|
||||
}
|
||||
|
||||
entry.timer.Update()
|
||||
pBuf := buf.New()
|
||||
pBuf.Write(payload)
|
||||
_ = entry.link.Writer.WriteMultiBuffer(buf.MultiBuffer{pBuf})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *MultiUserInbound) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
userInt, _ := A.UserFromContext[int](ctx)
|
||||
user := i.users[userInt]
|
||||
inbound.User = user
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: metadata.Source,
|
||||
To: metadata.Destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: user.Email,
|
||||
})
|
||||
errors.LogInfo(ctx, "tunnelling request to tcp:", metadata.Destination)
|
||||
dispatcher := session.DispatcherFromContext(ctx)
|
||||
destination, err := singbridge.ToDestination(metadata.Destination, net.Network_TCP)
|
||||
if err != nil {
|
||||
return err
|
||||
func (i *MultiUserInbound) encodeServerUDPPacket(clientSessionID uint64, userPSK []byte, dest net.Destination, payload []byte) ([]byte, error) {
|
||||
sessionItem := i.udpSessions.GetOrCreate(clientSessionID)
|
||||
if err := sessionItem.EnsureServerState(i.method, i.udpMasterCipher, nil, userPSK); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
link, err := dispatcher.Dispatch(ctx, destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return singbridge.CopyConn(ctx, conn, link, conn)
|
||||
}
|
||||
|
||||
func (i *MultiUserInbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
userInt, _ := A.UserFromContext[int](ctx)
|
||||
user := i.users[userInt]
|
||||
inbound.User = user
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: metadata.Source,
|
||||
To: metadata.Destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: user.Email,
|
||||
})
|
||||
errors.LogInfo(ctx, "tunnelling request to udp:", metadata.Destination)
|
||||
dispatcher := session.DispatcherFromContext(ctx)
|
||||
destination, err := singbridge.ToDestination(metadata.Destination, net.Network_UDP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
link, err := dispatcher.Dispatch(ctx, destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outConn := &singbridge.PacketConnWrapper{
|
||||
Reader: link.Reader,
|
||||
Writer: link.Writer,
|
||||
Dest: destination,
|
||||
T: signal.CancelAfterInactivity(ctx, func() {
|
||||
common.Interrupt(link.Reader)
|
||||
}, 300*time.Second),
|
||||
}
|
||||
return bufio.CopyPacketConn(ctx, conn, outConn)
|
||||
}
|
||||
|
||||
func (i *MultiUserInbound) NewError(ctx context.Context, err error) {
|
||||
if E.IsClosed(err) {
|
||||
return
|
||||
}
|
||||
errors.LogWarning(ctx, err.Error())
|
||||
return sessionItem.EncodeServerPacket(i.method, clientSessionID, dest, payload)
|
||||
}
|
||||
|
||||
@@ -2,18 +2,12 @@ package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/cipher"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
|
||||
C "github.com/sagernet/sing/common"
|
||||
A "github.com/sagernet/sing/common/auth"
|
||||
B "github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
@@ -22,8 +16,11 @@ import (
|
||||
"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/singbridge"
|
||||
"github.com/xtls/xray-core/common/task"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
"github.com/xtls/xray-core/common/uuid"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/features/policy"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
)
|
||||
@@ -34,10 +31,22 @@ func init() {
|
||||
}))
|
||||
}
|
||||
|
||||
type relayDest struct {
|
||||
destination net.Destination
|
||||
email string
|
||||
level uint32
|
||||
key []byte
|
||||
blockCipher cipher.Block
|
||||
}
|
||||
|
||||
type RelayInbound struct {
|
||||
networks []net.Network
|
||||
destinations []*RelayDestination
|
||||
service *shadowaead_2022.RelayService[int]
|
||||
networks []net.Network
|
||||
method *CipherMethod
|
||||
relayPSK []byte
|
||||
relayBlock cipher.Block
|
||||
destinations map[[AESBlockSize]byte]*relayDest
|
||||
rawDestinations []*RelayDestination
|
||||
policyManager policy.Manager
|
||||
}
|
||||
|
||||
func NewRelayServer(ctx context.Context, config *RelayServerConfig) (*RelayInbound, error) {
|
||||
@@ -48,39 +57,63 @@ func NewRelayServer(ctx context.Context, config *RelayServerConfig) (*RelayInbou
|
||||
net.Network_UDP,
|
||||
}
|
||||
}
|
||||
inbound := &RelayInbound{
|
||||
networks: networks,
|
||||
destinations: config.Destinations,
|
||||
}
|
||||
if !C.Contains(shadowaead_2022.List, config.Method) || !strings.Contains(config.Method, "aes") {
|
||||
return nil, errors.New("unsupported method ", config.Method)
|
||||
}
|
||||
service, err := shadowaead_2022.NewRelayServiceWithPassword[int](config.Method, config.Key, 500, inbound)
|
||||
|
||||
method, err := GetCipherMethod(config.Method)
|
||||
if err != nil {
|
||||
return nil, errors.New("create service").Base(err)
|
||||
return nil, err
|
||||
}
|
||||
if method.IsChaCha {
|
||||
return nil, errors.New("shadowsocks 2022 relay: only aes methods are supported")
|
||||
}
|
||||
|
||||
for i, destination := range config.Destinations {
|
||||
if destination.Email == "" {
|
||||
relayPSK, err := ParseKey(config.Key, method.KeySaltLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
relayBlock, err := method.NewBlock(relayPSK)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := core.MustFromContext(ctx)
|
||||
i := &RelayInbound{
|
||||
networks: networks,
|
||||
method: method,
|
||||
relayPSK: relayPSK,
|
||||
relayBlock: relayBlock,
|
||||
destinations: make(map[[AESBlockSize]byte]*relayDest),
|
||||
rawDestinations: config.Destinations,
|
||||
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
|
||||
}
|
||||
|
||||
for idx, d := range config.Destinations {
|
||||
if d.Email == "" {
|
||||
u := uuid.New()
|
||||
destination.Email = "unnamed-destination-" + strconv.Itoa(i) + "-" + u.String()
|
||||
d.Email = "unnamed-destination-" + strconv.Itoa(idx) + "-" + u.String()
|
||||
}
|
||||
destKey, err := ParseKey(d.Key, method.KeySaltLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
destBlock, err := method.NewBlock(destKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hash := DeriveUserPSKHash(destKey)
|
||||
|
||||
i.destinations[hash] = &relayDest{
|
||||
destination: net.TCPDestination(d.Address.AsAddress(), net.Port(d.Port)),
|
||||
email: d.Email,
|
||||
level: uint32(d.Level),
|
||||
key: destKey,
|
||||
blockCipher: destBlock,
|
||||
}
|
||||
}
|
||||
err = service.UpdateUsersWithPasswords(
|
||||
C.MapIndexed(config.Destinations, func(index int, it *RelayDestination) int { return index }),
|
||||
C.Map(config.Destinations, func(it *RelayDestination) string { return it.Key }),
|
||||
C.Map(config.Destinations, func(it *RelayDestination) M.Socksaddr {
|
||||
return singbridge.ToSocksaddr(net.Destination{
|
||||
Address: it.Address.AsAddress(),
|
||||
Port: net.Port(it.Port),
|
||||
})
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.New("create service").Base(err)
|
||||
}
|
||||
inbound.service = service
|
||||
return inbound, nil
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i *RelayInbound) Network() []net.Network {
|
||||
@@ -92,103 +125,206 @@ func (i *RelayInbound) Process(ctx context.Context, network net.Network, connect
|
||||
inbound.Name = "shadowsocks-2022-relay"
|
||||
inbound.CanSpliceCopy = 3
|
||||
|
||||
var metadata M.Metadata
|
||||
if inbound.Source.IsValid() {
|
||||
metadata.Source = M.ParseSocksaddr(inbound.Source.NetAddr())
|
||||
if network == net.Network_TCP {
|
||||
return i.processTCP(ctx, connection, dispatcher)
|
||||
}
|
||||
return i.processUDP(ctx, connection, dispatcher)
|
||||
}
|
||||
|
||||
func (i *RelayInbound) processTCP(ctx context.Context, conn net.Conn, dispatcher routing.Dispatcher) error {
|
||||
defer conn.Close()
|
||||
|
||||
sessionPolicy := i.policyManager.ForLevel(0)
|
||||
if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil {
|
||||
return errors.New("unable to set read deadline").Base(err)
|
||||
}
|
||||
|
||||
ctx = session.ContextWithDispatcher(ctx, dispatcher)
|
||||
// Read Salt + Outer EIH
|
||||
needed := i.method.KeySaltLength + AESBlockSize
|
||||
var headerBuf [48]byte
|
||||
headerSlice := headerBuf[:needed]
|
||||
if _, err := io.ReadFull(conn, headerSlice); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if network == net.Network_TCP {
|
||||
return singbridge.ReturnError(i.service.NewConnection(ctx, connection, metadata))
|
||||
} else {
|
||||
reader := buf.NewReader(connection)
|
||||
pc := &natPacketConn{connection}
|
||||
for {
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return singbridge.ReturnError(err)
|
||||
salt := headerSlice[:i.method.KeySaltLength]
|
||||
eih := headerSlice[i.method.KeySaltLength:]
|
||||
|
||||
identitySubkey := DeriveIdentitySubKey(i.relayPSK, salt, i.method.KeySaltLength)
|
||||
block, err := i.method.NewBlock(identitySubkey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var decryptedHash [AESBlockSize]byte
|
||||
block.Decrypt(decryptedHash[:], eih)
|
||||
|
||||
targetDest, ok := i.destinations[decryptedHash]
|
||||
if !ok {
|
||||
return ErrInvalidRequest
|
||||
}
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
inbound.User = &protocol.MemoryUser{
|
||||
Email: targetDest.email,
|
||||
Level: targetDest.level,
|
||||
}
|
||||
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: conn.RemoteAddr(),
|
||||
To: targetDest.destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: targetDest.email,
|
||||
})
|
||||
|
||||
errors.LogInfo(ctx, "relaying connection to ", targetDest.destination)
|
||||
|
||||
link, err := dispatcher.Dispatch(ctx, targetDest.destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Unwrap outer EIH: send client salt to next hop, stripping this hop's EIH
|
||||
saltBuf := buf.New()
|
||||
saltBuf.Write(salt)
|
||||
if err := link.Writer.WriteMultiBuffer(buf.MultiBuffer{saltBuf}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sessionPolicy = i.policyManager.ForLevel(targetDest.level)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer)
|
||||
|
||||
requestDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
|
||||
return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||
return buf.Copy(link.Reader, buf.NewWriter(conn), buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
|
||||
return task.Run(ctx, requestDone, responseDoneAndCloseWriter)
|
||||
}
|
||||
|
||||
func (i *RelayInbound) processUDP(ctx context.Context, conn stat.Connection, dispatcher routing.Dispatcher) error {
|
||||
udpConns := utils.NewTypedSyncMap[uint64, *udpConnEntry]()
|
||||
defer func() {
|
||||
udpConns.Range(func(key uint64, entry *udpConnEntry) bool {
|
||||
entry.timer.SetTimeout(0)
|
||||
return true
|
||||
})
|
||||
}()
|
||||
|
||||
reader := buf.NewReader(conn)
|
||||
for {
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, b := range mb {
|
||||
data := b.Bytes()
|
||||
if len(data) < 2*AESBlockSize {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
for _, buffer := range mb {
|
||||
packet := B.As(buffer.Bytes()).ToOwned()
|
||||
buffer.Release()
|
||||
err = i.service.NewPacket(ctx, pc, packet, metadata)
|
||||
|
||||
var packetHeader [AESBlockSize]byte
|
||||
i.relayBlock.Decrypt(packetHeader[:], data[:AESBlockSize])
|
||||
|
||||
var eiHeader [AESBlockSize]byte
|
||||
i.relayBlock.Decrypt(eiHeader[:], data[AESBlockSize:2*AESBlockSize])
|
||||
for idx := 0; idx < AESBlockSize; idx++ {
|
||||
eiHeader[idx] ^= packetHeader[idx]
|
||||
}
|
||||
|
||||
targetDest, ok := i.destinations[eiHeader]
|
||||
if !ok {
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract sessionID from raw packetHeader for session-level link caching before re-encrypting
|
||||
sessionID := binary.BigEndian.Uint64(packetHeader[:8])
|
||||
|
||||
// Re-encrypt packetHeader with next hop block cipher
|
||||
targetDest.blockCipher.Encrypt(packetHeader[:], packetHeader[:])
|
||||
|
||||
// Strip outer EIH: replace second block with re-encrypted packetHeader and advance
|
||||
copy(data[AESBlockSize:2*AESBlockSize], packetHeader[:])
|
||||
b.Advance(int32(AESBlockSize))
|
||||
|
||||
dest := targetDest.destination
|
||||
dest.Network = net.Network_UDP
|
||||
|
||||
entry, ok := udpConns.Load(sessionID)
|
||||
if !ok {
|
||||
sessCtx, cancel := context.WithCancel(ctx)
|
||||
inbound := session.InboundFromContext(sessCtx)
|
||||
inbound.User = &protocol.MemoryUser{
|
||||
Email: targetDest.email,
|
||||
Level: targetDest.level,
|
||||
}
|
||||
|
||||
sessCtx = log.ContextWithAccessMessage(sessCtx, &log.AccessMessage{
|
||||
From: conn.RemoteAddr(),
|
||||
To: dest,
|
||||
Status: log.AccessAccepted,
|
||||
Email: targetDest.email,
|
||||
})
|
||||
|
||||
link, err := dispatcher.Dispatch(sessCtx, dest)
|
||||
if err != nil {
|
||||
packet.Release()
|
||||
buf.ReleaseMulti(mb)
|
||||
return err
|
||||
cancel()
|
||||
b.Release()
|
||||
continue
|
||||
}
|
||||
|
||||
newEntry := &udpConnEntry{
|
||||
link: link,
|
||||
cancel: cancel,
|
||||
}
|
||||
sessionPolicy := i.policyManager.ForLevel(targetDest.level)
|
||||
newEntry.timer = signal.CancelAfterInactivity(sessCtx, func() {
|
||||
udpConns.Delete(sessionID)
|
||||
common.Interrupt(link.Reader)
|
||||
common.Interrupt(link.Writer)
|
||||
cancel()
|
||||
}, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
|
||||
actual, loaded := udpConns.LoadOrStore(sessionID, newEntry)
|
||||
if loaded {
|
||||
newEntry.timer.SetTimeout(0)
|
||||
entry = actual
|
||||
} else {
|
||||
entry = newEntry
|
||||
go func(cEntry *udpConnEntry) {
|
||||
defer func() {
|
||||
cEntry.timer.SetTimeout(0)
|
||||
}()
|
||||
for {
|
||||
resMb, err := cEntry.link.Reader.ReadMultiBuffer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cEntry.timer.Update()
|
||||
for _, rb := range resMb {
|
||||
_, _ = conn.Write(rb.Bytes())
|
||||
rb.Release()
|
||||
}
|
||||
}
|
||||
}(entry)
|
||||
}
|
||||
}
|
||||
|
||||
entry.timer.Update()
|
||||
_ = entry.link.Writer.WriteMultiBuffer(buf.MultiBuffer{b})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *RelayInbound) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
userInt, _ := A.UserFromContext[int](ctx)
|
||||
user := i.destinations[userInt]
|
||||
inbound.User = &protocol.MemoryUser{
|
||||
Email: user.Email,
|
||||
Level: uint32(user.Level),
|
||||
}
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: metadata.Source,
|
||||
To: metadata.Destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: user.Email,
|
||||
})
|
||||
errors.LogInfo(ctx, "tunnelling request to tcp:", metadata.Destination)
|
||||
dispatcher := session.DispatcherFromContext(ctx)
|
||||
destination, err := singbridge.ToDestination(metadata.Destination, net.Network_TCP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
link, err := dispatcher.Dispatch(ctx, destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return singbridge.CopyConn(ctx, nil, link, conn)
|
||||
}
|
||||
|
||||
func (i *RelayInbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
userInt, _ := A.UserFromContext[int](ctx)
|
||||
user := i.destinations[userInt]
|
||||
inbound.User = &protocol.MemoryUser{
|
||||
Email: user.Email,
|
||||
Level: uint32(user.Level),
|
||||
}
|
||||
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
|
||||
From: metadata.Source,
|
||||
To: metadata.Destination,
|
||||
Status: log.AccessAccepted,
|
||||
Email: user.Email,
|
||||
})
|
||||
errors.LogInfo(ctx, "tunnelling request to udp:", metadata.Destination)
|
||||
dispatcher := session.DispatcherFromContext(ctx)
|
||||
destination, err := singbridge.ToDestination(metadata.Destination, net.Network_UDP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
link, err := dispatcher.Dispatch(ctx, destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outConn := &singbridge.PacketConnWrapper{
|
||||
Reader: link.Reader,
|
||||
Writer: link.Writer,
|
||||
Dest: destination,
|
||||
T: signal.CancelAfterInactivity(ctx, func() {
|
||||
common.Interrupt(link.Reader)
|
||||
}, 300*time.Second),
|
||||
}
|
||||
return bufio.CopyPacketConn(ctx, conn, outConn)
|
||||
}
|
||||
|
||||
func (i *RelayInbound) NewError(ctx context.Context, err error) {
|
||||
if E.IsClosed(err) {
|
||||
return
|
||||
}
|
||||
errors.LogWarning(ctx, err.Error())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"lukechampine.com/blake3"
|
||||
)
|
||||
|
||||
const (
|
||||
ContextSessionSubKey = "shadowsocks 2022 session subkey"
|
||||
ContextIdentitySubKey = "shadowsocks 2022 identity subkey"
|
||||
)
|
||||
|
||||
// ParseKey decodes a base64 or raw PSK key string and validates its length
|
||||
func ParseKey(key string, keyLength int) ([]byte, error) {
|
||||
raw, err := base64.StdEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
raw = []byte(key)
|
||||
}
|
||||
if len(raw) != keyLength {
|
||||
return nil, ErrBadKey
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
func ParsePSKList(password string, keyLength int) ([][]byte, error) {
|
||||
parts := strings.Split(password, ":")
|
||||
pskList := make([][]byte, len(parts))
|
||||
for i, part := range parts {
|
||||
norm, err := ParseKey(part, keyLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pskList[i] = norm
|
||||
}
|
||||
return pskList, nil
|
||||
}
|
||||
|
||||
func deriveSubKey(ctx string, psk, salt []byte, keyLength int) []byte {
|
||||
var keyMaterial [64]byte
|
||||
kmLen := len(psk) + len(salt)
|
||||
copy(keyMaterial[:], psk)
|
||||
copy(keyMaterial[len(psk):], salt)
|
||||
out := make([]byte, keyLength)
|
||||
blake3.DeriveKey(out, ctx, keyMaterial[:kmLen])
|
||||
return out
|
||||
}
|
||||
|
||||
func DeriveSessionSubKey(psk, salt []byte, keyLength int) []byte {
|
||||
return deriveSubKey(ContextSessionSubKey, psk, salt, keyLength)
|
||||
}
|
||||
|
||||
func DeriveIdentitySubKey(psk, salt []byte, keyLength int) []byte {
|
||||
return deriveSubKey(ContextIdentitySubKey, psk, salt, keyLength)
|
||||
}
|
||||
|
||||
func DeriveUserPSKHash(userPSK []byte) [AESBlockSize]byte {
|
||||
h := blake3.Sum512(userPSK)
|
||||
var out [AESBlockSize]byte
|
||||
copy(out[:], h[:AESBlockSize])
|
||||
return out
|
||||
}
|
||||
@@ -2,21 +2,20 @@ package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
shadowsocks "github.com/sagernet/sing-shadowsocks"
|
||||
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
|
||||
C "github.com/sagernet/sing/common"
|
||||
B "github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"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/retry"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/common/signal"
|
||||
"github.com/xtls/xray-core/common/singbridge"
|
||||
"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/transport"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
@@ -28,42 +27,47 @@ func init() {
|
||||
}
|
||||
|
||||
type Outbound struct {
|
||||
ctx context.Context
|
||||
server net.Destination
|
||||
method shadowsocks.Method
|
||||
server net.Destination
|
||||
method *CipherMethod
|
||||
pskList [][]byte
|
||||
finalPSK []byte
|
||||
udpCodec *UDPPacketCodec
|
||||
policyManager policy.Manager
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) {
|
||||
o := &Outbound{
|
||||
ctx: ctx,
|
||||
method, err := GetCipherMethod(config.Method)
|
||||
if err != nil {
|
||||
return nil, errors.New("unsupported method: ", config.Method).Base(err)
|
||||
}
|
||||
|
||||
pskList, err := ParsePSKList(config.Key, method.KeySaltLength)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid key: ", config.Key).Base(err)
|
||||
}
|
||||
|
||||
finalPSK := pskList[len(pskList)-1]
|
||||
udpCodec, err := NewUDPPacketCodec(method, finalPSK)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to create udp packet codec").Base(err)
|
||||
}
|
||||
|
||||
v := core.MustFromContext(ctx)
|
||||
return &Outbound{
|
||||
server: net.Destination{
|
||||
Address: config.Address.AsAddress(),
|
||||
Port: net.Port(config.Port),
|
||||
Network: net.Network_TCP,
|
||||
},
|
||||
}
|
||||
if C.Contains(shadowaead_2022.List, config.Method) {
|
||||
if config.Key == "" {
|
||||
return nil, errors.New("missing psk")
|
||||
}
|
||||
method, err := shadowaead_2022.NewWithPassword(config.Method, config.Key, nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("create method").Base(err)
|
||||
}
|
||||
o.method = method
|
||||
} else {
|
||||
return nil, errors.New("unknown method ", config.Method)
|
||||
}
|
||||
return o, nil
|
||||
method: method,
|
||||
pskList: pskList,
|
||||
finalPSK: finalPSK,
|
||||
udpCodec: udpCodec,
|
||||
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error {
|
||||
var inboundConn net.Conn
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
if inbound != nil {
|
||||
inboundConn = inbound.Conn
|
||||
}
|
||||
|
||||
outbounds := session.OutboundsFromContext(ctx)
|
||||
ob := outbounds[len(outbounds)-1]
|
||||
if !ob.Target.IsValid() {
|
||||
@@ -78,70 +82,123 @@ func (o *Outbound) Process(ctx context.Context, link *transport.Link, dialer int
|
||||
|
||||
serverDestination := o.server
|
||||
serverDestination.Network = network
|
||||
connection, err := dialer.Dial(ctx, serverDestination)
|
||||
if err != nil {
|
||||
return errors.New("failed to connect to server").Base(err)
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
var conn net.Conn
|
||||
if err := retry.ExponentialBackoff(5, 100).On(func() error {
|
||||
rawConn, err := dialer.Dial(ctx, serverDestination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn = rawConn
|
||||
return nil
|
||||
}); err != nil {
|
||||
return errors.New("failed to find an available destination").Base(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var newCtx context.Context
|
||||
var newCancel context.CancelFunc
|
||||
if session.TimeoutOnlyFromContext(ctx) {
|
||||
ctx, _ = context.WithCancel(context.Background())
|
||||
newCtx, newCancel = context.WithCancel(context.Background())
|
||||
}
|
||||
|
||||
sessionPolicy := o.policyManager.ForLevel(0)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
timer := signal.CancelAfterInactivity(ctx, func() {
|
||||
cancel()
|
||||
if newCancel != nil {
|
||||
newCancel()
|
||||
}
|
||||
}, sessionPolicy.Timeouts.ConnectionIdle)
|
||||
|
||||
ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer)
|
||||
|
||||
if newCtx != nil {
|
||||
ctx = newCtx
|
||||
}
|
||||
|
||||
if network == net.Network_TCP {
|
||||
serverConn := o.method.DialEarlyConn(connection, singbridge.ToSocksaddr(destination))
|
||||
var handshake bool
|
||||
if timeoutReader, isTimeoutReader := link.Reader.(buf.TimeoutReader); isTimeoutReader {
|
||||
mb, err := timeoutReader.ReadMultiBufferTimeout(time.Millisecond * 100)
|
||||
if err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout {
|
||||
return errors.New("read payload").Base(err)
|
||||
}
|
||||
payload := B.New()
|
||||
for {
|
||||
payload.Reset()
|
||||
nb, n := buf.SplitBytes(mb, payload.FreeBytes())
|
||||
if n > 0 {
|
||||
payload.Truncate(n)
|
||||
_, err = serverConn.Write(payload.Bytes())
|
||||
if err != nil {
|
||||
payload.Release()
|
||||
return errors.New("write payload").Base(err)
|
||||
}
|
||||
handshake = true
|
||||
}
|
||||
if nb.IsEmpty() {
|
||||
break
|
||||
}
|
||||
mb = nb
|
||||
}
|
||||
payload.Release()
|
||||
}
|
||||
if !handshake {
|
||||
_, err = serverConn.Write(nil)
|
||||
if err != nil {
|
||||
return errors.New("client handshake").Base(err)
|
||||
}
|
||||
}
|
||||
return singbridge.CopyConn(ctx, inboundConn, link, serverConn)
|
||||
} else {
|
||||
var packetConn N.PacketConn
|
||||
if pc, isPacketConn := inboundConn.(N.PacketConn); isPacketConn {
|
||||
packetConn = pc
|
||||
} else if nc, isNetPacket := inboundConn.(net.PacketConn); isNetPacket {
|
||||
packetConn = bufio.NewPacketConn(nc)
|
||||
} else {
|
||||
packetConn = &singbridge.PacketConnWrapper{
|
||||
Reader: link.Reader,
|
||||
Writer: link.Writer,
|
||||
Conn: inboundConn,
|
||||
Dest: destination,
|
||||
T: signal.CancelAfterInactivity(ctx, func() {
|
||||
common.Interrupt(link.Reader)
|
||||
}, 300*time.Second),
|
||||
}
|
||||
var clientSalt [32]byte
|
||||
clientSaltSlice := clientSalt[:o.method.KeySaltLength]
|
||||
if _, err := io.ReadFull(rand.Reader, clientSaltSlice); err != nil {
|
||||
return errors.New("failed to generate client salt").Base(err)
|
||||
}
|
||||
|
||||
serverConn := o.method.DialPacketConn(connection)
|
||||
return singbridge.ReturnError(bufio.CopyPacketConn(ctx, packetConn, serverConn))
|
||||
requestDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
|
||||
bufferedWriter := buf.NewBufferedWriter(buf.NewWriter(conn))
|
||||
bodyWriter, err := WriteTCPRequest(bufferedWriter, o.method, o.pskList, destination, clientSaltSlice, nil)
|
||||
if err != nil {
|
||||
return errors.New("failed to write request").Base(err)
|
||||
}
|
||||
|
||||
if err = buf.CopyOnceTimeout(link.Reader, bodyWriter, time.Millisecond*100); err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout {
|
||||
return errors.New("failed to write A request payload").Base(err)
|
||||
}
|
||||
|
||||
if err := bufferedWriter.SetBuffered(false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return buf.Copy(link.Reader, bodyWriter, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||
|
||||
responseReader, err := ReadTCPResponse(conn, o.method, o.finalPSK, clientSaltSlice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return buf.Copy(responseReader, link.Writer, buf.UpdateActivity(timer))
|
||||
}
|
||||
|
||||
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
|
||||
if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
|
||||
return errors.New("connection ends").Base(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if network == net.Network_UDP {
|
||||
requestDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly)
|
||||
|
||||
writer := &UDPWriter{
|
||||
Writer: conn,
|
||||
Destination: destination,
|
||||
Codec: o.udpCodec,
|
||||
}
|
||||
|
||||
if err := buf.Copy(link.Reader, writer, buf.UpdateActivity(timer)); err != nil {
|
||||
return errors.New("failed to transport all UDP request").Base(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
responseDone := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||
|
||||
reader := &UDPReader{
|
||||
Reader: conn,
|
||||
Codec: o.udpCodec,
|
||||
}
|
||||
|
||||
if err := buf.Copy(reader, link.Writer, buf.UpdateActivity(timer)); err != nil {
|
||||
return errors.New("failed to transport all UDP response").Base(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer))
|
||||
if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil {
|
||||
return errors.New("connection ends").Base(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("unsupported network: ", network)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math"
|
||||
mrand "math/rand/v2"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
)
|
||||
|
||||
type UDPCodec struct {
|
||||
method *CipherMethod
|
||||
psk []byte
|
||||
blockCipher cipher.Block
|
||||
chachaCipher cipher.AEAD
|
||||
clientBodyCipher cipher.AEAD
|
||||
clientSessionID uint64
|
||||
nextPacketID atomic.Uint64
|
||||
sessions *UDPSessionManager
|
||||
}
|
||||
|
||||
type (
|
||||
UDPPacketCodec = UDPCodec
|
||||
UDPServerCodec = UDPCodec
|
||||
)
|
||||
|
||||
func newUDPCodec(method *CipherMethod, psk []byte) (*UDPCodec, error) {
|
||||
c := &UDPCodec{
|
||||
method: method,
|
||||
psk: psk,
|
||||
}
|
||||
var err error
|
||||
if method.IsChaCha {
|
||||
c.chachaCipher, err = method.NewUDPCipher(psk)
|
||||
} else {
|
||||
c.blockCipher, err = method.NewBlock(psk)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func NewUDPPacketCodec(method *CipherMethod, psk []byte) (*UDPCodec, error) {
|
||||
c, err := newUDPCodec(method, psk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sessID [8]byte
|
||||
if _, err := io.ReadFull(rand.Reader, sessID[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.clientSessionID = binary.BigEndian.Uint64(sessID[:])
|
||||
|
||||
if !method.IsChaCha {
|
||||
clientBodyKey := DeriveSessionSubKey(psk, sessID[:], method.KeySaltLength)
|
||||
c.clientBodyCipher, err = method.NewAEAD(clientBodyKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func NewUDPServerCodec(method *CipherMethod, psk []byte, sessionTimeout time.Duration) (*UDPCodec, error) {
|
||||
c, err := newUDPCodec(method, psk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.sessions = NewUDPSessionManager(sessionTimeout)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *UDPCodec) EncodeClientPacket(dest net.Destination, payload []byte) (*buf.Buffer, error) {
|
||||
packetID := c.nextPacketID.Add(1)
|
||||
sessID := c.clientSessionID
|
||||
|
||||
// Padding determination (e.g. DNS port 53 disguise)
|
||||
var paddingLen int
|
||||
if dest.Port == 53 && len(payload) < MaxPaddingLength {
|
||||
paddingLen = mrand.IntN(MaxPaddingLength-len(payload)) + 1
|
||||
}
|
||||
|
||||
addrPortLen := AddrPortLength(dest)
|
||||
|
||||
if c.method.IsChaCha {
|
||||
// ChaCha20 mode: 24-byte nonce + plaintext header (27B) + padding + dest + payload + AEAD tag (16B)
|
||||
totalLen := PacketNonceSize + 27 + paddingLen + addrPortLen + len(payload) + AEADTagSize
|
||||
if totalLen > buf.Size {
|
||||
return nil, ErrPacketTooLarge
|
||||
}
|
||||
|
||||
outBuf := buf.New()
|
||||
|
||||
var nonce [PacketNonceSize]byte
|
||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||
outBuf.Release()
|
||||
return nil, err
|
||||
}
|
||||
outBuf.Write(nonce[:])
|
||||
|
||||
var hdr [16 + 1 + 8 + 2]byte
|
||||
binary.BigEndian.PutUint64(hdr[0:8], sessID)
|
||||
binary.BigEndian.PutUint64(hdr[8:16], packetID)
|
||||
hdr[16] = HeaderTypeClient
|
||||
binary.BigEndian.PutUint64(hdr[17:25], uint64(time.Now().Unix()))
|
||||
binary.BigEndian.PutUint16(hdr[25:27], uint16(paddingLen))
|
||||
outBuf.Write(hdr[:])
|
||||
if paddingLen > 0 {
|
||||
outBuf.Write(zeroPadding[:paddingLen])
|
||||
}
|
||||
|
||||
if err := WriteAddressPort(outBuf, dest); err != nil {
|
||||
outBuf.Release()
|
||||
return nil, err
|
||||
}
|
||||
outBuf.Write(payload)
|
||||
|
||||
plainBytes := outBuf.Bytes()[PacketNonceSize:]
|
||||
outBuf.Extend(int32(c.chachaCipher.Overhead()))
|
||||
c.chachaCipher.Seal(plainBytes[:0], nonce[:], plainBytes, nil)
|
||||
return outBuf, nil
|
||||
}
|
||||
|
||||
// AES mode:
|
||||
// 16B Encrypted Header + (11B header + padding + dest + payload + 16B AEAD tag)
|
||||
totalLen := 16 + 11 + paddingLen + addrPortLen + len(payload) + AEADTagSize
|
||||
if totalLen > buf.Size {
|
||||
return nil, ErrPacketTooLarge
|
||||
}
|
||||
|
||||
outBuf := buf.New()
|
||||
|
||||
var rawHeader [16]byte
|
||||
binary.BigEndian.PutUint64(rawHeader[:8], sessID)
|
||||
binary.BigEndian.PutUint64(rawHeader[8:16], packetID)
|
||||
|
||||
var encryptedHeader [16]byte
|
||||
c.blockCipher.Encrypt(encryptedHeader[:], rawHeader[:])
|
||||
outBuf.Write(encryptedHeader[:])
|
||||
|
||||
bodyAead := c.clientBodyCipher
|
||||
|
||||
var hdr [1 + 8 + 2]byte
|
||||
hdr[0] = HeaderTypeClient
|
||||
binary.BigEndian.PutUint64(hdr[1:9], uint64(time.Now().Unix()))
|
||||
binary.BigEndian.PutUint16(hdr[9:11], uint16(paddingLen))
|
||||
outBuf.Write(hdr[:])
|
||||
if paddingLen > 0 {
|
||||
outBuf.Write(zeroPadding[:paddingLen])
|
||||
}
|
||||
|
||||
if err := WriteAddressPort(outBuf, dest); err != nil {
|
||||
outBuf.Release()
|
||||
return nil, err
|
||||
}
|
||||
outBuf.Write(payload)
|
||||
|
||||
plainBytes := outBuf.Bytes()[16:]
|
||||
bodyNonce := rawHeader[4:16]
|
||||
outBuf.Extend(int32(bodyAead.Overhead()))
|
||||
bodyAead.Seal(plainBytes[:0], bodyNonce, plainBytes, nil)
|
||||
return outBuf, nil
|
||||
}
|
||||
|
||||
type DecodedUDPPacket struct {
|
||||
SessionID uint64
|
||||
PacketID uint64
|
||||
HeaderType byte
|
||||
Timestamp uint64
|
||||
Destination net.Destination
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func parseAddressPort(data []byte) (net.Destination, int, error) {
|
||||
if len(data) < 1 {
|
||||
return net.Destination{}, 0, ErrPacketTooShort
|
||||
}
|
||||
switch data[0] {
|
||||
case 1: // IPv4
|
||||
if len(data) < 1+4+2 {
|
||||
return net.Destination{}, 0, ErrPacketTooShort
|
||||
}
|
||||
ip := net.IPAddress(data[1:5])
|
||||
port := binary.BigEndian.Uint16(data[5:7])
|
||||
return net.UDPDestination(ip, net.Port(port)), 7, nil
|
||||
case 4: // IPv6
|
||||
if len(data) < 1+16+2 {
|
||||
return net.Destination{}, 0, ErrPacketTooShort
|
||||
}
|
||||
ip := net.IPAddress(data[1:17])
|
||||
port := binary.BigEndian.Uint16(data[17:19])
|
||||
return net.UDPDestination(ip, net.Port(port)), 19, nil
|
||||
case 3: // Domain
|
||||
if len(data) < 2 {
|
||||
return net.Destination{}, 0, ErrPacketTooShort
|
||||
}
|
||||
domainLen := int(data[1])
|
||||
if len(data) < 2+domainLen+2 {
|
||||
return net.Destination{}, 0, ErrPacketTooShort
|
||||
}
|
||||
domain := string(data[2 : 2+domainLen])
|
||||
port := binary.BigEndian.Uint16(data[2+domainLen : 2+domainLen+2])
|
||||
return net.UDPDestination(net.DomainAddress(domain), net.Port(port)), 2 + domainLen + 2, nil
|
||||
default:
|
||||
return net.Destination{}, 0, errors.New("unknown address type")
|
||||
}
|
||||
}
|
||||
|
||||
func parsePlainUDPPacket(sessionID, packetID uint64, bodyPlain []byte) (DecodedUDPPacket, error) {
|
||||
if len(bodyPlain) < 1+8+2 {
|
||||
return DecodedUDPPacket{}, ErrPacketTooShort
|
||||
}
|
||||
|
||||
headerType := bodyPlain[0]
|
||||
epoch := binary.BigEndian.Uint64(bodyPlain[1:9])
|
||||
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch))))
|
||||
if diff > 30 {
|
||||
return DecodedUDPPacket{}, ErrBadTimestamp
|
||||
}
|
||||
|
||||
offset := 9
|
||||
if headerType == HeaderTypeServer {
|
||||
if len(bodyPlain) < offset+8+2 {
|
||||
return DecodedUDPPacket{}, ErrPacketTooShort
|
||||
}
|
||||
offset += 8 // skip clientSessionID
|
||||
}
|
||||
|
||||
paddingLen := int(binary.BigEndian.Uint16(bodyPlain[offset : offset+2]))
|
||||
offset += 2
|
||||
|
||||
if len(bodyPlain) < offset+paddingLen {
|
||||
return DecodedUDPPacket{}, ErrNoPadding
|
||||
}
|
||||
offset += paddingLen
|
||||
|
||||
dest, addrLen, err := parseAddressPort(bodyPlain[offset:])
|
||||
if err != nil {
|
||||
return DecodedUDPPacket{}, err
|
||||
}
|
||||
payload := bodyPlain[offset+addrLen:]
|
||||
|
||||
return DecodedUDPPacket{
|
||||
SessionID: sessionID,
|
||||
PacketID: packetID,
|
||||
HeaderType: headerType,
|
||||
Timestamp: epoch,
|
||||
Destination: dest,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *UDPCodec) DecodePacket(data []byte) (DecodedUDPPacket, error) {
|
||||
if len(data) < PacketMinimalHeaderSize {
|
||||
return DecodedUDPPacket{}, ErrPacketTooShort
|
||||
}
|
||||
|
||||
if c.method.IsChaCha {
|
||||
if len(data) < PacketNonceSize+AEADTagSize {
|
||||
return DecodedUDPPacket{}, ErrPacketTooShort
|
||||
}
|
||||
nonce := data[:PacketNonceSize]
|
||||
ciphertext := data[PacketNonceSize:]
|
||||
plain, err := c.chachaCipher.Open(ciphertext[:0], nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return DecodedUDPPacket{}, errors.New("failed to decrypt chacha udp packet").Base(err)
|
||||
}
|
||||
if len(plain) < 16+1+8+2 {
|
||||
return DecodedUDPPacket{}, ErrPacketTooShort
|
||||
}
|
||||
|
||||
sessionID := binary.BigEndian.Uint64(plain[:8])
|
||||
packetID := binary.BigEndian.Uint64(plain[8:16])
|
||||
|
||||
if c.sessions != nil {
|
||||
sessionItem := c.sessions.GetOrCreate(sessionID)
|
||||
sessionItem.Lock()
|
||||
if !sessionItem.Window.CheckAndAdd(packetID) {
|
||||
sessionItem.Unlock()
|
||||
return DecodedUDPPacket{}, ErrPacketIdNotUnique
|
||||
}
|
||||
sessionItem.Unlock()
|
||||
}
|
||||
|
||||
return parsePlainUDPPacket(sessionID, packetID, plain[16:])
|
||||
}
|
||||
|
||||
// AES mode
|
||||
var rawHeader [16]byte
|
||||
c.blockCipher.Decrypt(rawHeader[:], data[:16])
|
||||
sessionID := binary.BigEndian.Uint64(rawHeader[:8])
|
||||
packetID := binary.BigEndian.Uint64(rawHeader[8:16])
|
||||
|
||||
var bodyAead cipher.AEAD
|
||||
var sessionItem *ServerUDPSession
|
||||
|
||||
if c.sessions != nil {
|
||||
sessionItem = c.sessions.GetOrCreate(sessionID)
|
||||
sessionItem.Lock()
|
||||
if !sessionItem.Window.Check(packetID) {
|
||||
sessionItem.Unlock()
|
||||
return DecodedUDPPacket{}, ErrPacketIdNotUnique
|
||||
}
|
||||
sessionItem.Unlock()
|
||||
|
||||
bodyAead = sessionItem.GetRemoteCipher()
|
||||
if bodyAead == nil {
|
||||
bodyKey := DeriveSessionSubKey(c.psk, rawHeader[:8], c.method.KeySaltLength)
|
||||
var err error
|
||||
bodyAead, err = c.method.NewAEAD(bodyKey)
|
||||
if err != nil {
|
||||
return DecodedUDPPacket{}, err
|
||||
}
|
||||
sessionItem.SetRemoteCipher(bodyAead)
|
||||
}
|
||||
} else {
|
||||
bodyKey := DeriveSessionSubKey(c.psk, rawHeader[:8], c.method.KeySaltLength)
|
||||
var err error
|
||||
bodyAead, err = c.method.NewAEAD(bodyKey)
|
||||
if err != nil {
|
||||
return DecodedUDPPacket{}, err
|
||||
}
|
||||
}
|
||||
|
||||
bodyNonce := rawHeader[4:16]
|
||||
bodyCipher := data[16:]
|
||||
bodyPlain, err := bodyAead.Open(bodyCipher[:0], bodyNonce, bodyCipher, nil)
|
||||
if err != nil {
|
||||
return DecodedUDPPacket{}, errors.New("failed to decrypt aes udp body").Base(err)
|
||||
}
|
||||
|
||||
if sessionItem != nil {
|
||||
sessionItem.Lock()
|
||||
sessionItem.Window.Add(packetID)
|
||||
sessionItem.Unlock()
|
||||
}
|
||||
|
||||
return parsePlainUDPPacket(sessionID, packetID, bodyPlain)
|
||||
}
|
||||
|
||||
func (s *ServerUDPSession) EnsureServerState(method *CipherMethod, headerBlock cipher.Block, chachaCipher cipher.AEAD, psk []byte) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
if s.ServerSessionID != 0 {
|
||||
return nil
|
||||
}
|
||||
var sidBuf [8]byte
|
||||
for {
|
||||
if _, err := io.ReadFull(rand.Reader, sidBuf[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
s.ServerSessionID = binary.BigEndian.Uint64(sidBuf[:])
|
||||
if s.ServerSessionID != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if method.IsChaCha {
|
||||
s.ServerChaCha = chachaCipher
|
||||
} else {
|
||||
s.ServerBlockCipher = headerBlock
|
||||
bodyKey := DeriveSessionSubKey(psk, sidBuf[:], method.KeySaltLength)
|
||||
bodyAead, err := method.NewAEAD(bodyKey)
|
||||
if err != nil {
|
||||
s.ServerSessionID = 0
|
||||
return err
|
||||
}
|
||||
s.ServerCipher = bodyAead
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ServerUDPSession) EncodeServerPacket(method *CipherMethod, clientSessionID uint64, dest net.Destination, payload []byte) ([]byte, error) {
|
||||
serverSessionID := s.ServerSessionID
|
||||
serverPacketID := s.ServerPacketID.Add(1)
|
||||
|
||||
if method.IsChaCha {
|
||||
var nonce [PacketNonceSize]byte
|
||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plainBuf := buf.New()
|
||||
defer plainBuf.Release()
|
||||
|
||||
var hdr [16 + 1 + 8 + 8 + 2]byte
|
||||
binary.BigEndian.PutUint64(hdr[0:8], serverSessionID)
|
||||
binary.BigEndian.PutUint64(hdr[8:16], serverPacketID)
|
||||
hdr[16] = HeaderTypeServer
|
||||
binary.BigEndian.PutUint64(hdr[17:25], uint64(time.Now().Unix()))
|
||||
binary.BigEndian.PutUint64(hdr[25:33], clientSessionID)
|
||||
binary.BigEndian.PutUint16(hdr[33:35], 0)
|
||||
plainBuf.Write(hdr[:])
|
||||
|
||||
if err := WriteAddressPort(plainBuf, dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plainBuf.Write(payload)
|
||||
|
||||
sealed := s.ServerChaCha.Seal(nil, nonce[:], plainBuf.Bytes(), nil)
|
||||
res := make([]byte, PacketNonceSize+len(sealed))
|
||||
copy(res[:PacketNonceSize], nonce[:])
|
||||
copy(res[PacketNonceSize:], sealed)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// AES mode
|
||||
var rawHeader [16]byte
|
||||
binary.BigEndian.PutUint64(rawHeader[:8], serverSessionID)
|
||||
binary.BigEndian.PutUint64(rawHeader[8:16], serverPacketID)
|
||||
|
||||
var encryptedHeader [16]byte
|
||||
s.ServerBlockCipher.Encrypt(encryptedHeader[:], rawHeader[:])
|
||||
|
||||
bodyBuf := buf.New()
|
||||
defer bodyBuf.Release()
|
||||
|
||||
var hdr [1 + 8 + 8 + 2]byte
|
||||
hdr[0] = HeaderTypeServer
|
||||
binary.BigEndian.PutUint64(hdr[1:9], uint64(time.Now().Unix()))
|
||||
binary.BigEndian.PutUint64(hdr[9:17], clientSessionID)
|
||||
binary.BigEndian.PutUint16(hdr[17:19], 0)
|
||||
bodyBuf.Write(hdr[:])
|
||||
|
||||
if err := WriteAddressPort(bodyBuf, dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyBuf.Write(payload)
|
||||
|
||||
bodyNonce := rawHeader[4:16]
|
||||
sealedBody := s.ServerCipher.Seal(nil, bodyNonce, bodyBuf.Bytes(), nil)
|
||||
|
||||
res := make([]byte, 16+len(sealedBody))
|
||||
copy(res[:16], encryptedHeader[:])
|
||||
copy(res[16:], sealedBody)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *UDPCodec) EncodeServerPacket(clientSessionID uint64, dest net.Destination, payload []byte) ([]byte, error) {
|
||||
sessionItem := c.sessions.GetOrCreate(clientSessionID)
|
||||
if err := sessionItem.EnsureServerState(c.method, c.blockCipher, c.chachaCipher, c.psk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sessionItem.EncodeServerPacket(c.method, clientSessionID, dest, payload)
|
||||
}
|
||||
|
||||
type UDPWriter struct {
|
||||
Writer io.Writer
|
||||
Destination net.Destination
|
||||
Codec *UDPPacketCodec
|
||||
}
|
||||
|
||||
func (w *UDPWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
for {
|
||||
mb2, b := buf.SplitFirst(mb)
|
||||
mb = mb2
|
||||
if b == nil {
|
||||
break
|
||||
}
|
||||
dest := w.Destination
|
||||
if b.UDP != nil {
|
||||
dest = *b.UDP
|
||||
}
|
||||
pktBuf, err := w.Codec.EncodeClientPacket(dest, b.Bytes())
|
||||
b.Release()
|
||||
if err != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return err
|
||||
}
|
||||
_, writeErr := w.Writer.Write(pktBuf.Bytes())
|
||||
pktBuf.Release()
|
||||
if writeErr != nil {
|
||||
buf.ReleaseMulti(mb)
|
||||
return writeErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type UDPReader struct {
|
||||
Reader io.Reader
|
||||
Codec *UDPPacketCodec
|
||||
}
|
||||
|
||||
func (r *UDPReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||
for {
|
||||
buffer := buf.New()
|
||||
_, err := buffer.ReadFrom(r.Reader)
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decoded, err := r.Codec.DecodePacket(buffer.Bytes())
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
continue
|
||||
}
|
||||
buffer.Clear()
|
||||
buffer.Write(decoded.Payload)
|
||||
dest := decoded.Destination
|
||||
buffer.UDP = &dest
|
||||
return buf.MultiBuffer{buffer}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package shadowsocks_2022_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
gonet "net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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/features/routing"
|
||||
. "github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
"lukechampine.com/blake3"
|
||||
)
|
||||
|
||||
// encodeRelayClientUDPPacket encodes a Shadowsocks-2022 UDP packet with 1 layer of EIH (Relay)
|
||||
func encodeRelayClientUDPPacket(relayKey, destKey []byte, sessionID, packetID uint64, dest net.Destination, payload []byte) ([]byte, error) {
|
||||
method, err := GetCipherMethod(MethodAES128GCM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
relayBlock, err := method.NewBlock(relayKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 1. Plain packet header: sessionID (8B) + packetID (8B)
|
||||
var rawHeader [16]byte
|
||||
binary.BigEndian.PutUint64(rawHeader[:8], sessionID)
|
||||
binary.BigEndian.PutUint64(rawHeader[8:16], packetID)
|
||||
|
||||
// Encrypt packetHeader under relayKey
|
||||
var encPacketHeader [16]byte
|
||||
relayBlock.Encrypt(encPacketHeader[:], rawHeader[:])
|
||||
|
||||
// 2. EI Header: blake3(destKey)[:16] ^ rawHeader
|
||||
var destHash [16]byte
|
||||
hash512 := blake3.Sum512(destKey)
|
||||
copy(destHash[:], hash512[:16])
|
||||
|
||||
var eiHeader [16]byte
|
||||
for i := 0; i < 16; i++ {
|
||||
eiHeader[i] = destHash[i] ^ rawHeader[i]
|
||||
}
|
||||
var encEIHeader [16]byte
|
||||
relayBlock.Encrypt(encEIHeader[:], eiHeader[:])
|
||||
|
||||
// 3. Payload under destination server's AEAD
|
||||
bodyKey := DeriveSessionSubKey(destKey, rawHeader[:8], 16)
|
||||
bodyAead, err := method.NewAEAD(bodyKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyNonce := rawHeader[4:16]
|
||||
|
||||
outBuf := buf.New()
|
||||
defer outBuf.Release()
|
||||
|
||||
// VarHeader: client type (1) + timestamp (8) + paddingLen (2) + padding + dest + payload
|
||||
var hdr [1 + 8 + 2]byte
|
||||
hdr[0] = HeaderTypeClient
|
||||
binary.BigEndian.PutUint64(hdr[1:9], uint64(time.Now().Unix()))
|
||||
binary.BigEndian.PutUint16(hdr[9:11], 0)
|
||||
outBuf.Write(hdr[:])
|
||||
|
||||
if err := WriteAddressPort(outBuf, dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outBuf.Write(payload)
|
||||
|
||||
plainBytes := outBuf.Bytes()
|
||||
outBuf.Extend(int32(bodyAead.Overhead()))
|
||||
bodyAead.Seal(plainBytes[:0], bodyNonce, plainBytes, nil)
|
||||
|
||||
// Full packet: encPacketHeader (16B) + encEIHeader (16B) + sealedBody
|
||||
packet := make([]byte, 0, 32+outBuf.Len())
|
||||
packet = append(packet, encPacketHeader[:]...)
|
||||
packet = append(packet, encEIHeader[:]...)
|
||||
packet = append(packet, outBuf.Bytes()...)
|
||||
return packet, nil
|
||||
}
|
||||
|
||||
func TestRelayUDPSessionStabilityAndDispatch(t *testing.T) {
|
||||
relayKey := []byte("0123456789abcdef")
|
||||
destKey := []byte("fedcba9876543210")
|
||||
relayKeyB64 := base64.StdEncoding.EncodeToString(relayKey)
|
||||
destKeyB64 := base64.StdEncoding.EncodeToString(destKey)
|
||||
|
||||
config := &RelayServerConfig{
|
||||
Method: MethodAES128GCM,
|
||||
Key: relayKeyB64,
|
||||
Destinations: []*RelayDestination{
|
||||
{
|
||||
Key: destKeyB64,
|
||||
Address: &net.IPOrDomain{Address: &net.IPOrDomain_Ip{Ip: []byte{127, 0, 0, 1}}},
|
||||
Port: 8388,
|
||||
Email: "dest@example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
inbound, err := NewRelayServer(newTestContext(), config)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create RelayServer: %v", err)
|
||||
}
|
||||
|
||||
sessionID := uint64(0x1122334455667788)
|
||||
dest := net.UDPDestination(net.LocalHostIP, 8388)
|
||||
|
||||
pkt1, err := encodeRelayClientUDPPacket(relayKey, destKey, sessionID, 1, dest, []byte("xray packet 1"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode pkt1: %v", err)
|
||||
}
|
||||
pkt2, err := encodeRelayClientUDPPacket(relayKey, destKey, sessionID, 2, dest, []byte("xray packet 2"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to encode pkt2: %v", err)
|
||||
}
|
||||
|
||||
var dispatchCount atomic.Int32
|
||||
var receivedPackets [][]byte
|
||||
var mu sync.Mutex
|
||||
|
||||
disp := &dummyDispatcher{
|
||||
onDispatch: func(ctx context.Context, d net.Destination) (*transport.Link, error) {
|
||||
dispatchCount.Add(1)
|
||||
linkR, linkW := gonet.Pipe()
|
||||
t.Cleanup(func() {
|
||||
linkW.Close()
|
||||
linkR.Close()
|
||||
})
|
||||
link := &transport.Link{
|
||||
Reader: buf.NewReader(linkR),
|
||||
Writer: &customWriter{
|
||||
write: func(mb buf.MultiBuffer) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for _, b := range mb {
|
||||
cpy := make([]byte, b.Len())
|
||||
copy(cpy, b.Bytes())
|
||||
receivedPackets = append(receivedPackets, cpy)
|
||||
b.Release()
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
return link, nil
|
||||
},
|
||||
}
|
||||
|
||||
clientConn, serverConn := gonet.Pipe()
|
||||
defer clientConn.Close()
|
||||
defer serverConn.Close()
|
||||
|
||||
inboundConn := &dummyStatConn{Conn: serverConn}
|
||||
ctx, cancel := context.WithCancel(newTestContext())
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
_ = inbound.Process(ctx, net.Network_UDP, inboundConn, disp)
|
||||
}()
|
||||
|
||||
// Send Packet 1
|
||||
_, err = clientConn.Write(pkt1)
|
||||
if err != nil {
|
||||
t.Fatalf("write pkt1 failed: %v", err)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Send Packet 2 (same sessionID, packetID=2)
|
||||
_, err = clientConn.Write(pkt2)
|
||||
if err != nil {
|
||||
t.Fatalf("write pkt2 failed: %v", err)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Check dispatch count: For the SAME UDP session, Dispatch MUST be called exactly ONCE!
|
||||
if count := dispatchCount.Load(); count != 1 {
|
||||
t.Fatalf("CRITICAL BUG CONFIRMED: expected dispatchCount = 1 for same session, got %d (sessionID was corrupted by Encrypt!)", count)
|
||||
}
|
||||
|
||||
// Verify downstream destination can decode both packets
|
||||
method, err := GetCipherMethod(MethodAES128GCM)
|
||||
common.Must(err)
|
||||
destCodec, err := NewUDPServerCodec(method, destKey, 300*time.Second)
|
||||
common.Must(err)
|
||||
|
||||
mu.Lock()
|
||||
pkts := receivedPackets
|
||||
mu.Unlock()
|
||||
|
||||
if len(pkts) != 2 {
|
||||
t.Fatalf("expected 2 received packets at destination, got %d", len(pkts))
|
||||
}
|
||||
|
||||
dec1, err := destCodec.DecodePacket(pkts[0])
|
||||
if err != nil {
|
||||
t.Fatalf("dest failed to decode packet 1: %v", err)
|
||||
}
|
||||
if dec1.SessionID != sessionID || dec1.PacketID != 1 || string(dec1.Payload) != "xray packet 1" {
|
||||
t.Fatalf("dec1 mismatch: sess=%x, pktID=%d, payload=%s", dec1.SessionID, dec1.PacketID, string(dec1.Payload))
|
||||
}
|
||||
|
||||
dec2, err := destCodec.DecodePacket(pkts[1])
|
||||
if err != nil {
|
||||
t.Fatalf("dest failed to decode packet 2: %v", err)
|
||||
}
|
||||
if dec2.SessionID != sessionID || dec2.PacketID != 2 || string(dec2.Payload) != "xray packet 2" {
|
||||
t.Fatalf("dec2 mismatch: sess=%x, pktID=%d, payload=%s", dec2.SessionID, dec2.PacketID, string(dec2.Payload))
|
||||
}
|
||||
}
|
||||
|
||||
type customWriter struct {
|
||||
write func(mb buf.MultiBuffer) error
|
||||
}
|
||||
|
||||
func (w *customWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
return w.write(mb)
|
||||
}
|
||||
|
||||
func (w *customWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *customWriter) Interrupt() {}
|
||||
|
||||
type dummyDispatcher struct {
|
||||
onDispatch func(ctx context.Context, dest net.Destination) (*transport.Link, error)
|
||||
}
|
||||
|
||||
func (d *dummyDispatcher) Dispatch(ctx context.Context, dest net.Destination) (*transport.Link, error) {
|
||||
if d.onDispatch != nil {
|
||||
return d.onDispatch(ctx, dest)
|
||||
}
|
||||
return nil, errors.New("not handled")
|
||||
}
|
||||
|
||||
func (d *dummyDispatcher) DispatchLink(ctx context.Context, dest net.Destination, link *transport.Link) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dummyDispatcher) Start() error { return nil }
|
||||
func (d *dummyDispatcher) Close() error { return nil }
|
||||
func (d *dummyDispatcher) Type() interface{} { return routing.DispatcherType() }
|
||||
|
||||
type dummyStatConn struct {
|
||||
gonet.Conn
|
||||
}
|
||||
|
||||
func (c *dummyStatConn) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||
b := buf.New()
|
||||
_, err := b.ReadFrom(c.Conn)
|
||||
return buf.MultiBuffer{b}, err
|
||||
}
|
||||
|
||||
func (c *dummyStatConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
defer buf.ReleaseMulti(mb)
|
||||
for _, b := range mb {
|
||||
if _, err := c.Conn.Write(b.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/protocol"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
swBlockBitLog = 6 // 1<<6 == 64 bits
|
||||
swBlockBits = 1 << swBlockBitLog // 64
|
||||
swRingBlocks = 1 << 7 // 128
|
||||
swBlockMask = swRingBlocks - 1 // 127
|
||||
swBitMask = swBlockBits - 1 // 63
|
||||
swSize = (swRingBlocks - 1) * swBlockBits // 8128
|
||||
)
|
||||
|
||||
type SlidingWindow struct {
|
||||
last uint64
|
||||
ring [swRingBlocks]uint64
|
||||
}
|
||||
|
||||
func (f *SlidingWindow) Reset() {
|
||||
*f = SlidingWindow{}
|
||||
}
|
||||
|
||||
func (f *SlidingWindow) Check(counter uint64) bool {
|
||||
switch {
|
||||
case counter > f.last:
|
||||
return true
|
||||
case f.last-counter > swSize:
|
||||
return false
|
||||
}
|
||||
|
||||
blockIndex := (counter >> swBlockBitLog) & swBlockMask
|
||||
bitIndex := counter & swBitMask
|
||||
return (f.ring[blockIndex]>>bitIndex)&1 == 0
|
||||
}
|
||||
|
||||
func (f *SlidingWindow) Add(counter uint64) {
|
||||
blockIndex := counter >> swBlockBitLog
|
||||
|
||||
if counter > f.last {
|
||||
lastBlockIndex := f.last >> swBlockBitLog
|
||||
diff := int(blockIndex - lastBlockIndex)
|
||||
if diff > swRingBlocks {
|
||||
diff = swRingBlocks
|
||||
}
|
||||
|
||||
for i := 0; i < diff; i++ {
|
||||
lastBlockIndex = (lastBlockIndex + 1) & swBlockMask
|
||||
f.ring[lastBlockIndex] = 0
|
||||
}
|
||||
|
||||
f.last = counter
|
||||
}
|
||||
|
||||
blockIndex &= swBlockMask
|
||||
bitIndex := counter & swBitMask
|
||||
f.ring[blockIndex] |= 1 << bitIndex
|
||||
}
|
||||
|
||||
func (f *SlidingWindow) CheckAndAdd(counter uint64) bool {
|
||||
if !f.Check(counter) {
|
||||
return false
|
||||
}
|
||||
f.Add(counter)
|
||||
return true
|
||||
}
|
||||
|
||||
type ServerUDPSession struct {
|
||||
sync.Mutex
|
||||
SessionID uint64
|
||||
RemoteCipher atomic.Pointer[cipher.AEAD]
|
||||
Window SlidingWindow
|
||||
User *protocol.MemoryUser
|
||||
UserPSK []byte
|
||||
LastActive atomic.Int64 // Unix timestamp in seconds
|
||||
|
||||
ServerSessionID uint64
|
||||
ServerPacketID atomic.Uint64
|
||||
ServerCipher cipher.AEAD
|
||||
ServerBlockCipher cipher.Block
|
||||
ServerChaCha cipher.AEAD
|
||||
}
|
||||
|
||||
func (s *ServerUDPSession) GetRemoteCipher() cipher.AEAD {
|
||||
ptr := s.RemoteCipher.Load()
|
||||
if ptr == nil {
|
||||
return nil
|
||||
}
|
||||
return *ptr
|
||||
}
|
||||
|
||||
func (s *ServerUDPSession) SetRemoteCipher(c cipher.AEAD) {
|
||||
s.RemoteCipher.Store(&c)
|
||||
}
|
||||
|
||||
type UDPSessionManager struct {
|
||||
sessions *utils.TypedSyncMap[uint64, *ServerUDPSession]
|
||||
timeout time.Duration
|
||||
lastClean atomic.Int64 // Unix timestamp in seconds
|
||||
}
|
||||
|
||||
func NewUDPSessionManager(timeout time.Duration) *UDPSessionManager {
|
||||
return &UDPSessionManager{
|
||||
sessions: utils.NewTypedSyncMap[uint64, *ServerUDPSession](),
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *UDPSessionManager) GetOrCreate(sessionID uint64) *ServerUDPSession {
|
||||
now := time.Now().Unix()
|
||||
if s, ok := m.sessions.Load(sessionID); ok {
|
||||
s.LastActive.Store(now)
|
||||
return s
|
||||
}
|
||||
|
||||
s := &ServerUDPSession{
|
||||
SessionID: sessionID,
|
||||
}
|
||||
s.LastActive.Store(now)
|
||||
|
||||
actual, loaded := m.sessions.LoadOrStore(sessionID, s)
|
||||
if loaded {
|
||||
actual.LastActive.Store(now)
|
||||
return actual
|
||||
}
|
||||
|
||||
// Trigger cleanup if at least 30 seconds have passed since last cleanup
|
||||
last := m.lastClean.Load()
|
||||
if now-last > 30 && m.lastClean.CompareAndSwap(last, now) {
|
||||
go m.cleanup(now)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (m *UDPSessionManager) cleanup(now int64) {
|
||||
timeoutSec := int64(m.timeout.Seconds())
|
||||
if timeoutSec <= 0 {
|
||||
timeoutSec = 60
|
||||
}
|
||||
m.sessions.Range(func(k uint64, v *ServerUDPSession) bool {
|
||||
if now-v.LastActive.Load() > timeoutSec {
|
||||
m.sessions.Delete(k)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (m *UDPSessionManager) Delete(sessionID uint64) {
|
||||
m.sessions.Delete(sessionID)
|
||||
}
|
||||
@@ -1 +1,50 @@
|
||||
package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/signal"
|
||||
"github.com/xtls/xray-core/transport"
|
||||
)
|
||||
|
||||
type udpConnEntry struct {
|
||||
sync.Mutex
|
||||
link *transport.Link
|
||||
timer *signal.ActivityTimer
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
const (
|
||||
HeaderTypeClient = 0
|
||||
HeaderTypeServer = 1
|
||||
MaxPaddingLength = 900
|
||||
PacketNonceSize = 24
|
||||
MaxPacketSize = 65535
|
||||
RequestHeaderFixedChunkLength = 1 + 8 + 2 // Type (1B) + Timestamp (8B) + VarHeaderLen (2B)
|
||||
PacketMinimalHeaderSize = 30
|
||||
StreamNonceSize = 12
|
||||
AESBlockSize = 16
|
||||
AEADTagSize = 16
|
||||
)
|
||||
|
||||
var zeroPadding [MaxPaddingLength]byte
|
||||
|
||||
const (
|
||||
MethodAES128GCM = "2022-blake3-aes-128-gcm"
|
||||
MethodAES256GCM = "2022-blake3-aes-256-gcm"
|
||||
MethodChaCha20Poly1305 = "2022-blake3-chacha20-poly1305"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBadKey = errors.New("bad key")
|
||||
ErrBadHeaderType = errors.New("bad header type")
|
||||
ErrBadTimestamp = errors.New("bad timestamp")
|
||||
ErrSaltNotUnique = errors.New("salt not unique")
|
||||
ErrPacketIdNotUnique = errors.New("packet id not unique")
|
||||
ErrPacketTooShort = errors.New("packet too short")
|
||||
ErrPacketTooLarge = errors.New("packet too large")
|
||||
ErrNoPadding = errors.New("bad request: missing payload or padding")
|
||||
ErrInvalidRequest = errors.New("invalid request")
|
||||
)
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
package shadowsocks_2022_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
gonet "net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"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/protocol"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/core"
|
||||
. "github.com/xtls/xray-core/proxy/shadowsocks_2022"
|
||||
)
|
||||
|
||||
func newTestContext() context.Context {
|
||||
v, err := core.New(&core.Config{})
|
||||
common.Must(err)
|
||||
ctx := context.WithValue(context.Background(), core.XrayKey(1), v)
|
||||
ctx = session.ContextWithInbound(ctx, &session.Inbound{})
|
||||
return ctx
|
||||
}
|
||||
|
||||
func generateRandomKey(size int) string {
|
||||
b := make([]byte, size)
|
||||
_, _ = rand.Read(b)
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
func TestKDF(t *testing.T) {
|
||||
// Test ParseKey
|
||||
if _, err := ParseKey("", 16); err != ErrBadKey {
|
||||
t.Fatalf("expected ErrBadKey for empty key, got %v", err)
|
||||
}
|
||||
|
||||
shortKey := base64.StdEncoding.EncodeToString([]byte("short"))
|
||||
if _, err := ParseKey(shortKey, 16); err != ErrBadKey {
|
||||
t.Fatalf("expected ErrBadKey for short key, got %v", err)
|
||||
}
|
||||
|
||||
exactKey := []byte("0123456789abcdef")
|
||||
exactKeyB64 := base64.StdEncoding.EncodeToString(exactKey)
|
||||
normExact, err := ParseKey(exactKeyB64, 16)
|
||||
if err != nil || !bytes.Equal(normExact, exactKey) {
|
||||
t.Fatalf("unexpected parsed exact key: %v, err: %v", normExact, err)
|
||||
}
|
||||
|
||||
longKey := base64.StdEncoding.EncodeToString([]byte("0123456789abcdef_longer_key_for_testing"))
|
||||
if _, err := ParseKey(longKey, 16); err != ErrBadKey {
|
||||
t.Fatalf("expected ErrBadKey for long key, got %v", err)
|
||||
}
|
||||
|
||||
// Test Session Subkey determinism
|
||||
salt := []byte("random_salt_1234")
|
||||
k1 := DeriveSessionSubKey(normExact, salt, 16)
|
||||
k2 := DeriveSessionSubKey(normExact, salt, 16)
|
||||
if !bytes.Equal(k1, k2) {
|
||||
t.Fatal("DeriveSessionSubKey should be deterministic")
|
||||
}
|
||||
|
||||
// Identity subkey must differ from session subkey with same inputs
|
||||
idKey := DeriveIdentitySubKey(normExact, salt, 16)
|
||||
if bytes.Equal(k1, idKey) {
|
||||
t.Fatal("DeriveIdentitySubKey must differ from DeriveSessionSubKey")
|
||||
}
|
||||
|
||||
// User PSK hash
|
||||
h1 := DeriveUserPSKHash(normExact)
|
||||
h2 := DeriveUserPSKHash(normExact)
|
||||
if h1 != h2 {
|
||||
t.Fatal("DeriveUserPSKHash should be deterministic")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlidingWindow(t *testing.T) {
|
||||
var window SlidingWindow
|
||||
if !window.Check(1) {
|
||||
t.Fatal("packet 1 should be accepted")
|
||||
}
|
||||
window.Add(1)
|
||||
|
||||
if window.Check(1) {
|
||||
t.Fatal("duplicate packet 1 should be rejected")
|
||||
}
|
||||
|
||||
if !window.Check(100) {
|
||||
t.Fatal("packet 100 should be accepted")
|
||||
}
|
||||
window.Add(100)
|
||||
|
||||
if window.Check(100) {
|
||||
t.Fatal("duplicate packet 100 should be rejected")
|
||||
}
|
||||
|
||||
if !window.Check(50) {
|
||||
t.Fatal("out-of-order packet 50 within window should be accepted")
|
||||
}
|
||||
window.Add(50)
|
||||
if window.Check(50) {
|
||||
t.Fatal("duplicate packet 50 should be rejected")
|
||||
}
|
||||
|
||||
// Check packet far behind window (> 8128)
|
||||
window.Add(10000)
|
||||
if window.Check(1) {
|
||||
t.Fatal("packet 1 should be rejected as behind window")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPStream(t *testing.T) {
|
||||
methods := []struct {
|
||||
name string
|
||||
keySize int
|
||||
}{
|
||||
{MethodAES128GCM, 16},
|
||||
{MethodAES256GCM, 32},
|
||||
{MethodChaCha20Poly1305, 32},
|
||||
}
|
||||
|
||||
dest := net.TCPDestination(net.LocalHostIP, net.Port(8080))
|
||||
testPayload := []byte("Hello, Shadowsocks 2022 Native Implementation!")
|
||||
|
||||
for _, m := range methods {
|
||||
t.Run(m.name, func(t *testing.T) {
|
||||
rawKey := make([]byte, m.keySize)
|
||||
_, _ = rand.Read(rawKey)
|
||||
method, err := GetCipherMethod(m.name)
|
||||
common.Must(err)
|
||||
|
||||
clientConn, serverConn := gonet.Pipe()
|
||||
defer clientConn.Close()
|
||||
defer serverConn.Close()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
var receivedDest net.Destination
|
||||
var receivedPayload []byte
|
||||
|
||||
// Server goroutine
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
salt := make([]byte, method.KeySaltLength)
|
||||
_, err := io.ReadFull(serverConn, salt)
|
||||
common.Must(err)
|
||||
|
||||
sessionKey := DeriveSessionSubKey(rawKey, salt, method.KeySaltLength)
|
||||
aead, err := method.NewAEAD(sessionKey)
|
||||
common.Must(err)
|
||||
|
||||
reader := NewStreamReader(serverConn, aead)
|
||||
|
||||
// Read fixed chunk (11 + 16 bytes)
|
||||
var fixedBuf [RequestHeaderFixedChunkLength + AEADTagSize]byte
|
||||
_, err = io.ReadFull(serverConn, fixedBuf[:])
|
||||
common.Must(err)
|
||||
|
||||
plainFixed, err := aead.Open(fixedBuf[:0], reader.Nonce(), fixedBuf[:], nil)
|
||||
common.Must(err)
|
||||
IncreaseNonce(reader.Nonce())
|
||||
if plainFixed[0] != HeaderTypeClient {
|
||||
t.Errorf("expected client header type, got %d", plainFixed[0])
|
||||
}
|
||||
|
||||
// Read variable chunk
|
||||
varLen := int(plainFixed[9])<<8 | int(plainFixed[10])
|
||||
varBuf := make([]byte, varLen+AEADTagSize)
|
||||
_, err = io.ReadFull(serverConn, varBuf)
|
||||
common.Must(err)
|
||||
|
||||
plainVar, err := aead.Open(varBuf[:0], reader.Nonce(), varBuf, nil)
|
||||
common.Must(err)
|
||||
IncreaseNonce(reader.Nonce())
|
||||
|
||||
vBuf := buf.New()
|
||||
vBuf.Write(plainVar)
|
||||
receivedDest, err = ReadAddressPort(vBuf)
|
||||
common.Must(err)
|
||||
|
||||
// Skip padding
|
||||
var padBytes [2]byte
|
||||
_, _ = vBuf.Read(padBytes[:])
|
||||
padLen := int(padBytes[0])<<8 | int(padBytes[1])
|
||||
vBuf.Advance(int32(padLen))
|
||||
|
||||
receivedPayload = make([]byte, vBuf.Len())
|
||||
copy(receivedPayload, vBuf.Bytes())
|
||||
vBuf.Release()
|
||||
|
||||
// Server sends response handshake
|
||||
serverSalt := make([]byte, method.KeySaltLength)
|
||||
_, _ = rand.Read(serverSalt)
|
||||
respKey := DeriveSessionSubKey(rawKey, serverSalt, method.KeySaltLength)
|
||||
respAead, err := method.NewAEAD(respKey)
|
||||
writer := NewStreamWriter(serverConn, respAead)
|
||||
_, _ = serverConn.Write(serverSalt)
|
||||
|
||||
fixedResp := make([]byte, 1+8+method.KeySaltLength+2)
|
||||
fixedResp[0] = HeaderTypeServer
|
||||
binary.BigEndian.PutUint64(fixedResp[1:9], uint64(time.Now().Unix()))
|
||||
copy(fixedResp[9:9+method.KeySaltLength], salt)
|
||||
binary.BigEndian.PutUint16(fixedResp[9+method.KeySaltLength:11+method.KeySaltLength], 0)
|
||||
|
||||
fixedChunk := respAead.Seal(nil, writer.Nonce(), fixedResp, nil)
|
||||
IncreaseNonce(writer.Nonce())
|
||||
_, _ = serverConn.Write(fixedChunk)
|
||||
|
||||
// Echo stream data
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
common.Must(err)
|
||||
_ = writer.WriteMultiBuffer(mb)
|
||||
}()
|
||||
|
||||
// Client goroutine
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
clientSalt, writer, err := ClientHandshake(clientConn, method, [][]byte{rawKey}, dest, testPayload)
|
||||
common.Must(err)
|
||||
|
||||
reader, _, err := ClientVerifyServerResponse(clientConn, method, rawKey, clientSalt)
|
||||
common.Must(err)
|
||||
|
||||
// Send additional stream data
|
||||
streamData := []byte("stream chunk test")
|
||||
_ = writer.WriteChunk(streamData)
|
||||
|
||||
mb, err := reader.ReadMultiBuffer()
|
||||
common.Must(err)
|
||||
if !bytes.Equal(mb[0].Bytes(), streamData) {
|
||||
t.Errorf("echoed stream data mismatch: got %s, want %s", mb[0].Bytes(), streamData)
|
||||
}
|
||||
buf.ReleaseMulti(mb)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if receivedDest.NetAddr() != dest.NetAddr() {
|
||||
t.Errorf("destination mismatch: got %s, want %s", receivedDest.NetAddr(), dest.NetAddr())
|
||||
}
|
||||
if diff := cmp.Diff(receivedPayload, testPayload); diff != "" {
|
||||
t.Errorf("payload mismatch: %s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUDPCodec(t *testing.T) {
|
||||
methods := []string{
|
||||
MethodAES128GCM,
|
||||
MethodAES256GCM,
|
||||
MethodChaCha20Poly1305,
|
||||
}
|
||||
|
||||
dest := net.UDPDestination(net.LocalHostIP, net.Port(53))
|
||||
payload := []byte("DNS query payload")
|
||||
|
||||
for _, methodName := range methods {
|
||||
t.Run(methodName, func(t *testing.T) {
|
||||
method, err := GetCipherMethod(methodName)
|
||||
common.Must(err)
|
||||
|
||||
psk := make([]byte, method.KeySaltLength)
|
||||
_, _ = rand.Read(psk)
|
||||
|
||||
clientCodec, err := NewUDPPacketCodec(method, psk)
|
||||
common.Must(err)
|
||||
serverCodec, err := NewUDPServerCodec(method, psk, time.Minute)
|
||||
common.Must(err)
|
||||
|
||||
pktBuf, err := clientCodec.EncodeClientPacket(dest, payload)
|
||||
common.Must(err)
|
||||
defer pktBuf.Release()
|
||||
|
||||
rawCopy := make([]byte, pktBuf.Len())
|
||||
copy(rawCopy, pktBuf.Bytes())
|
||||
|
||||
decoded, err := serverCodec.DecodePacket(pktBuf.Bytes())
|
||||
common.Must(err)
|
||||
|
||||
if decoded.HeaderType != HeaderTypeClient {
|
||||
t.Errorf("expected header type %d, got %d", HeaderTypeClient, decoded.HeaderType)
|
||||
}
|
||||
if decoded.Destination.Port != dest.Port {
|
||||
t.Errorf("port mismatch: got %d, want %d", decoded.Destination.Port, dest.Port)
|
||||
}
|
||||
if !bytes.Equal(decoded.Payload, payload) {
|
||||
t.Errorf("payload mismatch: got %s, want %s", decoded.Payload, payload)
|
||||
}
|
||||
|
||||
// Replay same packet wire bytes should fail with ErrPacketIdNotUnique
|
||||
_, err = serverCodec.DecodePacket(rawCopy)
|
||||
if err != ErrPacketIdNotUnique {
|
||||
t.Fatalf("expected ErrPacketIdNotUnique on replay, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiUserManager(t *testing.T) {
|
||||
masterKey := generateRandomKey(16)
|
||||
userKey1 := generateRandomKey(16)
|
||||
userKey2 := generateRandomKey(16)
|
||||
|
||||
config := &MultiUserServerConfig{
|
||||
Method: MethodAES128GCM,
|
||||
Key: masterKey,
|
||||
Users: []*protocol.User{
|
||||
{
|
||||
Email: "user1@example.com",
|
||||
Account: serial.ToTypedMessage(&Account{Key: userKey1}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
inbound, err := NewMultiServer(newTestContext(), config)
|
||||
common.Must(err)
|
||||
|
||||
if inbound.GetUsersCount(context.Background()) != 1 {
|
||||
t.Fatalf("expected 1 user, got %d", inbound.GetUsersCount(context.Background()))
|
||||
}
|
||||
|
||||
u1 := inbound.GetUser(context.Background(), "user1@example.com")
|
||||
if u1 == nil || u1.Email != "user1@example.com" {
|
||||
t.Fatal("user1 not found")
|
||||
}
|
||||
|
||||
// Add User 2
|
||||
rawKey2, _ := base64.StdEncoding.DecodeString(userKey2)
|
||||
u2 := &protocol.MemoryUser{
|
||||
Email: "user2@example.com",
|
||||
Account: &MemoryAccount{
|
||||
Key: rawKey2,
|
||||
},
|
||||
}
|
||||
err = inbound.AddUser(context.Background(), u2)
|
||||
common.Must(err)
|
||||
|
||||
if inbound.GetUsersCount(context.Background()) != 2 {
|
||||
t.Fatalf("expected 2 users, got %d", inbound.GetUsersCount(context.Background()))
|
||||
}
|
||||
|
||||
// Remove User 1
|
||||
err = inbound.RemoveUser(context.Background(), "user1@example.com")
|
||||
common.Must(err)
|
||||
|
||||
if inbound.GetUsersCount(context.Background()) != 1 {
|
||||
t.Fatalf("expected 1 user, got %d", inbound.GetUsersCount(context.Background()))
|
||||
}
|
||||
if inbound.GetUser(context.Background(), "user1@example.com") != nil {
|
||||
t.Fatal("user1 should have been removed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
package shadowsocks_2022
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"math"
|
||||
mrand "math/rand/v2"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var addrParser = protocol.NewAddressParser(
|
||||
protocol.AddressFamilyByte(0x01, net.AddressFamilyIPv4),
|
||||
protocol.AddressFamilyByte(0x04, net.AddressFamilyIPv6),
|
||||
protocol.AddressFamilyByte(0x03, net.AddressFamilyDomain),
|
||||
protocol.WithAddressTypeParser(func(b byte) byte {
|
||||
return b & 0x0F
|
||||
}),
|
||||
)
|
||||
|
||||
func IncreaseNonce(nonce []byte) {
|
||||
for i := range nonce {
|
||||
nonce[i]++
|
||||
if nonce[i] != 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteAddressPort writes a destination address and port in SOCKS5 format
|
||||
func WriteAddressPort(w io.Writer, dest net.Destination) error {
|
||||
return addrParser.WriteAddressPort(w, dest.Address, dest.Port)
|
||||
}
|
||||
|
||||
// ReadAddressPort reads a destination address and port in SOCKS5 format
|
||||
func ReadAddressPort(r io.Reader) (net.Destination, error) {
|
||||
addr, port, err := addrParser.ReadAddressPort(nil, r)
|
||||
if err != nil {
|
||||
return net.Destination{}, err
|
||||
}
|
||||
return net.TCPDestination(addr, port), nil
|
||||
}
|
||||
|
||||
// AddrPortLength returns the serialized length of a destination in SOCKS5 format
|
||||
func AddrPortLength(dest net.Destination) int {
|
||||
switch dest.Address.Family() {
|
||||
case net.AddressFamilyIPv4:
|
||||
return 1 + 4 + 2
|
||||
case net.AddressFamilyDomain:
|
||||
return 1 + 1 + len(dest.Address.Domain()) + 2
|
||||
case net.AddressFamilyIPv6:
|
||||
return 1 + 16 + 2
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
type StreamWriter struct {
|
||||
writer io.Writer
|
||||
cipher cipher.AEAD
|
||||
nonce [StreamNonceSize]byte
|
||||
lenBuf [2]byte
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func NewStreamWriter(w io.Writer, c cipher.AEAD) *StreamWriter {
|
||||
return &StreamWriter{
|
||||
writer: w,
|
||||
cipher: c,
|
||||
buf: make([]byte, 0, MaxPacketSize+2+2*AEADTagSize),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *StreamWriter) Nonce() []byte {
|
||||
return w.nonce[:]
|
||||
}
|
||||
|
||||
func (w *StreamWriter) WriteChunk(payload []byte) error {
|
||||
payloadLen := len(payload)
|
||||
if payloadLen == 0 {
|
||||
return nil
|
||||
}
|
||||
if payloadLen > MaxPacketSize {
|
||||
return errors.New("payload exceeds MaxPacketSize")
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint16(w.lenBuf[:], uint16(payloadLen))
|
||||
w.buf = w.cipher.Seal(w.buf[:0], w.nonce[:], w.lenBuf[:], nil)
|
||||
IncreaseNonce(w.nonce[:])
|
||||
|
||||
w.buf = w.cipher.Seal(w.buf, w.nonce[:], payload, nil)
|
||||
IncreaseNonce(w.nonce[:])
|
||||
|
||||
_, err := w.writer.Write(w.buf)
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *StreamWriter) Write(p []byte) (int, error) {
|
||||
n := len(p)
|
||||
for len(p) > 0 {
|
||||
chunkSize := len(p)
|
||||
if chunkSize > MaxPacketSize {
|
||||
chunkSize = MaxPacketSize
|
||||
}
|
||||
if err := w.WriteChunk(p[:chunkSize]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
p = p[chunkSize:]
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (w *StreamWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
defer buf.ReleaseMulti(mb)
|
||||
for _, b := range mb {
|
||||
if err := w.WriteChunk(b.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type StreamReader struct {
|
||||
reader io.Reader
|
||||
cipher cipher.AEAD
|
||||
nonce [StreamNonceSize]byte
|
||||
lenBuf [2 + AEADTagSize]byte
|
||||
buffer []byte
|
||||
cached int
|
||||
offset int
|
||||
}
|
||||
|
||||
func NewStreamReader(r io.Reader, c cipher.AEAD) *StreamReader {
|
||||
return &StreamReader{
|
||||
reader: r,
|
||||
cipher: c,
|
||||
buffer: make([]byte, MaxPacketSize+AEADTagSize),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *StreamReader) Nonce() []byte {
|
||||
return r.nonce[:]
|
||||
}
|
||||
|
||||
func (r *StreamReader) Read(p []byte) (int, error) {
|
||||
if r.cached > 0 {
|
||||
n := copy(p, r.buffer[r.offset:r.offset+r.cached])
|
||||
r.cached -= n
|
||||
r.offset += n
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Read 2-byte length + AEAD tag (18 bytes)
|
||||
if _, err := io.ReadFull(r.reader, r.lenBuf[:]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
decryptedLen, err := r.cipher.Open(r.lenBuf[:0], r.nonce[:], r.lenBuf[:], nil)
|
||||
if err != nil {
|
||||
return 0, errors.New("failed to decrypt chunk length").Base(err)
|
||||
}
|
||||
IncreaseNonce(r.nonce[:])
|
||||
|
||||
payloadLen := int(binary.BigEndian.Uint16(decryptedLen))
|
||||
if payloadLen == 0 {
|
||||
return 0, ErrInvalidRequest
|
||||
}
|
||||
|
||||
chunkEnd := payloadLen + AEADTagSize
|
||||
if _, err := io.ReadFull(r.reader, r.buffer[:chunkEnd]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
decryptedPayload, err := r.cipher.Open(r.buffer[:0], r.nonce[:], r.buffer[:chunkEnd], nil)
|
||||
if err != nil {
|
||||
return 0, errors.New("failed to decrypt chunk payload").Base(err)
|
||||
}
|
||||
IncreaseNonce(r.nonce[:])
|
||||
|
||||
r.cached = len(decryptedPayload)
|
||||
r.offset = 0
|
||||
|
||||
n := copy(p, r.buffer[r.offset:r.offset+r.cached])
|
||||
r.cached -= n
|
||||
r.offset += n
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (r *StreamReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||
if r.cached > 0 {
|
||||
b := buf.New()
|
||||
b.Write(r.buffer[r.offset : r.offset+r.cached])
|
||||
r.cached = 0
|
||||
r.offset = 0
|
||||
return buf.MultiBuffer{b}, nil
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(r.reader, r.lenBuf[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decryptedLen, err := r.cipher.Open(r.lenBuf[:0], r.nonce[:], r.lenBuf[:], nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to decrypt chunk length").Base(err)
|
||||
}
|
||||
IncreaseNonce(r.nonce[:])
|
||||
|
||||
payloadLen := int(binary.BigEndian.Uint16(decryptedLen))
|
||||
if payloadLen == 0 {
|
||||
return nil, ErrInvalidRequest
|
||||
}
|
||||
|
||||
chunkEnd := payloadLen + AEADTagSize
|
||||
if _, err := io.ReadFull(r.reader, r.buffer[:chunkEnd]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decryptedPayload, err := r.cipher.Open(r.buffer[:0], r.nonce[:], r.buffer[:chunkEnd], nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to decrypt chunk payload").Base(err)
|
||||
}
|
||||
IncreaseNonce(r.nonce[:])
|
||||
|
||||
b := buf.New()
|
||||
b.Write(decryptedPayload)
|
||||
return buf.MultiBuffer{b}, nil
|
||||
}
|
||||
|
||||
type ClientRequestHeader struct {
|
||||
Destination net.Destination
|
||||
EarlyData []byte
|
||||
}
|
||||
|
||||
func ReadClientRequestHeader(conn io.Reader, reader *StreamReader) (*ClientRequestHeader, error) {
|
||||
var fixedBuf [RequestHeaderFixedChunkLength + AEADTagSize]byte
|
||||
if _, err := io.ReadFull(conn, fixedBuf[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plainFixed, err := reader.cipher.Open(fixedBuf[:0], reader.Nonce(), fixedBuf[:], nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to decrypt client request header").Base(err)
|
||||
}
|
||||
IncreaseNonce(reader.Nonce())
|
||||
|
||||
if plainFixed[0] != HeaderTypeClient {
|
||||
return nil, ErrBadHeaderType
|
||||
}
|
||||
|
||||
epoch := binary.BigEndian.Uint64(plainFixed[1:9])
|
||||
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch))))
|
||||
if diff > 30 {
|
||||
return nil, ErrBadTimestamp
|
||||
}
|
||||
|
||||
varHeaderLen := int(binary.BigEndian.Uint16(plainFixed[9:11]))
|
||||
if varHeaderLen == 0 {
|
||||
return nil, ErrInvalidRequest
|
||||
}
|
||||
|
||||
var stackVarChunk [512]byte
|
||||
var varChunkCipher []byte
|
||||
needed := varHeaderLen + AEADTagSize
|
||||
if needed <= len(stackVarChunk) {
|
||||
varChunkCipher = stackVarChunk[:needed]
|
||||
} else {
|
||||
varChunkCipher = make([]byte, needed)
|
||||
}
|
||||
if _, err := io.ReadFull(conn, varChunkCipher); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plainVar, err := reader.cipher.Open(varChunkCipher[:0], reader.Nonce(), varChunkCipher, nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to decrypt variable request header").Base(err)
|
||||
}
|
||||
IncreaseNonce(reader.Nonce())
|
||||
|
||||
b := buf.New()
|
||||
b.Write(plainVar)
|
||||
defer b.Release()
|
||||
|
||||
dest, err := ReadAddressPort(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var padLenBytes [2]byte
|
||||
if _, err := b.Read(padLenBytes[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paddingLen := int(binary.BigEndian.Uint16(padLenBytes[:]))
|
||||
if int(b.Len()) < paddingLen {
|
||||
return nil, ErrNoPadding
|
||||
}
|
||||
if paddingLen > 0 {
|
||||
b.Advance(int32(paddingLen))
|
||||
}
|
||||
|
||||
var earlyData []byte
|
||||
if b.Len() > 0 {
|
||||
earlyData = make([]byte, b.Len())
|
||||
copy(earlyData, b.Bytes())
|
||||
}
|
||||
|
||||
return &ClientRequestHeader{
|
||||
Destination: dest,
|
||||
EarlyData: earlyData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ClientHandshake writes the full client request header to w
|
||||
func ClientHandshake(w io.Writer, method *CipherMethod, pskList [][]byte, dest net.Destination, payload []byte) ([]byte, *StreamWriter, error) {
|
||||
salt := make([]byte, method.KeySaltLength)
|
||||
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
writer, err := WriteTCPRequest(w, method, pskList, dest, salt, payload)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return salt, writer.(*StreamWriter), nil
|
||||
}
|
||||
|
||||
// ClientVerifyServerResponse reads and verifies the server's handshake response
|
||||
func ClientVerifyServerResponse(r io.Reader, method *CipherMethod, psk []byte, clientSalt []byte) (*StreamReader, []byte, error) {
|
||||
reader, err := ReadTCPResponse(r, method, psk, clientSalt)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sr := reader.(*StreamReader)
|
||||
var initialPayload []byte
|
||||
if sr.cached > 0 {
|
||||
initialPayload = make([]byte, sr.cached)
|
||||
copy(initialPayload, sr.buffer[sr.offset:sr.offset+sr.cached])
|
||||
}
|
||||
return sr, initialPayload, nil
|
||||
}
|
||||
|
||||
// WriteTCPRequest writes the Shadowsocks 2022 request header into w and returns a body writer.
|
||||
func WriteTCPRequest(w io.Writer, method *CipherMethod, pskList [][]byte, dest net.Destination, clientSalt []byte, payload []byte) (buf.Writer, error) {
|
||||
finalPSK := pskList[len(pskList)-1]
|
||||
sessionKey := DeriveSessionSubKey(finalPSK, clientSalt, method.KeySaltLength)
|
||||
aead, err := method.NewAEAD(sessionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
writer := NewStreamWriter(w, aead)
|
||||
|
||||
handshakeBuf := buf.New()
|
||||
defer handshakeBuf.Release()
|
||||
|
||||
handshakeBuf.Write(clientSalt)
|
||||
|
||||
for i, currPSK := range pskList[:len(pskList)-1] {
|
||||
identitySubkey := DeriveIdentitySubKey(currPSK, clientSalt, method.KeySaltLength)
|
||||
block, err := method.NewBlock(identitySubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nextPSK := pskList[i+1]
|
||||
pskHash := DeriveUserPSKHash(nextPSK)
|
||||
var encryptedEIH [AESBlockSize]byte
|
||||
block.Encrypt(encryptedEIH[:], pskHash[:])
|
||||
handshakeBuf.Write(encryptedEIH[:])
|
||||
}
|
||||
|
||||
payloadLen := len(payload)
|
||||
var paddingLen int
|
||||
if payloadLen < MaxPaddingLength {
|
||||
paddingLen = mrand.IntN(MaxPaddingLength-payloadLen) + 1
|
||||
}
|
||||
addrPortLen := AddrPortLength(dest)
|
||||
varHeaderLen := addrPortLen + 2 + paddingLen + payloadLen
|
||||
|
||||
var fixedHeaderPlaintext [RequestHeaderFixedChunkLength]byte
|
||||
fixedHeaderPlaintext[0] = HeaderTypeClient
|
||||
binary.BigEndian.PutUint64(fixedHeaderPlaintext[1:9], uint64(time.Now().Unix()))
|
||||
binary.BigEndian.PutUint16(fixedHeaderPlaintext[9:11], uint16(varHeaderLen))
|
||||
|
||||
fixedChunk := writer.cipher.Seal(nil, writer.nonce[:], fixedHeaderPlaintext[:], nil)
|
||||
IncreaseNonce(writer.nonce[:])
|
||||
handshakeBuf.Write(fixedChunk)
|
||||
|
||||
varHeaderBuf := buf.New()
|
||||
defer varHeaderBuf.Release()
|
||||
|
||||
if err := WriteAddressPort(varHeaderBuf, dest); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var padLenBytes [2]byte
|
||||
binary.BigEndian.PutUint16(padLenBytes[:], uint16(paddingLen))
|
||||
varHeaderBuf.Write(padLenBytes[:])
|
||||
|
||||
if paddingLen > 0 {
|
||||
varHeaderBuf.Write(zeroPadding[:paddingLen])
|
||||
}
|
||||
|
||||
if payloadLen > 0 {
|
||||
varHeaderBuf.Write(payload)
|
||||
}
|
||||
|
||||
varChunk := writer.cipher.Seal(nil, writer.nonce[:], varHeaderBuf.Bytes(), nil)
|
||||
IncreaseNonce(writer.nonce[:])
|
||||
handshakeBuf.Write(varChunk)
|
||||
|
||||
if _, err := w.Write(handshakeBuf.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return writer, nil
|
||||
}
|
||||
|
||||
// ReadTCPResponse reads and verifies the server's handshake response and returns a reader for the stream.
|
||||
func ReadTCPResponse(r io.Reader, method *CipherMethod, psk []byte, clientSalt []byte) (buf.Reader, error) {
|
||||
var serverSalt [32]byte
|
||||
serverSaltSlice := serverSalt[:method.KeySaltLength]
|
||||
if _, err := io.ReadFull(r, serverSaltSlice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sessionKey := DeriveSessionSubKey(psk, serverSaltSlice, method.KeySaltLength)
|
||||
aead, err := method.NewAEAD(sessionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reader := NewStreamReader(r, aead)
|
||||
|
||||
fixedPlainLen := 1 + 8 + method.KeySaltLength + 2
|
||||
chunkCipherLen := fixedPlainLen + AEADTagSize
|
||||
var chunkBuf [64]byte
|
||||
chunkSlice := chunkBuf[:chunkCipherLen]
|
||||
if _, err := io.ReadFull(r, chunkSlice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decryptedFixed, err := reader.cipher.Open(chunkSlice[:0], reader.nonce[:], chunkSlice, nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to decrypt server response header").Base(err)
|
||||
}
|
||||
IncreaseNonce(reader.nonce[:])
|
||||
|
||||
if decryptedFixed[0] != HeaderTypeServer {
|
||||
return nil, ErrBadHeaderType
|
||||
}
|
||||
|
||||
serverEpoch := binary.BigEndian.Uint64(decryptedFixed[1:9])
|
||||
diff := int(math.Abs(float64(time.Now().Unix() - int64(serverEpoch))))
|
||||
if diff > 30 {
|
||||
return nil, ErrBadTimestamp
|
||||
}
|
||||
|
||||
echoedSalt := decryptedFixed[9 : 9+method.KeySaltLength]
|
||||
for i := 0; i < method.KeySaltLength; i++ {
|
||||
if echoedSalt[i] != clientSalt[i] {
|
||||
return nil, errors.New("bad request salt")
|
||||
}
|
||||
}
|
||||
|
||||
initialPayloadLen := int(binary.BigEndian.Uint16(decryptedFixed[9+method.KeySaltLength : 11+method.KeySaltLength]))
|
||||
if initialPayloadLen > 0 {
|
||||
initialCipherLen := initialPayloadLen + AEADTagSize
|
||||
if _, err := io.ReadFull(r, reader.buffer[:initialCipherLen]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decryptedInitial, err := reader.cipher.Open(reader.buffer[:0], reader.nonce[:], reader.buffer[:initialCipherLen], nil)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to decrypt initial response payload").Base(err)
|
||||
}
|
||||
IncreaseNonce(reader.nonce[:])
|
||||
reader.cached = len(decryptedInitial)
|
||||
reader.offset = 0
|
||||
}
|
||||
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
// WriteTCPResponse writes the server handshake response and returns a body writer for server stream.
|
||||
func WriteTCPResponse(w io.Writer, method *CipherMethod, psk []byte, clientSalt []byte, initialPayload []byte) (buf.Writer, error) {
|
||||
var serverSalt [32]byte
|
||||
serverSaltSlice := serverSalt[:method.KeySaltLength]
|
||||
if _, err := io.ReadFull(rand.Reader, serverSaltSlice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
respKey := DeriveSessionSubKey(psk, serverSaltSlice, method.KeySaltLength)
|
||||
respAead, err := method.NewAEAD(respKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writer := NewStreamWriter(w, respAead)
|
||||
|
||||
respBuf := buf.New()
|
||||
defer respBuf.Release()
|
||||
|
||||
respBuf.Write(serverSaltSlice)
|
||||
|
||||
var fixedRespPlain [1 + 8 + 32 + 2]byte
|
||||
fixedRespSlice := fixedRespPlain[:1+8+method.KeySaltLength+2]
|
||||
fixedRespSlice[0] = HeaderTypeServer
|
||||
binary.BigEndian.PutUint64(fixedRespSlice[1:9], uint64(time.Now().Unix()))
|
||||
copy(fixedRespSlice[9:9+method.KeySaltLength], clientSalt)
|
||||
binary.BigEndian.PutUint16(fixedRespSlice[9+method.KeySaltLength:11+method.KeySaltLength], uint16(len(initialPayload)))
|
||||
|
||||
fixedRespChunk := writer.cipher.Seal(nil, writer.nonce[:], fixedRespSlice, nil)
|
||||
IncreaseNonce(writer.nonce[:])
|
||||
respBuf.Write(fixedRespChunk)
|
||||
|
||||
if len(initialPayload) > 0 {
|
||||
initialChunk := writer.cipher.Seal(nil, writer.nonce[:], initialPayload, nil)
|
||||
IncreaseNonce(writer.nonce[:])
|
||||
respBuf.Write(initialChunk)
|
||||
}
|
||||
|
||||
if _, err := w.Write(respBuf.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return writer, nil
|
||||
}
|
||||
@@ -105,7 +105,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
|
||||
}
|
||||
udpRequest, err := ClientHandshake(request, conn, conn)
|
||||
if err != nil {
|
||||
return errors.New("failed to establish connection to server").AtWarning().Base(err)
|
||||
return errors.New("failed to establish connection to server").Base(err)
|
||||
}
|
||||
if udpRequest != nil {
|
||||
if udpRequest.Address == net.AnyIP || udpRequest.Address == net.AnyIPv6 {
|
||||
|
||||
@@ -458,10 +458,10 @@ func ClientHandshake(request *protocol.RequestHeader, reader io.Reader, writer i
|
||||
}
|
||||
|
||||
if b.Byte(0) != socks5Version {
|
||||
return nil, errors.New("unexpected server version: ", b.Byte(0)).AtWarning()
|
||||
return nil, errors.New("unexpected server version: ", b.Byte(0))
|
||||
}
|
||||
if b.Byte(1) != authByte {
|
||||
return nil, errors.New("auth method not supported.").AtWarning()
|
||||
return nil, errors.New("auth method not supported.")
|
||||
}
|
||||
|
||||
if authByte == authPassword {
|
||||
|
||||
@@ -69,7 +69,7 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
|
||||
return 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)
|
||||
}
|
||||
errors.LogInfo(ctx, "tunneling request to ", destination, " via ", server.Destination.NetAddr())
|
||||
|
||||
@@ -116,21 +116,21 @@ func (c *Client) Process(ctx context.Context, link *transport.Link, dialer inter
|
||||
|
||||
// write some request payload to buffer
|
||||
if err = buf.CopyOnceTimeout(link.Reader, bodyWriter, time.Millisecond*100); err != nil && err != buf.ErrNotTimeoutReader && err != buf.ErrReadTimeout {
|
||||
return errors.New("failed to write A request payload").Base(err).AtWarning()
|
||||
return errors.New("failed to write A request payload").Base(err)
|
||||
}
|
||||
|
||||
// Flush; bufferWriter.WriteMultiBuffer now is bufferWriter.writer.WriteMultiBuffer
|
||||
if err = bufferWriter.SetBuffered(false); err != nil {
|
||||
return errors.New("failed to flush payload").Base(err).AtWarning()
|
||||
return errors.New("failed to flush payload").Base(err)
|
||||
}
|
||||
|
||||
// Send header if not sent yet
|
||||
if _, err = connWriter.Write([]byte{}); err != nil {
|
||||
return err.(*errors.Error).AtWarning()
|
||||
return err
|
||||
}
|
||||
|
||||
if err = buf.Copy(link.Reader, bodyWriter, buf.UpdateActivity(timer)); err != nil {
|
||||
return errors.New("failed to transfer request payload").Base(err).AtInfo()
|
||||
return errors.New("failed to transfer request payload").Base(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+12
-12
@@ -47,11 +47,11 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
|
||||
for _, user := range config.Users {
|
||||
u, err := user.ToMemoryUser()
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to get trojan user").Base(err).AtError()
|
||||
return nil, errors.New("failed to get trojan user").Base(err)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con
|
||||
|
||||
sessionPolicy := s.policyManager.ForLevel(0)
|
||||
if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil {
|
||||
return errors.New("unable to set read deadline").Base(err).AtWarning()
|
||||
return errors.New("unable to set read deadline").Base(err)
|
||||
}
|
||||
|
||||
first := buf.FromBytes(make([]byte, buf.Size))
|
||||
@@ -219,7 +219,7 @@ func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Con
|
||||
|
||||
destination := clientReader.Target
|
||||
if err := conn.SetReadDeadline(time.Time{}); err != nil {
|
||||
return errors.New("unable to set read deadline").Base(err).AtWarning()
|
||||
return errors.New("unable to set read deadline").Base(err)
|
||||
}
|
||||
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
@@ -402,7 +402,7 @@ func (s *Server) fallback(ctx context.Context, err error, sessionPolicy policy.S
|
||||
}
|
||||
apfb := napfb[name]
|
||||
if apfb == nil {
|
||||
return errors.New(`failed to find the default "name" config`).AtWarning()
|
||||
return errors.New(`failed to find the default "name" config`)
|
||||
}
|
||||
|
||||
if apfb[alpn] == nil {
|
||||
@@ -410,7 +410,7 @@ func (s *Server) fallback(ctx context.Context, err error, sessionPolicy policy.S
|
||||
}
|
||||
pfb := apfb[alpn]
|
||||
if pfb == nil {
|
||||
return errors.New(`failed to find the default "alpn" config`).AtWarning()
|
||||
return errors.New(`failed to find the default "alpn" config`)
|
||||
}
|
||||
|
||||
path := ""
|
||||
@@ -444,7 +444,7 @@ func (s *Server) fallback(ctx context.Context, err error, sessionPolicy policy.S
|
||||
}
|
||||
fb := pfb[path]
|
||||
if fb == nil {
|
||||
return errors.New(`failed to find the default "path" config`).AtWarning()
|
||||
return errors.New(`failed to find the default "path" config`)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
@@ -460,7 +460,7 @@ func (s *Server) fallback(ctx context.Context, err error, sessionPolicy policy.S
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return errors.New("failed to dial to " + fb.Dest).Base(err).AtWarning()
|
||||
return errors.New("failed to dial to " + fb.Dest).Base(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
@@ -520,11 +520,11 @@ func (s *Server) fallback(ctx context.Context, err error, sessionPolicy policy.S
|
||||
common.Must2(pro.Write([]byte{byte(p1 >> 8), byte(p1), byte(p2 >> 8), byte(p2)}))
|
||||
}
|
||||
if err := serverWriter.WriteMultiBuffer(buf.MultiBuffer{pro}); err != nil {
|
||||
return errors.New("failed to set PROXY protocol v", fb.Xver).Base(err).AtWarning()
|
||||
return errors.New("failed to set PROXY protocol v", fb.Xver).Base(err)
|
||||
}
|
||||
}
|
||||
if err := buf.Copy(reader, serverWriter, buf.UpdateActivity(timer)); err != nil {
|
||||
return errors.New("failed to fallback request payload").Base(err).AtInfo()
|
||||
return errors.New("failed to fallback request payload").Base(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -534,7 +534,7 @@ func (s *Server) fallback(ctx context.Context, err error, sessionPolicy policy.S
|
||||
getResponse := func() error {
|
||||
defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly)
|
||||
if err := buf.Copy(serverReader, writer, buf.UpdateActivity(timer)); err != nil {
|
||||
return errors.New("failed to deliver response payload").Base(err).AtInfo()
|
||||
return errors.New("failed to deliver response payload").Base(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -542,7 +542,7 @@ func (s *Server) fallback(ctx context.Context, err error, sessionPolicy policy.S
|
||||
if err := task.Run(ctx, task.OnSuccess(postRequest, task.Close(serverWriter)), task.OnSuccess(getResponse, task.Close(writer))); err != nil {
|
||||
common.Must(common.Interrupt(serverReader))
|
||||
common.Must(common.Interrupt(serverWriter))
|
||||
return errors.New("fallback ends").Base(err).AtInfo()
|
||||
return errors.New("fallback ends").Base(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+60
-2
@@ -15,13 +15,57 @@ Plainly enabling it in the config probably will result nothing, or lock your rou
|
||||
## DETAILS
|
||||
|
||||
By default, enabling the feature will only bring the tun interface up. \
|
||||
When configured explicitly, Windows and Linux can apply interface addresses from `gateway`, while macOS uses the first IPv4 prefix from `gateway` to configure the utun point-to-point address. \
|
||||
When configured explicitly, Windows and Linux can apply interface addresses from `gateway`, while macOS and FreeBSD use the first IPv4 prefix from `gateway` for the point-to-point address. \
|
||||
Without `gateway`, the systems differ: Xray assigns no address on Linux, Windows gives the interface link-local addresses itself (an IPv6 one at once, an IPv4 one from `169.254.0.0/16` after a few seconds), and macOS and FreeBSD use `169.254.10.1/30`. \
|
||||
Windows, Linux and macOS can also apply system routes from `autoSystemRoutingTable`.
|
||||
Linux and macOS do not configure system DNS from the `dns` field; system DNS remains managed by the OS or distribution-specific network services. \
|
||||
macOS does not configure system DNS from the `dns` field, and neither does Linux by default; system DNS remains managed by the OS or distribution-specific network services. \
|
||||
For more advanced routing policies or rules, OS level configuration can still manage the named interface (e.g. xray0) when it appears.
|
||||
This keeps complex system level routing and rules in a single place of responsibility - the OS itself. \
|
||||
Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-networkd) can be found at the end of this README.
|
||||
|
||||
### SYSTEM DNS ON LINUX (`autoSystemDnsToGateway`)
|
||||
|
||||
On Linux, setting `autoSystemDnsToGateway` to `true` lets the inbound point the system resolver at the tun interface, so name lookups resolve through Xray instead of going out over the physical link. It is off by default, and it is Linux-only.
|
||||
|
||||
It uses `resolvectl`, which means it only works when all of these hold. Where Xray can tell that one does not, it does not start:
|
||||
|
||||
- the system runs systemd and `resolvectl` is on `PATH`
|
||||
- `systemd-resolved` is enabled and actually managing DNS (installed but not running is not enough)
|
||||
- systemd-resolved is version 240 or newer, where `default-route` exists
|
||||
- no `dns` upstream resolves through the system resolver, directly or through its own bootstrap (see below)
|
||||
|
||||
The address handed over is the first IPv4 `gateway`, or without one the first IPv6 `gateway`, incremented by one (e.g. `192.168.100.1/30` -> `192.168.100.2`, `fc00::1/64` -> `fc00::2`). Without any `gateway`, the config is rejected. It is not taken from `dns`: handing `1.1.1.1` to `resolvectl dns` would make systemd-resolved query that server directly over the physical link, which is the leak this option exists to close.
|
||||
|
||||
Because that address has to actually answer, the takeover is checked before it happens. A query from the interface address to that address is routed through the configured rules, and host-wide DNS is only changed when the result is a DNS-capable outbound. Otherwise DNS is left alone and Xray does not start. In practice this means you also need a routing rule sending the interface's port 53 to a `dns` outbound, for example:
|
||||
|
||||
```json
|
||||
"routing": {
|
||||
"rules": [
|
||||
{ "type": "field", "inboundTag": ["tun"], "port": 53, "outboundTag": "dns" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The check is a preflight, not a proof for arbitrary rules. It sends its query from the interface address and from a representative ephemeral source port, so a rule that matches on the source port cannot be predicted ahead of time: if the interface's port 53 reaches the `dns` outbound only from some source ports, the takeover is accepted and queries from the other ports fail. Supported configurations are those where the DNS path does not depend on the source port, that is, where the interface's port 53 reaches a `dns` outbound whatever its source.
|
||||
|
||||
It is also a check for the dependencies it knows about, not a proof that no indirect one exists. A hostname-based upstream that bootstraps through system DNS is the case in point: `https+local://dns.google/dns-query` resolves its own hostname with `DialSystem`, so once the takeover is in place that bootstrap goes `resolved -> TUN -> DNS outbound -> bootstrap -> resolved` and the query times out. The preflight does not see it, because the dependency sits in the upstream's bootstrap rather than in the clients it inspects. Upstream resolution, bootstrap included, therefore has to stay independent of the resolver path being redirected; configuring the address instead of the hostname, or resolving the hostname beforehand, avoids it.
|
||||
|
||||
The upstream requirement in the list above matters as much as the routing rule. With no name servers configured, Core resolves through a client that forwards to the system resolver; pointing the system resolver at the TUN would then close a loop through the DNS outbound, `resolved -> TUN -> DNS outbound -> system resolver -> resolved`, and resolution stops. The takeover is refused in that case, and Xray does not start.
|
||||
|
||||
The same applies to a name server pointed at `localhost`, and to a `dns` section that is present but lists no name servers. One such upstream is enough to refuse the takeover even when independent upstreams are configured alongside it: name servers are selected per domain, so a domain-specific rule can still choose the local one, and the loop then affects whichever domains reach it. The check is deliberately broader than the loop it observed, because the alternative would be to drop a name server the user configured.
|
||||
|
||||
Where it cannot apply, Xray does not start, rather than run with the leak described in XTLS/Xray-core#6454, so leave the option off there:
|
||||
|
||||
| Environment | Behaviour |
|
||||
|---|---|
|
||||
| systemd distribution with systemd-resolved enabled | applies |
|
||||
| Alpine, Void, Devuan, OpenRC-based, OpenWrt | no `resolvectl`, does not start |
|
||||
| DNS managed by dnsmasq / unbound / BIND / static `resolv.conf` | unreachable by `resolvectl`, does not start |
|
||||
| Containers without a systemd-resolved daemon | does not start |
|
||||
| systemd older than 240 | `default-route` unavailable, does not start |
|
||||
|
||||
On `Close()` the setting is reverted. It is **not** reverted if the process is killed with `SIGKILL`, since a process cannot handle that signal; run `resolvectl revert <iface>` to clean up by hand. An application that brings its own DNS endpoint is unaffected either way — this only covers the system resolver.
|
||||
|
||||
Due to this inbound not actually being a proxy, the configuration ignore required listen and port options, and never listen on any port. \
|
||||
Here is simple Xray config snippet to enable the inbound:
|
||||
```
|
||||
@@ -155,6 +199,20 @@ To make it start, wintun.dll specific for your Windows/arch must be present next
|
||||
|
||||
After the start network adapter with the name you chose in the config will be created in the system, and exist while Xray is running.
|
||||
|
||||
When `dns` is set, those servers are applied to the adapter. Windows is kept from registering the TUN's addresses in DNS, and its DNS cache is flushed when the TUN starts and stops.
|
||||
|
||||
With `autoSystemWfpBlockLeak`, which needs `autoSystemRoutingTable` (the config is rejected otherwise), Xray also adds Windows Filtering Platform filters that keep two kinds of traffic of every program but Xray itself from leaving outside the TUN, each chosen by a value in the list, e.g. `"autoSystemWfpBlockLeak": ["dns", "misconfigtun"]`:
|
||||
- `"dns"` (needs `dns`, the config is rejected otherwise): DNS (port 53) only goes through the TUN. Windows keeps sending name queries to the DNS servers of the other interfaces as well, out through those interfaces whatever the routes say, and other programs reach a resolver on the local network (e.g. `192.168.1.1` handed out by DHCP) through its more specific LAN route instead of the TUN. On Windows 11 and Server 2022 and later, where those queries may also go over HTTPS or TLS, Windows' DNS Client service cannot connect outside the TUN at all, except for name resolution on the local network (LLMNR, mDNS). The `dns` servers therefore have to lie within `gateway` or `autoSystemRoutingTable` (a warning is logged otherwise), and DNS servers that should be reached directly belong in Xray's own `dns` settings.
|
||||
- `"misconfigtun"`: an IP version without routes in `autoSystemRoutingTable`, IPv4 or IPv6, is blocked entirely, in both directions, as it would bypass the TUN. Only loopback and what Windows itself needs on the local link (DHCP, and for IPv6 neighbor and multicast listener discovery) remain allowed. An address of that version in `gateway` is not needed: without one, Windows gives the TUN link-local addresses itself, an IPv6 one at once and an IPv4 one from `169.254.0.0/16` after some seconds (until then, IPv4 routed to the TUN is unreachable), and what is routed to the TUN goes through it with those.
|
||||
|
||||
With the filters in place, Xray's own connections out also get past Windows Firewall's block rules (other firewalls may still block them), while connections to Xray's inbounds stay subject to them.
|
||||
|
||||
Names that Xray resolves through the system resolver, such as an outbound's server address given as a domain with the default `AsIs` domain strategy, would be looked up by Windows on Xray's behalf, and those queries would then go into the TUN too. While DNS is restricted this way and `autoOutboundsInterface` is in use (the default with `autoSystemRoutingTable`), Xray therefore resolves them itself, with its own queries to the DNS servers of the other interfaces. That bypasses Windows' DNS cache, and its name resolution on the local network (LLMNR, mDNS): a server address given as a domain is looked up again for every connection, and a DNS server that does not answer delays each lookup. Having Xray's own `dns` resolve it, through the outbound's `sockopt.domainStrategy`, avoids that. The `localhost` DNS server queries the same servers whenever `autoOutboundsInterface` is in use. Both skip the TUN's own DNS servers, unless another interface uses them as well: queried from Xray itself, they would lead back into it, or nowhere.
|
||||
|
||||
If the filters cannot be added, Xray does not start. They are removed when Xray exits. Not covered is name resolution on the local network (LLMNR, mDNS, NetBIOS), except over an IP version that is blocked.
|
||||
|
||||
`autoSystemWfpBlockLeak` (Windows only) is empty by default, as the filters break some setups: with `"dns"`, a local DNS resolver other programs use (e.g. on `127.0.0.1:53`), the DNS of another VPN on its own interface, virtual machines whose NAT resolves names on the host, or signing in to a captive portal; with `"misconfigtun"`, IPv4 or IPv6 on the local network while no route of that version leads to the TUN. Without the filters, DNS may leak as described above. To keep an IP version out of the TUN on purpose while still blocking DNS leaks, use only `["dns"]`.
|
||||
|
||||
You can give the adapter ip address manually, you can live Windows to give it autogenerated ip address (which take few seconds), it doesn't matter, the traffic going _through_ the interface will be forwarded into the app for proxying. \
|
||||
Minimal configuration that will work for local machine is routing passing the traffic on-link through the interface.
|
||||
You will need the interface id for that, unfortunately it is going to change with every Xray start due to implementation ambiguity between Xray and wintun driver.
|
||||
|
||||
+21
-2
@@ -32,6 +32,8 @@ type Config struct {
|
||||
AutoSystemRoutingTable []string `protobuf:"bytes,6,rep,name=auto_system_routing_table,json=autoSystemRoutingTable,proto3" json:"auto_system_routing_table,omitempty"`
|
||||
AutoOutboundsInterface string `protobuf:"bytes,7,opt,name=auto_outbounds_interface,json=autoOutboundsInterface,proto3" json:"auto_outbounds_interface,omitempty"`
|
||||
Desc string `protobuf:"bytes,8,opt,name=desc,proto3" json:"desc,omitempty"`
|
||||
AutoSystemDnsToGateway bool `protobuf:"varint,9,opt,name=auto_system_dns_to_gateway,json=autoSystemDnsToGateway,proto3" json:"auto_system_dns_to_gateway,omitempty"`
|
||||
AutoSystemWfpBlockLeak []string `protobuf:"bytes,10,rep,name=auto_system_wfp_block_leak,json=autoSystemWfpBlockLeak,proto3" json:"auto_system_wfp_block_leak,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -122,11 +124,25 @@ func (x *Config) GetDesc() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetAutoSystemDnsToGateway() bool {
|
||||
if x != nil {
|
||||
return x.AutoSystemDnsToGateway
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Config) GetAutoSystemWfpBlockLeak() []string {
|
||||
if x != nil {
|
||||
return x.AutoSystemWfpBlockLeak
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_proxy_tun_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_proxy_tun_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\x82\x02\n" +
|
||||
"\x16proxy/tun/config.proto\x12\x0exray.proxy.tun\"\xfa\x02\n" +
|
||||
"\x06Config\x12\x12\n" +
|
||||
"\x04name\x18\x01 \x01(\tR\x04name\x12\x10\n" +
|
||||
"\x03MTU\x18\x02 \x01(\rR\x03MTU\x12\x18\n" +
|
||||
@@ -136,7 +152,10 @@ const file_proxy_tun_config_proto_rawDesc = "" +
|
||||
"user_level\x18\x05 \x01(\rR\tuserLevel\x129\n" +
|
||||
"\x19auto_system_routing_table\x18\x06 \x03(\tR\x16autoSystemRoutingTable\x128\n" +
|
||||
"\x18auto_outbounds_interface\x18\a \x01(\tR\x16autoOutboundsInterface\x12\x12\n" +
|
||||
"\x04desc\x18\b \x01(\tR\x04descBL\n" +
|
||||
"\x04desc\x18\b \x01(\tR\x04desc\x12:\n" +
|
||||
"\x1aauto_system_dns_to_gateway\x18\t \x01(\bR\x16autoSystemDnsToGateway\x12:\n" +
|
||||
"\x1aauto_system_wfp_block_leak\x18\n" +
|
||||
" \x03(\tR\x16autoSystemWfpBlockLeakBL\n" +
|
||||
"\x12com.xray.proxy.tunP\x01Z#github.com/xtls/xray-core/proxy/tun\xaa\x02\x0eXray.Proxy.Tunb\x06proto3"
|
||||
|
||||
var (
|
||||
|
||||
@@ -15,4 +15,6 @@ message Config {
|
||||
repeated string auto_system_routing_table = 6;
|
||||
string auto_outbounds_interface = 7;
|
||||
string desc = 8;
|
||||
bool auto_system_dns_to_gateway = 9;
|
||||
repeated string auto_system_wfp_block_leak = 10;
|
||||
}
|
||||
|
||||
@@ -165,6 +165,18 @@ func (t *Handler) Start() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Platform-specific system DNS takeover, where the platform implements it.
|
||||
// Rather no TUN than one that the system DNS bypasses.
|
||||
if c, ok := tunInterface.(interface {
|
||||
ConfigureSystemDNS(context.Context, string) error
|
||||
}); ok {
|
||||
if err := c.ConfigureSystemDNS(t.ctx, t.tag); err != nil {
|
||||
_ = tunStack.Close()
|
||||
_ = tunInterface.Close()
|
||||
return errors.New("unable to set the system DNS (remove autoSystemDnsToGateway to run without)").Base(err)
|
||||
}
|
||||
}
|
||||
|
||||
t.stack = tunStack
|
||||
t.tun = tunInterface
|
||||
|
||||
|
||||
@@ -6,12 +6,24 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
appdns "github.com/xtls/xray-core/app/dns"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
xnet "github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/platform"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
"github.com/xtls/xray-core/core"
|
||||
feature_dns "github.com/xtls/xray-core/features/dns"
|
||||
"github.com/xtls/xray-core/features/dns/localdns"
|
||||
"github.com/xtls/xray-core/features/outbound"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
routingsession "github.com/xtls/xray-core/features/routing/session"
|
||||
"github.com/xtls/xray-core/proxy/dns"
|
||||
"golang.org/x/sys/unix"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/link/fdbased"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||
@@ -30,6 +42,244 @@ type LinuxTun struct {
|
||||
systemRoutes []netlink.Route
|
||||
routeMonitorStop chan struct{}
|
||||
routeMonitorOnce sync.Once
|
||||
|
||||
systemDNSSet bool
|
||||
systemDNSDirty bool
|
||||
}
|
||||
|
||||
// resolvectlRunner runs a resolvectl command. Overridable for tests.
|
||||
var resolvectlRunner = func(name string, args ...string) ([]byte, error) {
|
||||
return exec.Command(name, args...).CombinedOutput()
|
||||
}
|
||||
|
||||
// systemDNSAddrs derives the addresses used for the system DNS takeover from the
|
||||
// first IPv4 gateway, or without one, the first IPv6 gateway: the gateway
|
||||
// address itself is what a query from this interface appears to come from, and
|
||||
// the next address is what the resolver is pointed at. The latter belongs to
|
||||
// the TUN and is answered inside Xray; handing the configured public resolvers
|
||||
// to resolvectl instead would leave the system querying them directly over the
|
||||
// physical link, defeating the point of the TUN.
|
||||
func systemDNSAddrs(gateway []string) (source, dns netip.Addr, ok bool) {
|
||||
var first6 netip.Addr
|
||||
for _, address := range gateway {
|
||||
prefix, err := netip.ParsePrefix(address)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
addr := prefix.Addr()
|
||||
if addr.Is4() {
|
||||
return addr, addr.Next(), true
|
||||
}
|
||||
if !first6.IsValid() {
|
||||
first6 = addr
|
||||
}
|
||||
}
|
||||
if first6.IsValid() {
|
||||
return first6, first6.Next(), true
|
||||
}
|
||||
return netip.Addr{}, netip.Addr{}, false
|
||||
}
|
||||
|
||||
func buildResolvectlArgs(action, iface string, extra ...string) []string {
|
||||
args := make([]string, 0, 2+len(extra))
|
||||
args = append(args, action, iface)
|
||||
args = append(args, extra...)
|
||||
return args
|
||||
}
|
||||
|
||||
func runResolvectl(action, iface string, extra ...string) error {
|
||||
args := buildResolvectlArgs(action, iface, extra...)
|
||||
if _, err := resolvectlRunner("resolvectl", args...); err != nil {
|
||||
return errors.New("resolvectl ", action, " failed").Base(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ifaceName returns the TUN interface name, or empty when the link is not
|
||||
// available. Callers must treat empty as "nothing to configure".
|
||||
func (t *LinuxTun) ifaceName() string {
|
||||
if t.tunLink == nil {
|
||||
return ""
|
||||
}
|
||||
attrs := t.tunLink.Attrs()
|
||||
if attrs == nil {
|
||||
return ""
|
||||
}
|
||||
return attrs.Name
|
||||
}
|
||||
|
||||
// probeSourcePort is a representative client port for the routing probe. A real
|
||||
// query arrives from an ephemeral port that cannot be known in advance, so this
|
||||
// only matters for a rule that matches on a source port.
|
||||
const probeSourcePort = 49152
|
||||
|
||||
// verifyDNSRouting reports whether a DNS query to address would actually be
|
||||
// handled. Redirecting the system resolver at an address nothing answers would
|
||||
// break name resolution outright, so the takeover only proceeds when routing
|
||||
// hands such a query to a DNS-capable outbound.
|
||||
//
|
||||
// Overridable for tests.
|
||||
var verifyDNSRouting = func(ctx context.Context, inboundTag, source, address string) error {
|
||||
ip, err := netip.ParseAddr(address)
|
||||
if err != nil {
|
||||
return errors.New("invalid DNS address ", address).Base(err)
|
||||
}
|
||||
src, err := netip.ParseAddr(source)
|
||||
if err != nil || src.Is4() != ip.Is4() {
|
||||
return errors.New("invalid source address ", source).Base(err)
|
||||
}
|
||||
|
||||
instance := core.MustFromContext(ctx)
|
||||
|
||||
// Any resolution path that could still reach the system resolver has to be
|
||||
// refused, because pointing the system resolver at the TUN would close a
|
||||
// loop through the DNS outbound. With no `dns` section Core installs such a
|
||||
// client; with a `dns` section that has no name servers app/dns falls back
|
||||
// to one; and a name server pointed at "localhost" is one even when
|
||||
// independent upstreams are configured alongside it, because name servers
|
||||
// are selected per domain.
|
||||
switch dnsFeature := instance.GetFeature(feature_dns.ClientType()).(type) {
|
||||
case *localdns.Client:
|
||||
return errors.New("DNS feature is the system resolver, takeover would loop")
|
||||
case *appdns.DNS:
|
||||
if dnsFeature.MayUseSystemResolver() {
|
||||
return errors.New("DNS configuration may resolve through the system resolver, takeover would loop")
|
||||
}
|
||||
}
|
||||
|
||||
router, ok := instance.GetFeature(routing.RouterType()).(routing.Router)
|
||||
if !ok {
|
||||
return errors.New("router feature unavailable")
|
||||
}
|
||||
|
||||
// A real query from this interface carries a source address, and rules may
|
||||
// match on it, so the probe has to carry one too.
|
||||
queryCtx := session.ContextWithInbound(ctx, &session.Inbound{
|
||||
Name: "tun",
|
||||
Tag: inboundTag,
|
||||
Source: xnet.UDPDestination(xnet.IPAddress(src.AsSlice()), probeSourcePort),
|
||||
})
|
||||
queryCtx = session.ContextWithOutbounds(queryCtx, []*session.Outbound{{
|
||||
Target: xnet.UDPDestination(xnet.IPAddress(ip.AsSlice()), 53),
|
||||
}})
|
||||
|
||||
route, err := router.PickRoute(routingsession.AsRoutingContext(queryCtx))
|
||||
if err != nil {
|
||||
return errors.New("no route for ", address, ":53").Base(err)
|
||||
}
|
||||
|
||||
manager, ok := instance.GetFeature(outbound.ManagerType()).(outbound.Manager)
|
||||
if !ok {
|
||||
return errors.New("outbound manager unavailable")
|
||||
}
|
||||
|
||||
handler := manager.GetHandler(route.GetOutboundTag())
|
||||
if handler == nil {
|
||||
return errors.New("outbound ", route.GetOutboundTag(), " does not exist")
|
||||
}
|
||||
if settings := handler.ProxySettings(); settings == nil || settings.Type != serial.GetMessageType(&dns.Config{}) {
|
||||
return errors.New("outbound ", route.GetOutboundTag(), " does not handle DNS")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigureSystemDNS points systemd-resolved at this interface so name lookups
|
||||
// resolve through Xray instead of leaking to the physical link.
|
||||
//
|
||||
// It acts only when the config opts in, and it verifies the data path first:
|
||||
// unless a query to the advertised address would actually be handled, host-wide
|
||||
// resolution is left to the OS and an error returned. The caller does not start
|
||||
// the TUN on an error, as the system DNS would bypass it.
|
||||
func (t *LinuxTun) ConfigureSystemDNS(ctx context.Context, inboundTag string) error {
|
||||
if !t.options.AutoSystemDnsToGateway {
|
||||
return nil
|
||||
}
|
||||
if t.systemDNSSet {
|
||||
return nil
|
||||
}
|
||||
|
||||
// A previous revert may have failed. Retry before applying anything, so a
|
||||
// dirty resolver does not silently outlive the attempt to clean it up.
|
||||
if t.systemDNSDirty {
|
||||
if err := t.revertSystemDNS(); err != nil {
|
||||
return errors.New("previous system DNS revert still failing").Base(err)
|
||||
}
|
||||
}
|
||||
|
||||
source, address, ok := systemDNSAddrs(t.options.Gateway)
|
||||
if !ok {
|
||||
return errors.New("no gateway, cannot derive a system DNS address")
|
||||
}
|
||||
|
||||
iface := t.ifaceName()
|
||||
if iface == "" {
|
||||
return errors.New("interface not available")
|
||||
}
|
||||
|
||||
if err := verifyDNSRouting(ctx, inboundTag, source.String(), address.String()); err != nil {
|
||||
return errors.New("no DNS path at ", address.String(), ":53").Base(err)
|
||||
}
|
||||
|
||||
// Applied as a sequence with rollback: a half-configured resolver would be
|
||||
// worse than none at all.
|
||||
if err := runResolvectl("dns", iface, address.String()); err != nil {
|
||||
return errors.New("resolvectl dns failed").Base(err)
|
||||
}
|
||||
if err := runResolvectl("domain", iface, "~."); err != nil {
|
||||
return t.rollbackSystemDNS(iface, errors.New("resolvectl domain failed").Base(err))
|
||||
}
|
||||
if err := runResolvectl("default-route", iface, "true"); err != nil {
|
||||
return t.rollbackSystemDNS(iface, errors.New("resolvectl default-route failed").Base(err))
|
||||
}
|
||||
|
||||
t.systemDNSSet = true
|
||||
errors.LogInfo(ctx, "[tun] system DNS set to ", address.String(), " on ", iface)
|
||||
return nil
|
||||
}
|
||||
|
||||
// rollbackSystemDNS undoes a partially applied takeover. A failed revert is
|
||||
// recorded so the next attempt retries it, and is reported rather than
|
||||
// swallowed.
|
||||
func (t *LinuxTun) rollbackSystemDNS(iface string, cause error) error {
|
||||
if err := runResolvectl("revert", iface); err != nil {
|
||||
t.systemDNSDirty = true
|
||||
// Combine, because Base overwrites: reporting only the cause would hide
|
||||
// the revert failure, and reporting only the revert failure would hide
|
||||
// why the revert was attempted.
|
||||
return errors.New("revert failed, per-link DNS settings may remain").Base(errors.Combine(err, cause))
|
||||
}
|
||||
return cause
|
||||
}
|
||||
|
||||
// revertSystemDNS issues the revert and keeps the dirty flag in step with the
|
||||
// outcome.
|
||||
func (t *LinuxTun) revertSystemDNS() error {
|
||||
err := runResolvectl("revert", t.ifaceName())
|
||||
t.systemDNSDirty = err != nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.systemDNSSet = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// unsetSystemDNS hands DNS back to the OS. Only meaningful when
|
||||
// ConfigureSystemDNS applied something, or a previous revert failed.
|
||||
func (t *LinuxTun) unsetSystemDNS() {
|
||||
if !t.systemDNSSet && !t.systemDNSDirty {
|
||||
return
|
||||
}
|
||||
|
||||
if t.ifaceName() == "" {
|
||||
// The link is gone, and its per-link settings went with it.
|
||||
t.systemDNSSet = false
|
||||
t.systemDNSDirty = false
|
||||
return
|
||||
}
|
||||
|
||||
if err := t.revertSystemDNS(); err != nil {
|
||||
errors.LogInfoInner(context.Background(), err, "[tun] failed to revert system DNS; per-link settings may remain until revert succeeds")
|
||||
}
|
||||
}
|
||||
|
||||
// LinuxTun implements Tun
|
||||
@@ -200,6 +450,7 @@ func (t *LinuxTun) Close() error {
|
||||
}
|
||||
})
|
||||
|
||||
t.unsetSystemDNS()
|
||||
_ = t.unsetSystemRoutes()
|
||||
_ = t.unsetInterfaceAddresses()
|
||||
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
//go:build linux && !android
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/app/dispatcher"
|
||||
appdns "github.com/xtls/xray-core/app/dns"
|
||||
"github.com/xtls/xray-core/app/proxyman"
|
||||
_ "github.com/xtls/xray-core/app/proxyman/inbound"
|
||||
_ "github.com/xtls/xray-core/app/proxyman/outbound"
|
||||
"github.com/xtls/xray-core/app/router"
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/proxy/blackhole"
|
||||
proxydns "github.com/xtls/xray-core/proxy/dns"
|
||||
"github.com/xtls/xray-core/proxy/freedom"
|
||||
)
|
||||
|
||||
const (
|
||||
routeTestInboundTag = "tun"
|
||||
routeTestSource = "192.168.100.1"
|
||||
routeTestDNSAddress = "192.168.100.2"
|
||||
)
|
||||
|
||||
// port53Rule sends DNS queries arriving from the interface to the dns outbound.
|
||||
func port53Rule() *router.RoutingRule {
|
||||
return &router.RoutingRule{
|
||||
InboundTag: []string{routeTestInboundTag},
|
||||
PortList: &net.PortList{Range: []*net.PortRange{net.SinglePortRange(53)}},
|
||||
TargetTag: &router.RoutingRule_Tag{Tag: "dns"},
|
||||
}
|
||||
}
|
||||
|
||||
// sourceBlockRule diverts traffic from one address, which is the shape of a rule
|
||||
// that only matches because the real request carries a source.
|
||||
func sourceBlockRule(ip []byte) *router.RoutingRule {
|
||||
return &router.RoutingRule{
|
||||
SourceIp: []*geodata.IPRule{{
|
||||
Value: &geodata.IPRule_Custom{
|
||||
Custom: &geodata.CIDRRule{
|
||||
Cidr: &geodata.CIDR{Ip: ip, Prefix: 32},
|
||||
},
|
||||
},
|
||||
}},
|
||||
TargetTag: &router.RoutingRule_Tag{Tag: "block"},
|
||||
}
|
||||
}
|
||||
|
||||
// newRouteTestContext builds a real but unstarted instance: no TUN device, no
|
||||
// running resolver. The instance is placed in the context through the key core
|
||||
// exports for tests.
|
||||
func newRouteTestContext(t *testing.T, withDNSApp bool, nameServers []*appdns.NameServer, rules []*router.RoutingRule) context.Context {
|
||||
t.Helper()
|
||||
|
||||
apps := []*serial.TypedMessage{
|
||||
serial.ToTypedMessage(&dispatcher.Config{}),
|
||||
serial.ToTypedMessage(&proxyman.InboundConfig{}),
|
||||
serial.ToTypedMessage(&proxyman.OutboundConfig{}),
|
||||
serial.ToTypedMessage(&router.Config{Rule: rules}),
|
||||
}
|
||||
if withDNSApp {
|
||||
apps = append(apps, serial.ToTypedMessage(&appdns.Config{NameServer: nameServers}))
|
||||
}
|
||||
|
||||
instance, err := core.New(&core.Config{
|
||||
App: apps,
|
||||
Outbound: []*core.OutboundHandlerConfig{
|
||||
{Tag: "direct", ProxySettings: serial.ToTypedMessage(&freedom.Config{})},
|
||||
{Tag: "dns", ProxySettings: serial.ToTypedMessage(&proxydns.Config{})},
|
||||
{Tag: "block", ProxySettings: serial.ToTypedMessage(&blackhole.Config{})},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("core.New: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = instance.Close() })
|
||||
|
||||
return context.WithValue(context.Background(), core.XrayKey(1), instance)
|
||||
}
|
||||
|
||||
func udpNameServer(ip []byte) []*appdns.NameServer {
|
||||
return []*appdns.NameServer{{
|
||||
Address: &net.Endpoint{
|
||||
Network: net.Network_UDP,
|
||||
Address: &net.IPOrDomain{Address: &net.IPOrDomain_Ip{Ip: ip}},
|
||||
Port: 53,
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
// localNameServer is a name server pointed at "localhost", which app/dns
|
||||
// resolves through the system resolver.
|
||||
func localNameServer() *appdns.NameServer {
|
||||
return &appdns.NameServer{
|
||||
Address: &net.Endpoint{
|
||||
Network: net.Network_UDP,
|
||||
Address: &net.IPOrDomain{Address: &net.IPOrDomain_Domain{Domain: "localhost"}},
|
||||
Port: 53,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// These drive the real feature lookup and the real router. verifyDNSRouting is
|
||||
// the same function ConfigureSystemDNS calls, so a false positive here is a
|
||||
// false positive in the takeover decision itself, which is what assertions on
|
||||
// the resolvectl arguments could never catch.
|
||||
func TestVerifyDNSRoutingDecisions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
withDNSApp bool
|
||||
nameServers []*appdns.NameServer
|
||||
rules []*router.RoutingRule
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "independent upstream reaches the dns outbound",
|
||||
withDNSApp: true,
|
||||
nameServers: udpNameServer([]byte{9, 9, 9, 9}),
|
||||
rules: []*router.RoutingRule{port53Rule()},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "no dns section falls back to the system resolver",
|
||||
rules: []*router.RoutingRule{port53Rule()},
|
||||
wantErr: "system resolver",
|
||||
},
|
||||
{
|
||||
name: "dns section without name servers falls back too",
|
||||
withDNSApp: true,
|
||||
rules: []*router.RoutingRule{port53Rule()},
|
||||
wantErr: "system resolver",
|
||||
},
|
||||
{
|
||||
// An independent upstream is not enough on its own: name servers are
|
||||
// selected per domain, so a local one can still be the one chosen.
|
||||
// The refusal is deliberately domain-agnostic for that reason.
|
||||
name: "a local name server alongside an independent one",
|
||||
withDNSApp: true,
|
||||
nameServers: append(udpNameServer([]byte{9, 9, 9, 9}), localNameServer()),
|
||||
rules: []*router.RoutingRule{port53Rule()},
|
||||
wantErr: "system resolver",
|
||||
},
|
||||
{
|
||||
name: "a rule on the interface address diverts the real query",
|
||||
withDNSApp: true, nameServers: udpNameServer([]byte{9, 9, 9, 9}),
|
||||
rules: []*router.RoutingRule{
|
||||
sourceBlockRule([]byte{192, 168, 100, 1}),
|
||||
port53Rule(),
|
||||
},
|
||||
wantErr: "does not handle DNS",
|
||||
},
|
||||
{
|
||||
name: "a rule on another address does not match it",
|
||||
withDNSApp: true, nameServers: udpNameServer([]byte{9, 9, 9, 9}),
|
||||
rules: []*router.RoutingRule{
|
||||
sourceBlockRule([]byte{10, 0, 0, 1}),
|
||||
port53Rule(),
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "no rule matches the query",
|
||||
withDNSApp: true, nameServers: udpNameServer([]byte{9, 9, 9, 9}),
|
||||
wantErr: "no route",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := newRouteTestContext(t, tt.withDNSApp, tt.nameServers, tt.rules)
|
||||
err := verifyDNSRouting(ctx, routeTestInboundTag, routeTestSource, routeTestDNSAddress)
|
||||
|
||||
if tt.wantErr == "" {
|
||||
if err != nil {
|
||||
t.Fatalf("expected the takeover to be accepted, got: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatalf("expected the takeover to be refused with %q, got nil", tt.wantErr)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Errorf("error = %q, want it to contain %q", err.Error(), tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Without an IPv4 gateway, the takeover uses the first IPv6 one, and the probe
|
||||
// carries IPv6 addresses.
|
||||
func TestVerifyDNSRoutingIPv6(t *testing.T) {
|
||||
ctx := newRouteTestContext(t, true, udpNameServer([]byte{9, 9, 9, 9}), []*router.RoutingRule{port53Rule()})
|
||||
if err := verifyDNSRouting(ctx, routeTestInboundTag, "fc00::1", "fc00::2"); err != nil {
|
||||
t.Fatalf("expected the takeover to be accepted, got: %v", err)
|
||||
}
|
||||
if err := verifyDNSRouting(ctx, routeTestInboundTag, routeTestSource, "fc00::2"); err == nil {
|
||||
t.Fatal("expected mixed IPv4 and IPv6 addresses to be refused")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
//go:build linux && !android
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// testLink returns a minimal netlink.Link whose Attrs().Name is name, so the
|
||||
// DNS helpers can be exercised without a real TUN device.
|
||||
func testLink(name string) netlink.Link {
|
||||
return &netlink.Dummy{LinkAttrs: netlink.LinkAttrs{Name: name}}
|
||||
}
|
||||
|
||||
type probeCall struct {
|
||||
inboundTag string
|
||||
source string
|
||||
address string
|
||||
}
|
||||
|
||||
// stubDNSRouting replaces the routing probe for the duration of a test and
|
||||
// records how it was called, so tests can assert the probe is representative.
|
||||
func stubDNSRouting(t *testing.T, err error) *[]probeCall {
|
||||
t.Helper()
|
||||
original := verifyDNSRouting
|
||||
calls := []probeCall{}
|
||||
verifyDNSRouting = func(_ context.Context, inboundTag, source, address string) error {
|
||||
calls = append(calls, probeCall{inboundTag, source, address})
|
||||
return err
|
||||
}
|
||||
t.Cleanup(func() { verifyDNSRouting = original })
|
||||
return &calls
|
||||
}
|
||||
|
||||
// recorder installs a resolvectl stub for the duration of a test and returns the
|
||||
// captured invocations. An empty failOn succeeds every call; otherwise the named
|
||||
// subcommand fails.
|
||||
func recorder(t *testing.T, failOn string) *[][]string {
|
||||
t.Helper()
|
||||
original := resolvectlRunner
|
||||
calls := [][]string{}
|
||||
resolvectlRunner = func(name string, args ...string) ([]byte, error) {
|
||||
calls = append(calls, append([]string{name}, args...))
|
||||
if failOn != "" && len(args) > 0 && args[0] == failOn {
|
||||
return nil, errors.New("boom")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
t.Cleanup(func() { resolvectlRunner = original })
|
||||
return &calls
|
||||
}
|
||||
|
||||
func optedInTun() *LinuxTun {
|
||||
return &LinuxTun{
|
||||
options: &Config{
|
||||
Name: "xray_tun",
|
||||
Gateway: []string{"192.168.100.1/30"},
|
||||
AutoSystemDnsToGateway: true,
|
||||
},
|
||||
tunLink: testLink("xray_tun"),
|
||||
}
|
||||
}
|
||||
|
||||
func joined(calls [][]string) string {
|
||||
parts := make([]string, 0, len(calls))
|
||||
for _, call := range calls {
|
||||
parts = append(parts, strings.Join(call, " "))
|
||||
}
|
||||
return strings.Join(parts, " | ")
|
||||
}
|
||||
|
||||
func TestConfigureSystemDNSDisabledByDefault(t *testing.T) {
|
||||
probes := stubDNSRouting(t, nil)
|
||||
calls := recorder(t, "")
|
||||
|
||||
t1 := optedInTun()
|
||||
t1.options.AutoSystemDnsToGateway = false
|
||||
|
||||
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(*probes) != 0 {
|
||||
t.Errorf("routing probe must not run when disabled, got %d calls", len(*probes))
|
||||
}
|
||||
if len(*calls) != 0 {
|
||||
t.Errorf("resolvectl must not run when disabled, got %v", *calls)
|
||||
}
|
||||
if t1.systemDNSSet {
|
||||
t.Error("systemDNSSet should stay false when disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureSystemDNSNoGateway(t *testing.T) {
|
||||
probes := stubDNSRouting(t, nil)
|
||||
calls := recorder(t, "")
|
||||
|
||||
t1 := optedInTun()
|
||||
t1.options.Gateway = nil
|
||||
|
||||
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err == nil {
|
||||
t.Fatal("expected an error when no gateway is configured")
|
||||
}
|
||||
if len(*probes) != 0 {
|
||||
t.Errorf("routing probe must not run without a gateway, got %d calls", len(*probes))
|
||||
}
|
||||
if len(*calls) != 0 {
|
||||
t.Errorf("resolvectl must not run without a gateway, got %v", *calls)
|
||||
}
|
||||
}
|
||||
|
||||
// This is the case the reviewer flagged: without a routed DNS path, pointing the
|
||||
// system resolver at the derived address would break resolution outright.
|
||||
func TestConfigureSystemDNSLeavesOSDNSWhenNoRoute(t *testing.T) {
|
||||
probes := stubDNSRouting(t, errors.New("no route"))
|
||||
calls := recorder(t, "")
|
||||
|
||||
t1 := optedInTun()
|
||||
|
||||
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err == nil {
|
||||
t.Fatal("expected an error when the DNS path is unverified")
|
||||
}
|
||||
if len(*probes) != 1 {
|
||||
t.Errorf("routing probe should run once, got %d", len(*probes))
|
||||
}
|
||||
if len(*calls) != 0 {
|
||||
t.Errorf("system DNS must be left untouched, got %v", *calls)
|
||||
}
|
||||
if t1.systemDNSSet {
|
||||
t.Error("systemDNSSet should stay false when the path is unverified")
|
||||
}
|
||||
}
|
||||
|
||||
// A real query from the interface carries a source address, and rules may match
|
||||
// on it, so the probe must not be source-less.
|
||||
func TestConfigureSystemDNSProbeCarriesSource(t *testing.T) {
|
||||
probes := stubDNSRouting(t, nil)
|
||||
recorder(t, "")
|
||||
|
||||
t1 := optedInTun()
|
||||
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(*probes) != 1 {
|
||||
t.Fatalf("expected one probe call, got %d", len(*probes))
|
||||
}
|
||||
got := (*probes)[0]
|
||||
if got.source != "192.168.100.1" {
|
||||
t.Errorf("probe source = %q, want the interface address %q", got.source, "192.168.100.1")
|
||||
}
|
||||
if got.address != "192.168.100.2" {
|
||||
t.Errorf("probe address = %q, want %q", got.address, "192.168.100.2")
|
||||
}
|
||||
if got.inboundTag != "tun" {
|
||||
t.Errorf("probe inbound tag = %q, want %q", got.inboundTag, "tun")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureSystemDNSAppliesResolvectl(t *testing.T) {
|
||||
stubDNSRouting(t, nil)
|
||||
calls := recorder(t, "")
|
||||
|
||||
t1 := optedInTun()
|
||||
|
||||
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !t1.systemDNSSet {
|
||||
t.Fatal("systemDNSSet should be true after a successful takeover")
|
||||
}
|
||||
|
||||
want := "resolvectl dns xray_tun 192.168.100.2 | " +
|
||||
"resolvectl domain xray_tun ~. | " +
|
||||
"resolvectl default-route xray_tun true"
|
||||
if got := joined(*calls); got != want {
|
||||
t.Errorf("resolvectl calls = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureSystemDNSIdempotent(t *testing.T) {
|
||||
stubDNSRouting(t, nil)
|
||||
calls := recorder(t, "")
|
||||
|
||||
t1 := optedInTun()
|
||||
|
||||
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
before := len(*calls)
|
||||
|
||||
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(*calls) != before {
|
||||
t.Errorf("second call must be a no-op, calls went %d -> %d", before, len(*calls))
|
||||
}
|
||||
}
|
||||
|
||||
// A half-applied resolver is worse than none, so a failure mid-sequence reverts.
|
||||
func TestConfigureSystemDNSRollsBackOnPartialFailure(t *testing.T) {
|
||||
stubDNSRouting(t, nil)
|
||||
calls := recorder(t, "domain")
|
||||
|
||||
t1 := optedInTun()
|
||||
|
||||
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err == nil {
|
||||
t.Fatal("expected an error when a resolvectl step fails")
|
||||
}
|
||||
if t1.systemDNSSet {
|
||||
t.Error("systemDNSSet should stay false after a failed takeover")
|
||||
}
|
||||
if t1.systemDNSDirty {
|
||||
t.Error("a successful revert should not leave the resolver dirty")
|
||||
}
|
||||
if !strings.Contains(joined(*calls), "resolvectl revert xray_tun") {
|
||||
t.Errorf("expected a revert after partial failure, got %q", joined(*calls))
|
||||
}
|
||||
}
|
||||
|
||||
// If the revert itself fails the settings may still be installed, so the state
|
||||
// has to be remembered rather than silently dropped.
|
||||
func TestConfigureSystemDNSRollbackFailureKeepsDirty(t *testing.T) {
|
||||
stubDNSRouting(t, nil)
|
||||
calls := recorder(t, "revert")
|
||||
|
||||
t1 := optedInTun()
|
||||
t1.options.Gateway = []string{"192.168.100.1/30"}
|
||||
// Make only the rollback path fail: "dns" succeeds, "domain" fails, "revert" fails.
|
||||
*calls = nil
|
||||
|
||||
original := resolvectlRunner
|
||||
defer func() { resolvectlRunner = original }()
|
||||
resolvectlRunner = func(name string, args ...string) ([]byte, error) {
|
||||
*calls = append(*calls, append([]string{name}, args...))
|
||||
if len(args) > 0 && (args[0] == "domain" || args[0] == "revert") {
|
||||
return nil, errors.New("boom")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err == nil {
|
||||
t.Fatal("expected an error when domain fails")
|
||||
}
|
||||
if !t1.systemDNSDirty {
|
||||
t.Error("a failed revert must leave the resolver marked dirty")
|
||||
}
|
||||
if t1.systemDNSSet {
|
||||
t.Error("systemDNSSet must stay false when the takeover did not complete")
|
||||
}
|
||||
}
|
||||
|
||||
// A dirty resolver is retried before anything new is applied.
|
||||
func TestConfigureSystemDNSRetriesDirtyBeforeApplying(t *testing.T) {
|
||||
stubDNSRouting(t, nil)
|
||||
calls := recorder(t, "")
|
||||
|
||||
t1 := optedInTun()
|
||||
t1.systemDNSDirty = true
|
||||
|
||||
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
got := joined(*calls)
|
||||
if !strings.HasPrefix(got, "resolvectl revert xray_tun") {
|
||||
t.Errorf("expected the stale revert first, got %q", got)
|
||||
}
|
||||
if t1.systemDNSDirty {
|
||||
t.Error("a successful retry should clear the dirty flag")
|
||||
}
|
||||
if !t1.systemDNSSet {
|
||||
t.Error("the takeover should proceed once the retry succeeds")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsetSystemDNSReverts(t *testing.T) {
|
||||
stubDNSRouting(t, nil)
|
||||
calls := recorder(t, "")
|
||||
|
||||
t1 := optedInTun()
|
||||
if err := t1.ConfigureSystemDNS(context.Background(), "tun"); err != nil {
|
||||
t.Fatalf("setup failed: %v", err)
|
||||
}
|
||||
*calls = nil
|
||||
|
||||
t1.unsetSystemDNS()
|
||||
if t1.systemDNSSet {
|
||||
t.Error("systemDNSSet should be false after unset")
|
||||
}
|
||||
if got := joined(*calls); got != "resolvectl revert xray_tun" {
|
||||
t.Errorf("unset calls = %q, want %q", got, "resolvectl revert xray_tun")
|
||||
}
|
||||
|
||||
t1.unsetSystemDNS()
|
||||
if len(*calls) != 1 {
|
||||
t.Errorf("unsetSystemDNS must be idempotent, got %q", joined(*calls))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsetSystemDNSKeepsDirtyWhenRevertFails(t *testing.T) {
|
||||
stubDNSRouting(t, nil)
|
||||
calls := recorder(t, "revert")
|
||||
|
||||
t1 := optedInTun()
|
||||
t1.systemDNSSet = true
|
||||
|
||||
t1.unsetSystemDNS()
|
||||
if !t1.systemDNSDirty {
|
||||
t.Error("a failed revert during unset must be remembered")
|
||||
}
|
||||
if got := joined(*calls); !strings.Contains(got, "resolvectl revert xray_tun") {
|
||||
t.Errorf("expected a revert attempt, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemDNSAddrs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
gateway []string
|
||||
wantSource string
|
||||
wantDNS string
|
||||
wantOK bool
|
||||
}{
|
||||
{
|
||||
name: "ipv4 /30",
|
||||
gateway: []string{"192.168.100.1/30"},
|
||||
wantSource: "192.168.100.1",
|
||||
wantDNS: "192.168.100.2",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "ipv4 /16",
|
||||
gateway: []string{"10.0.0.1/16"},
|
||||
wantSource: "10.0.0.1",
|
||||
wantDNS: "10.0.0.2",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "first ipv4 wins",
|
||||
gateway: []string{"fc00::1/64", "172.18.0.1/30"},
|
||||
wantSource: "172.18.0.1",
|
||||
wantDNS: "172.18.0.2",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "no gateway",
|
||||
gateway: nil,
|
||||
wantOK: false,
|
||||
},
|
||||
{
|
||||
name: "ipv6 only",
|
||||
gateway: []string{"fc00::1/64"},
|
||||
wantSource: "fc00::1",
|
||||
wantDNS: "fc00::2",
|
||||
wantOK: true,
|
||||
},
|
||||
{
|
||||
name: "first ipv6 without ipv4",
|
||||
gateway: []string{"fc00::1/64", "fd00::1/64"},
|
||||
wantSource: "fc00::1",
|
||||
wantDNS: "fc00::2",
|
||||
wantOK: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
source, dnsAddr, ok := systemDNSAddrs(tt.gateway)
|
||||
if ok != tt.wantOK {
|
||||
t.Fatalf("ok = %v, want %v", ok, tt.wantOK)
|
||||
}
|
||||
if !tt.wantOK {
|
||||
return
|
||||
}
|
||||
if source.String() != tt.wantSource {
|
||||
t.Errorf("source = %q, want %q", source.String(), tt.wantSource)
|
||||
}
|
||||
if dnsAddr.String() != tt.wantDNS {
|
||||
t.Errorf("dns = %q, want %q", dnsAddr.String(), tt.wantDNS)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildResolvectlArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
action string
|
||||
iface string
|
||||
extra []string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "revert",
|
||||
action: "revert",
|
||||
iface: "xray_tun",
|
||||
want: []string{"revert", "xray_tun"},
|
||||
},
|
||||
{
|
||||
name: "dns single",
|
||||
action: "dns",
|
||||
iface: "xray_tun",
|
||||
extra: []string{"192.168.100.2"},
|
||||
want: []string{"dns", "xray_tun", "192.168.100.2"},
|
||||
},
|
||||
{
|
||||
name: "dns multiple",
|
||||
action: "dns",
|
||||
iface: "xray_tun",
|
||||
extra: []string{"192.168.100.2", "fc00::2"},
|
||||
want: []string{"dns", "xray_tun", "192.168.100.2", "fc00::2"},
|
||||
},
|
||||
{
|
||||
name: "domain wildcard",
|
||||
action: "domain",
|
||||
iface: "xray_tun",
|
||||
extra: []string{"~."},
|
||||
want: []string{"domain", "xray_tun", "~."},
|
||||
},
|
||||
{
|
||||
name: "default-route",
|
||||
action: "default-route",
|
||||
iface: "xray_tun",
|
||||
extra: []string{"true"},
|
||||
want: []string{"default-route", "xray_tun", "true"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := buildResolvectlArgs(tt.action, tt.iface, tt.extra...)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Fatalf("args = %v, want %v", got, tt.want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Errorf("args[%d] = %q, want %q (full: %v)", i, got[i], tt.want[i], got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+235
-3
@@ -3,17 +3,25 @@
|
||||
package tun
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
go_errors "errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wintun"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
@@ -38,6 +46,10 @@ type WindowsTun struct {
|
||||
luid winipcfg.LUID
|
||||
cbr winipcfg.ChangeCallback
|
||||
cbi winipcfg.ChangeCallback
|
||||
wfp windows.Handle
|
||||
resolver *savedResolver
|
||||
skipStop chan struct{}
|
||||
skipDone chan struct{}
|
||||
closed bool
|
||||
}
|
||||
|
||||
@@ -78,8 +90,13 @@ func open(name, desc string) (*wintun.Adapter, error) {
|
||||
// generate a deterministic GUID from the adapter name
|
||||
id := md5.Sum([]byte(name))
|
||||
guid := (*windows.GUID)(unsafe.Pointer(&id[0]))
|
||||
// try to open existing adapter by name
|
||||
adapter, err := wintun.OpenAdapter(name)
|
||||
if err == nil {
|
||||
return adapter, nil
|
||||
}
|
||||
// try to create adapter anew
|
||||
adapter, err := wintun.CreateAdapter(name, desc, guid)
|
||||
adapter, err = wintun.CreateAdapter(name, desc, guid)
|
||||
if err == nil {
|
||||
return adapter, nil
|
||||
}
|
||||
@@ -192,19 +209,105 @@ startOver:
|
||||
}
|
||||
}
|
||||
|
||||
// Windows lists the TUN's DNS servers among the system's ones, which Go's
|
||||
// resolver queries for Xray's own lookups past the TUN, where they lead
|
||||
// nowhere or back into Xray. Not skipped are those another interface uses
|
||||
// as well, as that could leave no server at all. As those can change at
|
||||
// any time, they are looked at again as often as Go rereads its servers.
|
||||
if len(dns) > 0 {
|
||||
skipped, err := tunOnlyDNS(t.luid, dns)
|
||||
if err != nil {
|
||||
skipped = dns
|
||||
}
|
||||
internet.SkipDNSServers(skipped)
|
||||
t.skipStop, t.skipDone = make(chan struct{}), make(chan struct{})
|
||||
go func() {
|
||||
defer close(t.skipDone)
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if skipped, err := tunOnlyDNS(t.luid, dns); err == nil {
|
||||
internet.SkipDNSServers(skipped)
|
||||
}
|
||||
case <-t.skipStop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Keep Windows from registering the TUN's addresses, and the host name
|
||||
// with them, through dynamic DNS updates. Best effort.
|
||||
if address4 || address6 {
|
||||
if err := disableDNSRegistration(t.luid, dns); err != nil {
|
||||
errors.LogDebugInner(context.Background(), err, "[tun] unable to disable DNS registration")
|
||||
}
|
||||
}
|
||||
|
||||
// With autoSystemWfpBlockLeak, once the system routes lead to the TUN,
|
||||
// keep DNS ("dns", if dns is set), and an IP version no route of which
|
||||
// leads to the TUN ("misconfigtun"), from leaving through the other
|
||||
// interfaces. Addresses do not matter: without one of a version in
|
||||
// gateway, Windows gives the TUN a link-local one.
|
||||
leaks := t.options.AutoSystemWfpBlockLeak
|
||||
blockDNS := slices.Contains(leaks, "dns") && len(dns) > 0
|
||||
blockIPv4 := slices.Contains(leaks, "misconfigtun") && !route4
|
||||
blockIPv6 := slices.Contains(leaks, "misconfigtun") && !route6
|
||||
if (route4 || route6) && (blockDNS || blockIPv4 || blockIPv6) {
|
||||
if t.wfp, err = blockLeaks(t.luid, blockDNS, blockIPv4, blockIPv6); err != nil {
|
||||
var blocked []string
|
||||
for _, b := range []struct {
|
||||
on bool
|
||||
what string
|
||||
}{{blockDNS, "DNS"}, {blockIPv4, "IPv4"}, {blockIPv6, "IPv6"}} {
|
||||
if b.on {
|
||||
blocked = append(blocked, b.what)
|
||||
}
|
||||
}
|
||||
// Rather no TUN than a leaking one.
|
||||
return errors.New("unable to block ", strings.Join(blocked, " and "), " outside the TUN (remove autoSystemWfpBlockLeak to run without)").Base(err)
|
||||
}
|
||||
errors.LogInfo(context.Background(), "[tun] outside the TUN, blocked DNS: ", blockDNS, ", blocked IPv4: ", blockIPv4, ", blocked IPv6: ", blockIPv6)
|
||||
if blockDNS {
|
||||
covered := slices.Clone(addresses)
|
||||
for _, route := range routesData {
|
||||
covered = append(covered, route.Destination)
|
||||
}
|
||||
for _, server := range dnsOutsideTUN(dns, covered) {
|
||||
errors.LogWarning(context.Background(), "[tun] DNS server ", server, " is in neither gateway nor autoSystemRoutingTable, so queries to it cannot go through the TUN and are blocked")
|
||||
}
|
||||
// With updater, the dialer controllers bind Xray's own sockets
|
||||
// to the physical interface.
|
||||
if updater != nil {
|
||||
t.resolver = resolveOnOwn()
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(dns) > 0 || route4 || route6 {
|
||||
if err := flushDNSCache(); err != nil {
|
||||
errors.LogInfoInner(context.Background(), err, "[tun] unable to flush DNS cache")
|
||||
}
|
||||
}
|
||||
|
||||
if updater != nil {
|
||||
t.cbr, err = winipcfg.RegisterRouteChangeCallback(func(notificationType winipcfg.MibNotificationType, route *winipcfg.MibIPforwardRow2) {
|
||||
// Only a registered callback goes into the fields: a nil pointer in
|
||||
// them would not compare equal to nil in Close.
|
||||
cbr, err := winipcfg.RegisterRouteChangeCallback(func(notificationType winipcfg.MibNotificationType, route *winipcfg.MibIPforwardRow2) {
|
||||
updater.Update()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.cbi, err = winipcfg.RegisterInterfaceChangeCallback(func(notificationType winipcfg.MibNotificationType, iface *winipcfg.MibIPInterfaceRow) {
|
||||
t.cbr = cbr
|
||||
cbi, err := winipcfg.RegisterInterfaceChangeCallback(func(notificationType winipcfg.MibNotificationType, iface *winipcfg.MibIPInterfaceRow) {
|
||||
updater.Update()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.cbi = cbi
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -231,6 +334,20 @@ func (t *WindowsTun) Close() error {
|
||||
t.luid.FlushIPAddresses(windows.AF_INET6)
|
||||
t.luid.FlushDNS(windows.AF_INET6)
|
||||
}
|
||||
if t.wfp != 0 {
|
||||
closeWFPEngine(t.wfp)
|
||||
}
|
||||
if t.resolver != nil {
|
||||
t.resolver.restore()
|
||||
}
|
||||
if t.skipStop != nil {
|
||||
close(t.skipStop)
|
||||
<-t.skipDone
|
||||
}
|
||||
internet.SkipDNSServers(nil)
|
||||
if len(t.options.DNS) > 0 || len(t.options.AutoSystemRoutingTable) > 0 {
|
||||
flushDNSCache()
|
||||
}
|
||||
if t.session != (wintun.Session{}) {
|
||||
t.session.End()
|
||||
}
|
||||
@@ -240,6 +357,121 @@ func (t *WindowsTun) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type savedResolver struct {
|
||||
preferGo bool
|
||||
dial func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
// resolveOnOwn has Go resolve the names Xray would otherwise ask Windows for,
|
||||
// on Xray's own sockets, which the dialer controllers bind to the physical
|
||||
// interface, and skipping the TUN's DNS servers, as localdns does. Windows'
|
||||
// resolver runs in the DNS Client service, whose queries the DNS filter lets
|
||||
// through the TUN only, so Xray's own lookups, like of an outbound's server
|
||||
// domain, would go into Xray again and could end up waiting on themselves.
|
||||
//
|
||||
// It changes net.DefaultResolver for the whole process, which covers every
|
||||
// lookup that would reach Windows' resolver; restore undoes it.
|
||||
func resolveOnOwn() *savedResolver {
|
||||
saved := &savedResolver{net.DefaultResolver.PreferGo, net.DefaultResolver.Dial}
|
||||
dialer := &net.Dialer{Control: func(network, address string, c syscall.RawConn) error {
|
||||
for _, ctl := range internet.Controllers {
|
||||
if err := ctl(network, address, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}}
|
||||
// Go's resolver moves on to the next server right away when a dial fails.
|
||||
net.DefaultResolver.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
if internet.IsSkippedDNSServer(address) {
|
||||
return nil, errors.New("skipped DNS server ", address)
|
||||
}
|
||||
return dialer.DialContext(ctx, network, address)
|
||||
}
|
||||
net.DefaultResolver.PreferGo = true
|
||||
return saved
|
||||
}
|
||||
|
||||
func (s *savedResolver) restore() {
|
||||
net.DefaultResolver.PreferGo = s.preferGo
|
||||
net.DefaultResolver.Dial = s.dial
|
||||
}
|
||||
|
||||
// tunOnlyDNS returns those of servers, the TUN's DNS servers, that Go's
|
||||
// resolver does not also get from another interface: one that is up and has
|
||||
// a gateway, as it reads them.
|
||||
func tunOnlyDNS(tun winipcfg.LUID, servers []netip.Addr) ([]netip.Addr, error) {
|
||||
adapters, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludeGateways)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var others []netip.Addr
|
||||
for _, adapter := range adapters {
|
||||
if adapter.LUID == tun || adapter.OperStatus != winipcfg.IfOperStatusUp || adapter.FirstGatewayAddress == nil {
|
||||
continue
|
||||
}
|
||||
for server := adapter.FirstDNSServerAddress; server != nil; server = server.Next {
|
||||
if addr, ok := netip.AddrFromSlice(server.Address.IP()); ok {
|
||||
others = append(others, addr.Unmap())
|
||||
}
|
||||
}
|
||||
}
|
||||
return slices.DeleteFunc(slices.Clone(servers), func(server netip.Addr) bool {
|
||||
return slices.Contains(others, server.Unmap())
|
||||
}), nil
|
||||
}
|
||||
|
||||
// disableDNSRegistration turns off the dynamic DNS registration of the
|
||||
// interface's addresses. dns are its DNS servers.
|
||||
func disableDNSRegistration(luid winipcfg.LUID, dns []netip.Addr) error {
|
||||
guid, err := luid.GUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = winipcfg.SetInterfaceDnsSettings(*guid, &winipcfg.DnsInterfaceSettings{
|
||||
Version: winipcfg.DnsInterfaceSettingsVersion1,
|
||||
Flags: winipcfg.DnsInterfaceSettingsFlagRegistrationEnabled,
|
||||
})
|
||||
if err == nil || !go_errors.Is(err, windows.ERROR_PROC_NOT_FOUND) {
|
||||
return err
|
||||
}
|
||||
return disableDNSRegistrationByNetsh(luid, dns)
|
||||
}
|
||||
|
||||
// disableDNSRegistrationByNetsh does it for Windows before 10 1809, which
|
||||
// lacks SetInterfaceDnsSettings. The setting is the interface's, not the
|
||||
// address family's, but netsh only applies it along with a DNS server, which
|
||||
// replaces the IPv4 ones, so they are set again afterwards.
|
||||
func disableDNSRegistrationByNetsh(luid winipcfg.LUID, dns []netip.Addr) error {
|
||||
row, err := luid.Interface()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
server := "127.0.0.1" // any will do when there is no IPv4 one
|
||||
if i := slices.IndexFunc(dns, netip.Addr.Is4); i >= 0 {
|
||||
server = dns[i].String()
|
||||
}
|
||||
err = runNetsh("interface", "ipv4", "set", "dnsservers", "name="+strconv.FormatUint(uint64(row.InterfaceIndex), 10), "source=static", "address="+server, "register=none", "validate=no")
|
||||
return errors.Combine(err, luid.SetDNS(windows.AF_INET, dns, nil))
|
||||
}
|
||||
|
||||
// runNetsh runs netsh.exe from the system directory. netsh reports some
|
||||
// failures, like a syntax error, only in its output, even with exit code 0,
|
||||
// so any output counts as a failure.
|
||||
func runNetsh(args ...string) error {
|
||||
system32, err := windows.GetSystemDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(filepath.Join(system32, "netsh.exe"), args...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
output, err := cmd.CombinedOutput()
|
||||
if output = bytes.TrimSpace(output); err != nil || len(output) > 0 {
|
||||
return errors.New("netsh ", strings.Join(args, " "), ": ", string(output)).Base(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *WindowsTun) Name() (string, error) {
|
||||
row, err := t.luid.Interface()
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,471 @@
|
||||
//go:build windows
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"runtime"
|
||||
"slices"
|
||||
"unsafe"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
)
|
||||
|
||||
var (
|
||||
modfwpuclnt = windows.NewLazySystemDLL("fwpuclnt.dll")
|
||||
moddnsapi = windows.NewLazySystemDLL("dnsapi.dll")
|
||||
|
||||
procFwpmEngineOpen0 = modfwpuclnt.NewProc("FwpmEngineOpen0")
|
||||
procFwpmEngineClose0 = modfwpuclnt.NewProc("FwpmEngineClose0")
|
||||
procFwpmTransactionBegin0 = modfwpuclnt.NewProc("FwpmTransactionBegin0")
|
||||
procFwpmTransactionCommit0 = modfwpuclnt.NewProc("FwpmTransactionCommit0")
|
||||
procFwpmTransactionAbort0 = modfwpuclnt.NewProc("FwpmTransactionAbort0")
|
||||
procFwpmSubLayerAdd0 = modfwpuclnt.NewProc("FwpmSubLayerAdd0")
|
||||
procFwpmFilterAdd0 = modfwpuclnt.NewProc("FwpmFilterAdd0")
|
||||
procFwpmGetAppIdFromFileName0 = modfwpuclnt.NewProc("FwpmGetAppIdFromFileName0")
|
||||
procFwpmFreeMemory0 = modfwpuclnt.NewProc("FwpmFreeMemory0")
|
||||
procDnsFlushResolverCache = moddnsapi.NewProc("DnsFlushResolverCache")
|
||||
)
|
||||
|
||||
// fwptypes.h and fwpmtypes.h
|
||||
const (
|
||||
rpcCAuthnWinNT = 10 // RPC_C_AUTHN_WINNT
|
||||
fwpmSessionFlagDynamic = 1 // FWPM_SESSION_FLAG_DYNAMIC
|
||||
fwpmFilterFlagClearActionRight = 8 // FWPM_FILTER_FLAG_CLEAR_ACTION_RIGHT
|
||||
|
||||
fwpUint8 = 1 // FWP_UINT8
|
||||
fwpUint16 = 2 // FWP_UINT16
|
||||
fwpUint32 = 3 // FWP_UINT32
|
||||
fwpUint64 = 4 // FWP_UINT64
|
||||
fwpByteArray16Type = 11 // FWP_BYTE_ARRAY16_TYPE
|
||||
fwpByteBlobType = 12 // FWP_BYTE_BLOB_TYPE
|
||||
fwpSecurityDescriptorType = 14 // FWP_SECURITY_DESCRIPTOR_TYPE
|
||||
|
||||
fwpMatchEqual = 0 // FWP_MATCH_EQUAL
|
||||
fwpMatchFlagsAllSet = 6 // FWP_MATCH_FLAGS_ALL_SET
|
||||
|
||||
fwpConditionFlagIsLoopback = 1 // FWP_CONDITION_FLAG_IS_LOOPBACK
|
||||
|
||||
fwpActionBlock = 0x1001 // FWP_ACTION_BLOCK
|
||||
fwpActionPermit = 0x1002 // FWP_ACTION_PERMIT
|
||||
)
|
||||
|
||||
// fwpmu.h
|
||||
var (
|
||||
fwpmLayerALEAuthConnectV4 = windows.GUID{Data1: 0xc38d57d1, Data2: 0x05a7, Data3: 0x4c33, Data4: [8]byte{0x90, 0x4f, 0x7f, 0xbc, 0xee, 0xe6, 0x0e, 0x82}}
|
||||
fwpmLayerALEAuthConnectV6 = windows.GUID{Data1: 0x4a72393b, Data2: 0x319f, Data3: 0x44bc, Data4: [8]byte{0x84, 0xc3, 0xba, 0x54, 0xdc, 0xb3, 0xb6, 0xb4}}
|
||||
fwpmLayerALEAuthRecvAcceptV4 = windows.GUID{Data1: 0xe1cd9fe7, Data2: 0xf4b5, Data3: 0x4273, Data4: [8]byte{0x96, 0xc0, 0x59, 0x2e, 0x48, 0x7b, 0x86, 0x50}}
|
||||
fwpmLayerALEAuthRecvAcceptV6 = windows.GUID{Data1: 0xa3b42c97, Data2: 0x9f04, Data3: 0x4672, Data4: [8]byte{0xb8, 0x7e, 0xce, 0xe9, 0xc4, 0x83, 0x25, 0x7f}}
|
||||
|
||||
fwpmConditionFlags = windows.GUID{Data1: 0x632ce23b, Data2: 0x5167, Data3: 0x435c, Data4: [8]byte{0x86, 0xd7, 0xe9, 0x03, 0x68, 0x4a, 0xa8, 0x0c}}
|
||||
fwpmConditionIPArrivalInterface = windows.GUID{Data1: 0x618a9b6d, Data2: 0x386b, Data3: 0x4136, Data4: [8]byte{0xad, 0x6e, 0xb5, 0x15, 0x87, 0xcf, 0xb1, 0xcd}}
|
||||
fwpmConditionIPLocalInterface = windows.GUID{Data1: 0x4cd62a49, Data2: 0x59c3, Data3: 0x4969, Data4: [8]byte{0xb7, 0xf3, 0xbd, 0xa5, 0xd3, 0x28, 0x90, 0xa4}}
|
||||
fwpmConditionIPLocalPort = windows.GUID{Data1: 0x0c1ba1af, Data2: 0x5765, Data3: 0x453f, Data4: [8]byte{0xaf, 0x22, 0xa8, 0xf7, 0x91, 0xac, 0x77, 0x5b}} // also FWPM_CONDITION_ICMP_TYPE
|
||||
fwpmConditionIPNexthopInterface = windows.GUID{Data1: 0x93ae8f5b, Data2: 0x7f6f, Data3: 0x4719, Data4: [8]byte{0x98, 0xc8, 0x14, 0xe9, 0x74, 0x29, 0xef, 0x04}}
|
||||
fwpmConditionIPProtocol = windows.GUID{Data1: 0x3971ef2b, Data2: 0x623e, Data3: 0x4f9a, Data4: [8]byte{0x8c, 0xb1, 0x6e, 0x79, 0xb8, 0x06, 0xb9, 0xa7}}
|
||||
fwpmConditionIPRemoteAddress = windows.GUID{Data1: 0xb235ae9a, Data2: 0x1d64, Data3: 0x49b8, Data4: [8]byte{0xa4, 0x4c, 0x5f, 0xf3, 0xd9, 0x09, 0x50, 0x45}}
|
||||
fwpmConditionIPRemotePort = windows.GUID{Data1: 0xc35a604d, Data2: 0xd22b, Data3: 0x4e1a, Data4: [8]byte{0x91, 0xb4, 0x68, 0xf6, 0x74, 0xee, 0x67, 0x4b}} // also FWPM_CONDITION_ICMP_CODE
|
||||
fwpmConditionALEAppID = windows.GUID{Data1: 0xd78e1e87, Data2: 0x8644, Data3: 0x4ea5, Data4: [8]byte{0x94, 0x37, 0xd8, 0x09, 0xec, 0xef, 0xc9, 0x71}}
|
||||
fwpmConditionALEUserID = windows.GUID{Data1: 0xaf043a0a, Data2: 0xb34d, Data3: 0x4f86, Data4: [8]byte{0x97, 0x9c, 0xc9, 0x03, 0x71, 0xaf, 0x6e, 0x66}}
|
||||
)
|
||||
|
||||
// dnsClientSID is the SID of Windows' DNS Client service, NT SERVICE\Dnscache.
|
||||
// Service SIDs derive from the service name, so it is the same everywhere (sc
|
||||
// showsid dnscache).
|
||||
const dnsClientSID = "S-1-5-80-859482183-879914841-863379149-1145462774-2388618682"
|
||||
|
||||
// ff02::1:2, where DHCPv6 clients send to. A package-level variable never
|
||||
// moves, so conditions may refer to it through uintptr.
|
||||
var ipv6AllDHCPv6Servers = [16]byte{0xff, 0x02, 13: 0x01, 15: 0x02}
|
||||
|
||||
type fwpByteBlob struct {
|
||||
size uint32
|
||||
data *byte
|
||||
}
|
||||
|
||||
// fwpValue0 is FWP_VALUE0 as well as FWP_CONDITION_VALUE0. Their union holds
|
||||
// a scalar of at most 32 bits, or a pointer for the larger types.
|
||||
type fwpValue0 struct {
|
||||
typ uint32
|
||||
value uintptr
|
||||
}
|
||||
|
||||
type fwpmDisplayData0 struct {
|
||||
name *uint16
|
||||
description *uint16
|
||||
}
|
||||
|
||||
type fwpmSession0 struct {
|
||||
sessionKey windows.GUID
|
||||
displayData fwpmDisplayData0
|
||||
flags uint32
|
||||
txnWaitTimeoutInMSec uint32
|
||||
processID uint32
|
||||
sid *windows.SID
|
||||
username *uint16
|
||||
kernelMode int32
|
||||
}
|
||||
|
||||
type fwpmSublayer0 struct {
|
||||
subLayerKey windows.GUID
|
||||
displayData fwpmDisplayData0
|
||||
flags uint32
|
||||
providerKey *windows.GUID
|
||||
providerData fwpByteBlob
|
||||
weight uint16
|
||||
}
|
||||
|
||||
type fwpmFilterCondition0 struct {
|
||||
fieldKey windows.GUID
|
||||
matchType uint32
|
||||
conditionValue fwpValue0
|
||||
}
|
||||
|
||||
type fwpmAction0 struct {
|
||||
typ uint32
|
||||
filterType windows.GUID
|
||||
}
|
||||
|
||||
type fwpmFilter0 struct {
|
||||
filterKey windows.GUID
|
||||
displayData fwpmDisplayData0
|
||||
flags uint32
|
||||
providerKey *windows.GUID
|
||||
providerData fwpByteBlob
|
||||
layerKey windows.GUID
|
||||
subLayerKey windows.GUID
|
||||
weight fwpValue0
|
||||
numFilterConditions uint32
|
||||
filterCondition *fwpmFilterCondition0
|
||||
action fwpmAction0
|
||||
_ uint32 // C aligns the following union to 8 bytes, as it holds a UINT64
|
||||
providerContextKey windows.GUID
|
||||
reserved *windows.GUID
|
||||
_ [8 - unsafe.Sizeof(uintptr(0))]byte // and filterId as well, also on 32-bit
|
||||
filterID uint64
|
||||
effectiveWeight fwpValue0
|
||||
}
|
||||
|
||||
// fwpmResult converts the DWORD status the Fwpm functions return.
|
||||
func fwpmResult(r1, _ uintptr, _ error) error {
|
||||
if r1 != 0 {
|
||||
return windows.Errno(r1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func utf16Ptr(s string) *uint16 {
|
||||
p, _ := windows.UTF16PtrFromString(s)
|
||||
return p
|
||||
}
|
||||
|
||||
func condition(field *windows.GUID, typ uint32, value uintptr) fwpmFilterCondition0 {
|
||||
return fwpmFilterCondition0{
|
||||
fieldKey: *field,
|
||||
matchType: fwpMatchEqual,
|
||||
conditionValue: fwpValue0{typ: typ, value: value},
|
||||
}
|
||||
}
|
||||
|
||||
// blockLeaks keeps traffic from leaving through interfaces other than tun,
|
||||
// for every program but Xray itself, whose outbounds (DNS included) use the
|
||||
// other interfaces on purpose:
|
||||
//
|
||||
// - dns: DNS (port 53) may only go through the TUN. Windows sends a name
|
||||
// query to the DNS servers of all interfaces, not only to those of the TUN:
|
||||
// to the first server of each interface, then to all of them when no answer
|
||||
// arrives within a second or two. It sends the queries for the servers of
|
||||
// an interface out through that interface, whatever the routes say, and
|
||||
// other programs reach an on-link resolver, like 192.168.1.1 from DHCP,
|
||||
// through its LAN route, which is more specific than the TUN's default
|
||||
// route. Since Windows 11 and Server 2022, Windows may also send its
|
||||
// queries over HTTPS or TLS, so there its DNS Client service may not
|
||||
// connect outside the TUN at all, except for name resolution on the local
|
||||
// link (mDNS, LLMNR).
|
||||
// - ipv4, ipv6: no IPv4, or no IPv6, at all, in either direction, for a TUN
|
||||
// that no route of it leads to, except loopback and what Windows itself
|
||||
// needs on the local link (DHCP, and for IPv6 neighbor and multicast
|
||||
// listener discovery), none of which can leave it. The TUN carries what
|
||||
// is routed to it even without an address of that IP version in gateway:
|
||||
// Windows gives it link-local ones itself, an IPv6 one at once, an IPv4
|
||||
// one from 169.254.0.0/16 after some seconds (until then, IPv4 routed to
|
||||
// the TUN is unreachable).
|
||||
//
|
||||
// The filters live in a dynamic WFP session: closing the returned engine handle
|
||||
// with closeWFPEngine deletes them, and so does Windows when the process dies.
|
||||
func blockLeaks(tun winipcfg.LUID, dns, ipv4, ipv6 bool) (windows.Handle, error) {
|
||||
engine, err := openWFPEngine()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := fwpmResult(procFwpmTransactionBegin0.Call(uintptr(engine), 0)); err != nil {
|
||||
closeWFPEngine(engine)
|
||||
return 0, errors.New("FwpmTransactionBegin0 failed").Base(err)
|
||||
}
|
||||
err = addLeakFilters(engine, tun, dns, ipv4, ipv6)
|
||||
if err == nil {
|
||||
if err = fwpmResult(procFwpmTransactionCommit0.Call(uintptr(engine))); err != nil {
|
||||
err = errors.New("FwpmTransactionCommit0 failed").Base(err)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
procFwpmTransactionAbort0.Call(uintptr(engine))
|
||||
closeWFPEngine(engine)
|
||||
return 0, err
|
||||
}
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func openWFPEngine() (windows.Handle, error) {
|
||||
if err := modfwpuclnt.Load(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// txnWaitTimeoutInMSec stays 0 for BFE's default, so that a transaction
|
||||
// held by another program cannot hang the start forever.
|
||||
session := fwpmSession0{
|
||||
displayData: fwpmDisplayData0{name: utf16Ptr("Xray TUN")},
|
||||
flags: fwpmSessionFlagDynamic,
|
||||
}
|
||||
var engine windows.Handle
|
||||
if err := fwpmResult(procFwpmEngineOpen0.Call(0, rpcCAuthnWinNT, 0, uintptr(unsafe.Pointer(&session)), uintptr(unsafe.Pointer(&engine)))); err != nil {
|
||||
return 0, errors.New("FwpmEngineOpen0 failed").Base(err)
|
||||
}
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func closeWFPEngine(engine windows.Handle) {
|
||||
procFwpmEngineClose0.Call(uintptr(engine))
|
||||
}
|
||||
|
||||
// addLeakFilters adds the filters of blockLeaks in a sublayer of their own.
|
||||
// blockLeaks runs it in a transaction, so that they take effect all at once.
|
||||
func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, dns, ipv4, ipv6 bool) error {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exePath, err := windows.UTF16PtrFromString(exe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var appID *fwpByteBlob
|
||||
if err := fwpmResult(procFwpmGetAppIdFromFileName0.Call(uintptr(unsafe.Pointer(exePath)), uintptr(unsafe.Pointer(&appID)))); err != nil {
|
||||
return errors.New("FwpmGetAppIdFromFileName0 failed for ", exe).Base(err)
|
||||
}
|
||||
defer func() { procFwpmFreeMemory0.Call(uintptr(unsafe.Pointer(&appID))) }()
|
||||
|
||||
sublayer := fwpmSublayer0{
|
||||
displayData: fwpmDisplayData0{name: utf16Ptr("Xray TUN")},
|
||||
weight: 0xffff,
|
||||
}
|
||||
if sublayer.subLayerKey, err = windows.GenerateGUID(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fwpmResult(procFwpmSubLayerAdd0.Call(uintptr(engine), uintptr(unsafe.Pointer(&sublayer)), 0)); err != nil {
|
||||
return errors.New("FwpmSubLayerAdd0 failed").Base(err)
|
||||
}
|
||||
add := func(layer *windows.GUID, name string, flags, action uint32, weight uint8, conditions ...fwpmFilterCondition0) error {
|
||||
return addFilter(engine, &sublayer.subLayerKey, layer, "Xray TUN: "+name, flags, action, weight, conditions...)
|
||||
}
|
||||
|
||||
var pinner runtime.Pinner
|
||||
defer pinner.Unpin()
|
||||
tunLUID := new(uint64)
|
||||
*tunLUID = uint64(tun)
|
||||
pinner.Pin(tunLUID) // the condition only holds it as uintptr
|
||||
|
||||
// The heaviest matching filter of a sublayer decides. All sublayers have
|
||||
// their say, though, and a block in any of them beats a permit, unless
|
||||
// the permit is hard: it clears the action right, and then the blocks of
|
||||
// lower sublayers, Windows Firewall rules among them, no longer override
|
||||
// it, only a callout's veto does. Xray's own connections out get such a
|
||||
// hard permit. Connections from outside to Xray get an ordinary one, so
|
||||
// that firewalls keep guarding its inbounds.
|
||||
self := condition(&fwpmConditionALEAppID, fwpByteBlobType, uintptr(unsafe.Pointer(appID)))
|
||||
dns53 := condition(&fwpmConditionIPRemotePort, fwpUint16, 53)
|
||||
// DNS goes through the TUN when its local address is the TUN's, and it
|
||||
// also leaves, or arrives, through the TUN. The local address alone
|
||||
// decides by default, but with weak host sending or receiving enabled,
|
||||
// packets of the TUN's address can use other interfaces. (The next hop,
|
||||
// the interface replies would leave by, is not known for arriving ones.)
|
||||
onTUN := func(field *windows.GUID) fwpmFilterCondition0 {
|
||||
return condition(field, fwpUint64, uintptr(unsafe.Pointer(tunLUID)))
|
||||
}
|
||||
out := []fwpmFilterCondition0{dns53, onTUN(&fwpmConditionIPLocalInterface), onTUN(&fwpmConditionIPNexthopInterface)}
|
||||
in := []fwpmFilterCondition0{dns53, onTUN(&fwpmConditionIPLocalInterface), onTUN(&fwpmConditionIPArrivalInterface)}
|
||||
for _, layer := range []struct {
|
||||
key *windows.GUID
|
||||
selfFlags uint32
|
||||
throughTUN []fwpmFilterCondition0
|
||||
}{
|
||||
{&fwpmLayerALEAuthConnectV4, fwpmFilterFlagClearActionRight, out},
|
||||
{&fwpmLayerALEAuthRecvAcceptV4, 0, in},
|
||||
{&fwpmLayerALEAuthConnectV6, fwpmFilterFlagClearActionRight, out},
|
||||
{&fwpmLayerALEAuthRecvAcceptV6, 0, in},
|
||||
} {
|
||||
if err := add(layer.key, "permit Xray", layer.selfFlags, fwpActionPermit, 4, self); err != nil {
|
||||
return err
|
||||
}
|
||||
if dns {
|
||||
if err := add(layer.key, "permit DNS through the TUN", 0, fwpActionPermit, 3, layer.throughTUN...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := add(layer.key, "block DNS", 0, fwpActionBlock, 2, dns53); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Since Windows 11 and Server 2022 (build 20348), the DNS Client service
|
||||
// may also send the queries for an interface's servers over HTTPS or TLS,
|
||||
// out through that interface and to any port. So there it may only
|
||||
// connect through the TUN, except for mDNS and LLMNR, which stay on the
|
||||
// local link (over an IP version only while it is not blocked altogether).
|
||||
// Earlier versions only query port 53, and may run the service in one
|
||||
// process with others, which the filters would catch as well. Like
|
||||
// Windows Firewall's rules for it, they recognize the service by its SID,
|
||||
// which Windows puts in the token of its process: the security descriptor
|
||||
// grants that SID the right to match (FWP_ACTRL_MATCH_FILTER, CC in SDDL).
|
||||
if _, _, build := windows.RtlGetNtVersionNumbers(); dns && build >= 20348 {
|
||||
sd, err := windows.SecurityDescriptorFromString("O:SYG:SYD:(A;;CCRC;;;" + dnsClientSID + ")")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sdBlob := &fwpByteBlob{size: sd.Length(), data: (*byte)(unsafe.Pointer(sd))}
|
||||
pinner.Pin(sdBlob) // the condition only holds it as uintptr
|
||||
dnsClient := condition(&fwpmConditionALEUserID, fwpSecurityDescriptorType, uintptr(unsafe.Pointer(sdBlob)))
|
||||
// Conditions on the same field match when any of them does.
|
||||
mdnsLLMNR := []fwpmFilterCondition0{dnsClient, condition(&fwpmConditionIPRemotePort, fwpUint16, 5353), condition(&fwpmConditionIPRemotePort, fwpUint16, 5355)}
|
||||
for _, layer := range []struct {
|
||||
key *windows.GUID
|
||||
localLink bool
|
||||
}{
|
||||
{&fwpmLayerALEAuthConnectV4, !ipv4},
|
||||
{&fwpmLayerALEAuthConnectV6, !ipv6},
|
||||
} {
|
||||
if err := add(layer.key, "permit the DNS Client service through the TUN", 0, fwpActionPermit, 3, dnsClient, onTUN(&fwpmConditionIPLocalInterface), onTUN(&fwpmConditionIPNexthopInterface)); err != nil {
|
||||
return err
|
||||
}
|
||||
if layer.localLink {
|
||||
if err := add(layer.key, "permit the DNS Client service's mDNS and LLMNR", 0, fwpActionPermit, 3, mdnsLLMNR...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := add(layer.key, "block the DNS Client service", 0, fwpActionBlock, 2, dnsClient); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Both directions: replies to a connection accepted from outside would
|
||||
// leave through the physical link as well.
|
||||
loopback := fwpmFilterCondition0{
|
||||
fieldKey: fwpmConditionFlags,
|
||||
matchType: fwpMatchFlagsAllSet,
|
||||
conditionValue: fwpValue0{typ: fwpUint32, value: fwpConditionFlagIsLoopback},
|
||||
}
|
||||
if ipv4 {
|
||||
// DHCP keeps the addresses of the other interfaces, which Xray's own
|
||||
// connections use.
|
||||
dhcp := []fwpmFilterCondition0{
|
||||
condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_UDP),
|
||||
condition(&fwpmConditionIPLocalPort, fwpUint16, 68),
|
||||
condition(&fwpmConditionIPRemotePort, fwpUint16, 67),
|
||||
}
|
||||
for _, layer := range []*windows.GUID{&fwpmLayerALEAuthConnectV4, &fwpmLayerALEAuthRecvAcceptV4} {
|
||||
if err := add(layer, "permit IPv4 loopback", 0, fwpActionPermit, 1, loopback); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := add(layer, "permit DHCP", 0, fwpActionPermit, 1, dhcp...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := add(layer, "block IPv4", 0, fwpActionBlock, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if ipv6 {
|
||||
// Neighbor and multicast listener discovery, ICMPv6 130-137 and 143,
|
||||
// whose type and code sit where the local and remote port are.
|
||||
discovery := []fwpmFilterCondition0{condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_ICMPV6)}
|
||||
for _, typ := range []uintptr{130, 131, 132, 133, 134, 135, 136, 137, 143} {
|
||||
discovery = append(discovery, condition(&fwpmConditionIPLocalPort, fwpUint16, typ))
|
||||
}
|
||||
discovery = append(discovery, condition(&fwpmConditionIPRemotePort, fwpUint16, 0))
|
||||
dhcpv6 := []fwpmFilterCondition0{
|
||||
condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_UDP),
|
||||
condition(&fwpmConditionIPLocalPort, fwpUint16, 546),
|
||||
condition(&fwpmConditionIPRemotePort, fwpUint16, 547),
|
||||
}
|
||||
for _, direction := range []struct {
|
||||
layer *windows.GUID
|
||||
dhcpv6 []fwpmFilterCondition0
|
||||
}{
|
||||
// The client sends to the servers' multicast address, and they
|
||||
// answer from their own.
|
||||
{&fwpmLayerALEAuthConnectV6, slices.Concat(dhcpv6, []fwpmFilterCondition0{condition(&fwpmConditionIPRemoteAddress, fwpByteArray16Type, uintptr(unsafe.Pointer(&ipv6AllDHCPv6Servers)))})},
|
||||
{&fwpmLayerALEAuthRecvAcceptV6, dhcpv6},
|
||||
} {
|
||||
if err := add(direction.layer, "permit IPv6 loopback", 0, fwpActionPermit, 1, loopback); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := add(direction.layer, "permit IPv6 neighbor and multicast listener discovery", 0, fwpActionPermit, 1, discovery...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := add(direction.layer, "permit DHCPv6", 0, fwpActionPermit, 1, direction.dhcpv6...); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := add(direction.layer, "block IPv6", 0, fwpActionBlock, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addFilter(engine windows.Handle, sublayer, layer *windows.GUID, name string, flags, action uint32, weight uint8, conditions ...fwpmFilterCondition0) error {
|
||||
filter := fwpmFilter0{
|
||||
displayData: fwpmDisplayData0{name: utf16Ptr(name)},
|
||||
flags: flags,
|
||||
layerKey: *layer,
|
||||
subLayerKey: *sublayer,
|
||||
weight: fwpValue0{typ: fwpUint8, value: uintptr(weight)},
|
||||
numFilterConditions: uint32(len(conditions)),
|
||||
action: fwpmAction0{typ: action},
|
||||
}
|
||||
if len(conditions) > 0 {
|
||||
filter.filterCondition = &conditions[0]
|
||||
}
|
||||
if err := fwpmResult(procFwpmFilterAdd0.Call(uintptr(engine), uintptr(unsafe.Pointer(&filter)), 0, 0)); err != nil {
|
||||
return errors.New("FwpmFilterAdd0 failed for ", name).Base(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dnsOutsideTUN returns the servers outside all of prefixes, the TUN's own
|
||||
// subnets and routes: queries to them cannot go through the TUN.
|
||||
func dnsOutsideTUN(servers []netip.Addr, prefixes []netip.Prefix) []netip.Addr {
|
||||
var outside []netip.Addr
|
||||
for _, server := range servers {
|
||||
server = server.Unmap()
|
||||
if !slices.ContainsFunc(prefixes, func(p netip.Prefix) bool { return p.Contains(server) }) {
|
||||
outside = append(outside, server)
|
||||
}
|
||||
}
|
||||
return outside
|
||||
}
|
||||
|
||||
// flushDNSCache drops the answers Windows cached so far, like ipconfig
|
||||
// /flushdns, so that names get resolved again with the current DNS setup.
|
||||
func flushDNSCache() error {
|
||||
if err := procDnsFlushResolverCache.Find(); err != nil {
|
||||
return err
|
||||
}
|
||||
if r, _, err := procDnsFlushResolverCache.Call(); r == 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
//go:build windows
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"context"
|
||||
go_errors "errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
)
|
||||
|
||||
// The WFP structures are handed to fwpuclnt.dll as they are, so their layout
|
||||
// has to match what MSVC produces for 64-bit and for 32-bit Windows.
|
||||
func TestWFPStructLayout(t *testing.T) {
|
||||
check := func(name string, got, want64, want32 []uintptr) {
|
||||
t.Helper()
|
||||
want := want32
|
||||
if unsafe.Sizeof(uintptr(0)) == 8 {
|
||||
want = want64
|
||||
}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Errorf("%s: size and offsets are %v, want %v", name, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
var blob fwpByteBlob
|
||||
check("FWP_BYTE_BLOB",
|
||||
[]uintptr{unsafe.Sizeof(blob), unsafe.Offsetof(blob.data)},
|
||||
[]uintptr{16, 8}, []uintptr{8, 4})
|
||||
|
||||
var value fwpValue0
|
||||
check("FWP_VALUE0",
|
||||
[]uintptr{unsafe.Sizeof(value), unsafe.Offsetof(value.value)},
|
||||
[]uintptr{16, 8}, []uintptr{8, 4})
|
||||
|
||||
var display fwpmDisplayData0
|
||||
check("FWPM_DISPLAY_DATA0",
|
||||
[]uintptr{unsafe.Sizeof(display), unsafe.Offsetof(display.description)},
|
||||
[]uintptr{16, 8}, []uintptr{8, 4})
|
||||
|
||||
var action fwpmAction0
|
||||
check("FWPM_ACTION0",
|
||||
[]uintptr{unsafe.Sizeof(action), unsafe.Offsetof(action.filterType)},
|
||||
[]uintptr{20, 4}, []uintptr{20, 4})
|
||||
|
||||
var cond fwpmFilterCondition0
|
||||
check("FWPM_FILTER_CONDITION0",
|
||||
[]uintptr{unsafe.Sizeof(cond), unsafe.Offsetof(cond.matchType), unsafe.Offsetof(cond.conditionValue)},
|
||||
[]uintptr{40, 16, 24}, []uintptr{28, 16, 20})
|
||||
|
||||
var session fwpmSession0
|
||||
check("FWPM_SESSION0",
|
||||
[]uintptr{
|
||||
unsafe.Sizeof(session), unsafe.Offsetof(session.displayData), unsafe.Offsetof(session.flags),
|
||||
unsafe.Offsetof(session.txnWaitTimeoutInMSec), unsafe.Offsetof(session.processID), unsafe.Offsetof(session.sid),
|
||||
unsafe.Offsetof(session.username), unsafe.Offsetof(session.kernelMode),
|
||||
},
|
||||
[]uintptr{72, 16, 32, 36, 40, 48, 56, 64},
|
||||
[]uintptr{48, 16, 24, 28, 32, 36, 40, 44})
|
||||
|
||||
var sublayer fwpmSublayer0
|
||||
check("FWPM_SUBLAYER0",
|
||||
[]uintptr{
|
||||
unsafe.Sizeof(sublayer), unsafe.Offsetof(sublayer.displayData), unsafe.Offsetof(sublayer.flags),
|
||||
unsafe.Offsetof(sublayer.providerKey), unsafe.Offsetof(sublayer.providerData), unsafe.Offsetof(sublayer.weight),
|
||||
},
|
||||
[]uintptr{72, 16, 32, 40, 48, 64},
|
||||
[]uintptr{44, 16, 24, 28, 32, 40})
|
||||
|
||||
var filter fwpmFilter0
|
||||
check("FWPM_FILTER0",
|
||||
[]uintptr{
|
||||
unsafe.Sizeof(filter), unsafe.Offsetof(filter.displayData), unsafe.Offsetof(filter.flags),
|
||||
unsafe.Offsetof(filter.providerKey), unsafe.Offsetof(filter.providerData), unsafe.Offsetof(filter.layerKey),
|
||||
unsafe.Offsetof(filter.subLayerKey), unsafe.Offsetof(filter.weight), unsafe.Offsetof(filter.numFilterConditions),
|
||||
unsafe.Offsetof(filter.filterCondition), unsafe.Offsetof(filter.action), unsafe.Offsetof(filter.providerContextKey),
|
||||
unsafe.Offsetof(filter.reserved), unsafe.Offsetof(filter.filterID), unsafe.Offsetof(filter.effectiveWeight),
|
||||
},
|
||||
[]uintptr{200, 16, 32, 40, 48, 64, 80, 96, 112, 120, 128, 152, 168, 176, 184},
|
||||
[]uintptr{152, 16, 24, 28, 32, 40, 56, 72, 80, 84, 88, 112, 128, 136, 144})
|
||||
}
|
||||
|
||||
// TestLeakFiltersAccepted has WFP validate the filters by adding them inside a
|
||||
// transaction that is then aborted, which leaves the system untouched. Adding
|
||||
// filters requires an elevated process.
|
||||
func TestLeakFiltersAccepted(t *testing.T) {
|
||||
skipUnlessElevated := func(err error) {
|
||||
t.Helper()
|
||||
if go_errors.Is(err, windows.ERROR_ACCESS_DENIED) {
|
||||
t.Skipf("WFP filters can only be added by an elevated process: %v", err)
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
engine, err := openWFPEngine()
|
||||
if err != nil {
|
||||
skipUnlessElevated(err)
|
||||
}
|
||||
defer closeWFPEngine(engine)
|
||||
if err := fwpmResult(procFwpmTransactionBegin0.Call(uintptr(engine), 0)); err != nil {
|
||||
skipUnlessElevated(err)
|
||||
}
|
||||
defer procFwpmTransactionAbort0.Call(uintptr(engine))
|
||||
|
||||
// Any interface stands in for the TUN; the loopback one always exists.
|
||||
loopback, err := winipcfg.LUIDFromIndex(1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := addLeakFilters(engine, loopback, true, true, true); err != nil {
|
||||
skipUnlessElevated(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSClientSID(t *testing.T) {
|
||||
sid, _, _, err := windows.LookupSID("", `NT SERVICE\Dnscache`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sid.String() != dnsClientSID {
|
||||
t.Errorf(`NT SERVICE\Dnscache is %v, not %v`, sid, dnsClientSID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSOutsideTUN(t *testing.T) {
|
||||
prefixes := []netip.Prefix{
|
||||
netip.MustParsePrefix("198.51.100.1/30"), // gateway, not masked
|
||||
netip.MustParsePrefix("203.0.113.0/24"), // route
|
||||
}
|
||||
servers := []netip.Addr{
|
||||
netip.MustParseAddr("198.51.100.2"),
|
||||
netip.MustParseAddr("203.0.113.53"),
|
||||
netip.MustParseAddr("::ffff:203.0.113.54"),
|
||||
netip.MustParseAddr("8.8.8.8"),
|
||||
netip.MustParseAddr("2001:db8::53"),
|
||||
}
|
||||
want := []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("2001:db8::53")}
|
||||
if got := dnsOutsideTUN(servers, prefixes); !slices.Equal(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveOnOwn(t *testing.T) {
|
||||
internet.SkipDNSServers([]netip.Addr{netip.MustParseAddr("::ffff:203.0.113.53")})
|
||||
t.Cleanup(func() { internet.SkipDNSServers(nil) })
|
||||
preferGo, dial := net.DefaultResolver.PreferGo, net.DefaultResolver.Dial
|
||||
saved := resolveOnOwn()
|
||||
t.Cleanup(saved.restore)
|
||||
if !net.DefaultResolver.PreferGo || net.DefaultResolver.Dial == nil {
|
||||
t.Fatal("net.DefaultResolver is unchanged")
|
||||
}
|
||||
if _, err := net.DefaultResolver.Dial(context.Background(), "udp", "203.0.113.53:53"); err == nil {
|
||||
t.Error("the TUN's DNS server was not skipped")
|
||||
}
|
||||
conn, err := net.DefaultResolver.Dial(context.Background(), "udp", "127.0.0.1:53")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conn.Close()
|
||||
saved.restore()
|
||||
if net.DefaultResolver.PreferGo != preferGo || (net.DefaultResolver.Dial == nil) != (dial == nil) {
|
||||
t.Error("net.DefaultResolver is not restored")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTunOnlyDNS checks that a DNS server another interface uses as well is
|
||||
// not skipped, while one of the TUN alone is.
|
||||
func TestTunOnlyDNS(t *testing.T) {
|
||||
adapters, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludeGateways)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var other netip.Addr
|
||||
for _, adapter := range adapters {
|
||||
if adapter.OperStatus == winipcfg.IfOperStatusUp && adapter.FirstGatewayAddress != nil && adapter.FirstDNSServerAddress != nil {
|
||||
other, _ = netip.AddrFromSlice(adapter.FirstDNSServerAddress.Address.IP())
|
||||
other = other.Unmap()
|
||||
break
|
||||
}
|
||||
}
|
||||
if !other.IsValid() {
|
||||
t.Skip("no interface with a gateway and a DNS server")
|
||||
}
|
||||
tunOnly := netip.MustParseAddr("203.0.113.53")
|
||||
// LUID 0 is no interface, so every one counts as another.
|
||||
got, err := tunOnlyDNS(0, []netip.Addr{other, tunOnly})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !slices.Equal(got, []netip.Addr{tunOnly}) {
|
||||
t.Errorf("got %v, want [%v]", got, tunOnly)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlushDNSCache(t *testing.T) {
|
||||
if err := flushDNSCache(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -78,11 +78,11 @@ func (u *udpConnectionHandler) HandlePacket(src net.Destination, dst net.Destina
|
||||
}
|
||||
}
|
||||
|
||||
func (u *udpConnectionHandler) connectionFinished(src net.Destination) {
|
||||
func (u *udpConnectionHandler) connectionFinished(conn *udpConn) {
|
||||
u.Lock()
|
||||
conn, found := u.udpConns[src]
|
||||
if found {
|
||||
delete(u.udpConns, src)
|
||||
// Close runs twice per flow; a newer conn may already own this src.
|
||||
if u.udpConns[conn.src] == conn {
|
||||
delete(u.udpConns, conn.src)
|
||||
close(conn.egress)
|
||||
}
|
||||
u.Unlock()
|
||||
@@ -161,7 +161,7 @@ func (c *udpConn) Write(p []byte) (int, error) {
|
||||
}
|
||||
|
||||
func (c *udpConn) Close() error {
|
||||
c.handler.connectionFinished(c.src)
|
||||
c.handler.connectionFinished(c)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user