mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-09-23 01:27:04 +00:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcdfc57ccd | ||
|
|
3461c511aa | ||
|
|
c412e77a9b | ||
|
|
ccb69ea5e2 | ||
|
|
52a412d9e2 | ||
|
|
18a1b5042a | ||
|
|
c26d2eda24 | ||
|
|
a1bf968be9 | ||
|
|
c037ccd98d | ||
|
|
37ceb8b4b6 | ||
|
|
fd2ca74822 |
@@ -67,9 +67,7 @@ jobs:
|
||||
check-latest: true
|
||||
cache: false
|
||||
- name: Check Format
|
||||
run: |
|
||||
go install -v mvdan.cc/gofumpt@latest
|
||||
go run ./infra/vformat/main.go -mode check -pwd ./
|
||||
run: go run ./infra/vformat/main.go -mode check -pwd ./
|
||||
|
||||
test:
|
||||
needs: check-assets
|
||||
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
)
|
||||
|
||||
type windowsReader struct {
|
||||
bufs []syscall.WSABuf
|
||||
bufs []syscall.WSABuf
|
||||
ready bool
|
||||
}
|
||||
|
||||
func (r *windowsReader) Init(bs []*Buffer) {
|
||||
@@ -15,6 +16,7 @@ func (r *windowsReader) Init(bs []*Buffer) {
|
||||
for _, b := range bs {
|
||||
r.bufs = append(r.bufs, syscall.WSABuf{Len: uint32(Size), Buf: &b.v[0]})
|
||||
}
|
||||
r.ready = false
|
||||
}
|
||||
|
||||
func (r *windowsReader) Clear() {
|
||||
@@ -25,6 +27,14 @@ func (r *windowsReader) Clear() {
|
||||
}
|
||||
|
||||
func (r *windowsReader) Read(fd uintptr) int32 {
|
||||
// On the first invocation, we return -1 to indicate "not ready"
|
||||
// to make rawConn.Read wait for readability using the runtime's own mechanism
|
||||
// because syscall.WSARecv() is a blocking call when used with nil OVERLAPPED
|
||||
if !r.ready {
|
||||
r.ready = true
|
||||
return -1
|
||||
}
|
||||
|
||||
var nBytes uint32
|
||||
var flags uint32
|
||||
err := syscall.WSARecv(syscall.Handle(fd), &r.bufs[0], uint32(len(r.bufs)), &nBytes, &flags, nil, nil)
|
||||
|
||||
@@ -10,12 +10,12 @@ import (
|
||||
|
||||
// [,)
|
||||
func RandBetween(from int64, to int64) int64 {
|
||||
if from == to {
|
||||
return from
|
||||
}
|
||||
if from > to {
|
||||
from, to = to, from
|
||||
}
|
||||
if d := to - from; d == 0 || d == 1 {
|
||||
return from
|
||||
}
|
||||
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
|
||||
return from + bigInt.Int64()
|
||||
}
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@ import (
|
||||
|
||||
var (
|
||||
Version_x byte = 26
|
||||
Version_y byte = 7
|
||||
Version_z byte = 28
|
||||
Version_y byte = 9
|
||||
Version_z byte = 9
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/xtls/xray-core
|
||||
|
||||
go 1.26
|
||||
go 1.27
|
||||
|
||||
require (
|
||||
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e
|
||||
@@ -37,6 +37,7 @@ require (
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0
|
||||
h12.io/socks v1.0.3
|
||||
lukechampine.com/blake3 v1.4.1
|
||||
mvdan.cc/gofumpt v0.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -48,7 +49,6 @@ require (
|
||||
github.com/juju/ratelimit v1.0.2 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/koron/go-ssdp v0.0.4 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/libp2p/go-netroute v0.2.1 // indirect
|
||||
github.com/pion/dtls/v3 v3.1.5 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
@@ -59,6 +59,7 @@ require (
|
||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.49.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
@@ -6,13 +6,14 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA=
|
||||
github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
|
||||
github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
|
||||
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
|
||||
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
@@ -73,8 +74,8 @@ github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
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=
|
||||
@@ -147,6 +148,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -176,3 +179,5 @@ h12.io/socks v1.0.3 h1:Ka3qaQewws4j4/eDQnOdpr4wXsC//dXtWvftlIcCQUo=
|
||||
h12.io/socks v1.0.3/go.mod h1:AIhxy1jOId/XCz9BO+EIgNL2rQiPTBNnOfnVnQ+3Eck=
|
||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||
mvdan.cc/gofumpt v0.12.0 h1:1Lbudkz2kpM9Cjz2pL4M19u7q+GaEhCTNf7N9mfpcho=
|
||||
mvdan.cc/gofumpt v0.12.0/go.mod h1:SmBHHrljiZu/uoypeKup3rFzP6eoC9UwCp2iH5E3jZA=
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
googleuuid "github.com/google/uuid"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/fragment"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/realm"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/salamander"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/sudoku"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/udphop"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xdns"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xicmp"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/xmc"
|
||||
@@ -83,6 +85,7 @@ var (
|
||||
"xdns": func() interface{} { return new(Xdns) },
|
||||
"xicmp": func() interface{} { return new(Xicmp) },
|
||||
"realm": func() interface{} { return new(Realm) },
|
||||
"udphop": func() interface{} { return new(UDPHop) },
|
||||
}, "type", "settings")
|
||||
)
|
||||
|
||||
@@ -905,6 +908,62 @@ func (c *Realm) Build() (proto.Message, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
type UDPHop struct {
|
||||
Sockopt *SocketConfig `json:"sockopt"`
|
||||
Mode string `json:"mode"`
|
||||
Interval Int32Range `json:"interval"`
|
||||
RemotePorts PortList `json:"remotePorts"`
|
||||
RemoteIPs []string `json:"remoteIPs"`
|
||||
}
|
||||
|
||||
func (c *UDPHop) Build() (proto.Message, error) {
|
||||
var sockopt *internet.SocketConfig
|
||||
if c.Sockopt != nil {
|
||||
var err error
|
||||
sockopt, err = c.Sockopt.Build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var local, remote, remoteOnce bool
|
||||
for _, mode := range strings.Split(c.Mode, ",") {
|
||||
switch strings.ToLower(mode) {
|
||||
case "intervallocal":
|
||||
local = true
|
||||
case "intervalremote":
|
||||
remote = true
|
||||
case "perconnremote":
|
||||
remoteOnce = true
|
||||
default:
|
||||
return nil, errors.New("invalid mode ", mode)
|
||||
}
|
||||
}
|
||||
var remoteIPs []string
|
||||
for _, ip := range c.RemoteIPs {
|
||||
prefix, err := netip.ParsePrefix(ip)
|
||||
if err == nil {
|
||||
remoteIPs = append(remoteIPs, prefix.String())
|
||||
continue
|
||||
}
|
||||
addr, err := netip.ParseAddr(ip)
|
||||
if err == nil {
|
||||
remoteIPs = append(remoteIPs, netip.PrefixFrom(addr, addr.BitLen()).String())
|
||||
continue
|
||||
}
|
||||
return nil, errors.New("invalid ip ", ip)
|
||||
}
|
||||
return &udphop.Config{
|
||||
Sockopt: sockopt,
|
||||
Local: local,
|
||||
Remote: remote,
|
||||
RemoteOnce: remoteOnce,
|
||||
IntervalMin: int64(c.Interval.From),
|
||||
IntervalMax: int64(c.Interval.To),
|
||||
RemotePorts: c.RemotePorts.Build().Ports(),
|
||||
RemoteIPs: remoteIPs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Mask struct {
|
||||
Type string `json:"type"`
|
||||
Settings *json.RawMessage `json:"settings"`
|
||||
@@ -938,7 +997,6 @@ type QuicParamsConfig struct {
|
||||
BrutalUp Bandwidth `json:"brutalUp"`
|
||||
BrutalDown Bandwidth `json:"brutalDown"`
|
||||
BrutalDisableLossCompensation bool `json:"brutalDisableLossCompensation"`
|
||||
UdpHop UdpHop `json:"udpHop"`
|
||||
InitStreamReceiveWindow uint64 `json:"initStreamReceiveWindow"`
|
||||
MaxStreamReceiveWindow uint64 `json:"maxStreamReceiveWindow"`
|
||||
InitConnectionReceiveWindow uint64 `json:"initConnectionReceiveWindow"`
|
||||
|
||||
@@ -36,6 +36,8 @@ func (p TransportProtocol) Build() (string, error) {
|
||||
return "", errors.PrintRemovedFeatureError("QUIC transport (without web service, etc.)", "XHTTP stream-one H3")
|
||||
case "hysteria":
|
||||
return "hysteria", nil
|
||||
case "xdrive":
|
||||
return "xdrive", nil
|
||||
default:
|
||||
return "", errors.New("Config: unknown transport protocol: ", p)
|
||||
}
|
||||
@@ -59,6 +61,7 @@ type StreamConfig struct {
|
||||
WSSettings *WebSocketConfig `json:"wsSettings"`
|
||||
HTTPUPGRADESettings *HttpUpgradeConfig `json:"httpupgradeSettings"`
|
||||
HysteriaSettings *HysteriaConfig `json:"hysteriaSettings"`
|
||||
XDRIVESettings *XDriveConfig `json:"xdriveSettings"`
|
||||
SocketSettings *SocketConfig `json:"sockopt"`
|
||||
}
|
||||
|
||||
@@ -192,6 +195,16 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
||||
Settings: serial.ToTypedMessage(hs),
|
||||
})
|
||||
}
|
||||
if c.XDRIVESettings != nil {
|
||||
xs, err := c.XDRIVESettings.Build()
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to build XDRIVE config.").Base(err)
|
||||
}
|
||||
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
|
||||
ProtocolName: "xdrive",
|
||||
Settings: serial.ToTypedMessage(xs),
|
||||
})
|
||||
}
|
||||
if c.SocketSettings != nil {
|
||||
ss, err := c.SocketSettings.Build()
|
||||
if err != nil {
|
||||
@@ -253,10 +266,6 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
||||
return nil, errors.New("unknown congestion control: ", c.FinalMask.QuicParams.Congestion, ", valid values: reno, bbr, brutal, force-brutal")
|
||||
}
|
||||
|
||||
if (c.FinalMask.QuicParams.UdpHop.Interval.From != 0 && c.FinalMask.QuicParams.UdpHop.Interval.From < 5) || (c.FinalMask.QuicParams.UdpHop.Interval.To != 0 && c.FinalMask.QuicParams.UdpHop.Interval.To < 5) {
|
||||
return nil, errors.New("Interval must be at least 5")
|
||||
}
|
||||
|
||||
if c.FinalMask.QuicParams.InitStreamReceiveWindow > 0 && c.FinalMask.QuicParams.InitStreamReceiveWindow < 16384 {
|
||||
return nil, errors.New("InitStreamReceiveWindow must be at least 16384")
|
||||
}
|
||||
@@ -290,22 +299,17 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
||||
BrutalUp: up,
|
||||
BrutalDown: down,
|
||||
BrutalDisableLossCompensation: c.FinalMask.QuicParams.BrutalDisableLossCompensation,
|
||||
UdpHop: &internet.UdpHop{
|
||||
Ports: c.FinalMask.QuicParams.UdpHop.PortList.Build().Ports(),
|
||||
IntervalMin: int64(c.FinalMask.QuicParams.UdpHop.Interval.From),
|
||||
IntervalMax: int64(c.FinalMask.QuicParams.UdpHop.Interval.To),
|
||||
},
|
||||
InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow,
|
||||
MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow,
|
||||
InitConnReceiveWindow: c.FinalMask.QuicParams.InitConnectionReceiveWindow,
|
||||
MaxConnReceiveWindow: c.FinalMask.QuicParams.MaxConnectionReceiveWindow,
|
||||
MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout,
|
||||
KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod,
|
||||
DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery,
|
||||
DisableChromeParrot: c.FinalMask.QuicParams.DisableChromeParrot,
|
||||
DisableGSO: c.FinalMask.QuicParams.DisableGSO,
|
||||
MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams,
|
||||
DisableStatelessReset: c.FinalMask.QuicParams.DisableStatelessReset,
|
||||
InitStreamReceiveWindow: c.FinalMask.QuicParams.InitStreamReceiveWindow,
|
||||
MaxStreamReceiveWindow: c.FinalMask.QuicParams.MaxStreamReceiveWindow,
|
||||
InitConnReceiveWindow: c.FinalMask.QuicParams.InitConnectionReceiveWindow,
|
||||
MaxConnReceiveWindow: c.FinalMask.QuicParams.MaxConnectionReceiveWindow,
|
||||
MaxIdleTimeout: c.FinalMask.QuicParams.MaxIdleTimeout,
|
||||
KeepAlivePeriod: c.FinalMask.QuicParams.KeepAlivePeriod,
|
||||
DisablePathMtuDiscovery: c.FinalMask.QuicParams.DisablePathMTUDiscovery,
|
||||
DisableChromeParrot: c.FinalMask.QuicParams.DisableChromeParrot,
|
||||
DisableGSO: c.FinalMask.QuicParams.DisableGSO,
|
||||
MaxIncomingStreams: c.FinalMask.QuicParams.MaxIncomingStreams,
|
||||
DisableStatelessReset: c.FinalMask.QuicParams.DisableStatelessReset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"net/url"
|
||||
@@ -24,6 +23,7 @@ import (
|
||||
"github.com/xtls/xray-core/transport/internet/splithttp"
|
||||
"github.com/xtls/xray-core/transport/internet/tcp"
|
||||
"github.com/xtls/xray-core/transport/internet/websocket"
|
||||
"github.com/xtls/xray-core/transport/internet/xdrive"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -534,10 +534,6 @@ type KCPConfig struct {
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *KCPConfig) Build() (proto.Message, error) {
|
||||
if c.HeaderConfig != nil || c.Seed != nil {
|
||||
return nil, errors.PrintRemovedFeatureError("mkcp header & seed", "finalmask/udp header-* & mkcp-original & mkcp-aes128gcm")
|
||||
}
|
||||
|
||||
config := common.Must2(internet.CreateTransportConfig(kcp.ProtocolName)).(*kcp.Config)
|
||||
|
||||
if c.Mtu != nil {
|
||||
@@ -560,16 +556,16 @@ func (c *KCPConfig) Build() (proto.Message, error) {
|
||||
}
|
||||
|
||||
if config.Mtu < 21 {
|
||||
return nil, errors.New("Mtu must be at least 21").AtError()
|
||||
return nil, errors.New("MTU must be at least 21")
|
||||
}
|
||||
if config.Tti < 10 || config.Tti > 1000 {
|
||||
return nil, errors.New("invalid mKCP TTI: ", c.Tti).AtError()
|
||||
return nil, errors.New("TTI must be between 10 and 1000")
|
||||
}
|
||||
if config.CwndMultiplier < 1 {
|
||||
return nil, errors.New("CwndMultiplier must be at least 1").AtError()
|
||||
return nil, errors.New("CwndMultiplier must be at least 1")
|
||||
}
|
||||
if config.GetSendingBufferSize() == 0 {
|
||||
return nil, errors.New("MaxSendingWindow must be >= Mtu").AtError()
|
||||
return nil, errors.New("MaxSendingWindow must be at least ", config.Mtu)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
@@ -739,11 +735,6 @@ func (b Bandwidth) Bps() (uint64, error) {
|
||||
return uint64(val*float64(mul)) / 8, nil
|
||||
}
|
||||
|
||||
type UdpHop struct {
|
||||
PortList PortList `json:"ports"`
|
||||
Interval Int32Range `json:"interval"`
|
||||
}
|
||||
|
||||
type Masquerade struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
@@ -760,14 +751,8 @@ type Masquerade struct {
|
||||
}
|
||||
|
||||
type HysteriaConfig struct {
|
||||
Version int32 `json:"version"`
|
||||
Auth string `json:"auth"`
|
||||
|
||||
Congestion *string `json:"congestion"`
|
||||
Up *Bandwidth `json:"up"`
|
||||
Down *Bandwidth `json:"down"`
|
||||
UdpHop *UdpHop `json:"udphop"`
|
||||
|
||||
Version int32 `json:"version"`
|
||||
Auth string `json:"auth"`
|
||||
UdpIdleTimeout int64 `json:"udpIdleTimeout"`
|
||||
Masquerade Masquerade `json:"masquerade"`
|
||||
}
|
||||
@@ -777,10 +762,6 @@ func (c *HysteriaConfig) Build() (proto.Message, error) {
|
||||
return nil, errors.New("version != 2")
|
||||
}
|
||||
|
||||
if c.Congestion != nil || c.Up != nil || c.Down != nil || c.UdpHop != nil {
|
||||
errors.LogWarning(context.Background(), "congestion & up & down & udphop move to finalmask/quicParams")
|
||||
}
|
||||
|
||||
if c.UdpIdleTimeout != 0 && (c.UdpIdleTimeout < 2 || c.UdpIdleTimeout > 600) {
|
||||
return nil, errors.New("UdpIdleTimeout must be between 2 and 600")
|
||||
}
|
||||
@@ -814,3 +795,50 @@ func readFileOrString(f string, s []string) ([]byte, error) {
|
||||
}
|
||||
return nil, errors.New("both file and bytes are empty.")
|
||||
}
|
||||
|
||||
type XDriveConfig struct {
|
||||
RemoteFolder string `json:"remoteFolder"`
|
||||
Service string `json:"service"`
|
||||
Secrets []string `json:"secrets"`
|
||||
SegmentBytes uint32 `json:"segmentBytes"`
|
||||
FlushIntervalMs uint32 `json:"flushIntervalMs"`
|
||||
PollIntervalMs uint32 `json:"pollIntervalMs"`
|
||||
MaxPollIntervalMs uint32 `json:"maxPollIntervalMs"`
|
||||
SessionTTLSeconds uint32 `json:"sessionTtlSeconds"`
|
||||
Concurrency uint32 `json:"concurrency"`
|
||||
EagerWindowMs uint32 `json:"eagerWindowMs"`
|
||||
HoleTimeoutMs uint32 `json:"holeTimeoutMs"`
|
||||
Template json.RawMessage `json:"template"`
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *XDriveConfig) Build() (proto.Message, error) {
|
||||
switch c.Service {
|
||||
case "local":
|
||||
case "Google Drive":
|
||||
if len(c.Secrets) != 3 {
|
||||
return nil, errors.New("Google Drive needs 3 secrets in order of ClientID, ClientSecret, RefreshToken")
|
||||
}
|
||||
case "template":
|
||||
if len(c.Template) == 0 {
|
||||
return nil, errors.New(`service "template" needs a "template" object`)
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unsupported service")
|
||||
}
|
||||
config := &xdrive.Config{
|
||||
RemoteFolder: c.RemoteFolder,
|
||||
Service: c.Service,
|
||||
Secrets: c.Secrets,
|
||||
SegmentBytes: c.SegmentBytes,
|
||||
FlushIntervalMs: c.FlushIntervalMs,
|
||||
PollIntervalMs: c.PollIntervalMs,
|
||||
MaxPollIntervalMs: c.MaxPollIntervalMs,
|
||||
SessionTtlSeconds: c.SessionTTLSeconds,
|
||||
Concurrency: c.Concurrency,
|
||||
EagerWindowMs: c.EagerWindowMs,
|
||||
HoleTimeoutMs: c.HoleTimeoutMs,
|
||||
Template: string(c.Template),
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
@@ -291,3 +291,76 @@ func TestHeaderCustomUDPBuildRejectsExprWithoutArgs(t *testing.T) {
|
||||
t.Fatalf("expected transform arg rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDriveStreamConfig(t *testing.T) {
|
||||
config := new(StreamConfig)
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"method": "xdrive",
|
||||
"xdriveSettings": {
|
||||
"remoteFolder": "/tmp/xdrive",
|
||||
"service": "local"
|
||||
}
|
||||
}`), config); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
|
||||
built, err := config.Build()
|
||||
if err != nil {
|
||||
t.Fatalf("Build: %v", err)
|
||||
}
|
||||
if built.ProtocolName != "xdrive" {
|
||||
t.Fatalf("ProtocolName is %q, want %q", built.ProtocolName, "xdrive")
|
||||
}
|
||||
if len(built.TransportSettings) != 1 || built.TransportSettings[0].ProtocolName != "xdrive" {
|
||||
t.Fatalf("TransportSettings is %v, want a single xdrive entry", built.TransportSettings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDriveRejectsUnknownService(t *testing.T) {
|
||||
config := new(XDriveConfig)
|
||||
if err := json.Unmarshal([]byte(`{"remoteFolder": "/tmp/xdrive", "service": "Dropbox"}`), config); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if _, err := config.Build(); err == nil {
|
||||
t.Fatal("Build accepted an unsupported service")
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDriveTemplateStreamConfig(t *testing.T) {
|
||||
config := new(StreamConfig)
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"method": "xdrive",
|
||||
"xdriveSettings": {
|
||||
"remoteFolder": "folder",
|
||||
"service": "template",
|
||||
"secrets": ["user", "pass"],
|
||||
"template": {
|
||||
"flatten": true,
|
||||
"auth": {"type": "basic", "username": "{secret0}", "password": "{secret1}"},
|
||||
"put": {"method": "PUT", "url": "https://dav.example/{folder}/{name}"},
|
||||
"get": {"method": "GET", "url": "https://dav.example/{folder}/{name}"},
|
||||
"delete": {"method": "DELETE", "url": "https://dav.example/{folder}/{name}"},
|
||||
"list": {"method": "PROPFIND", "url": "https://dav.example/{folder}/", "namesRegex": "<d:href>/folder/([^<]+)</d:href>"}
|
||||
}
|
||||
}
|
||||
}`), config); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
built, err := config.Build()
|
||||
if err != nil {
|
||||
t.Fatalf("Build: %v", err)
|
||||
}
|
||||
if built.ProtocolName != "xdrive" {
|
||||
t.Fatalf("ProtocolName is %q, want xdrive", built.ProtocolName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDriveTemplateNeedsTemplate(t *testing.T) {
|
||||
config := new(XDriveConfig)
|
||||
if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if _, err := config.Build(); err == nil {
|
||||
t.Fatal("Build accepted a template service without a template")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +312,9 @@ func (c *VLessOutboundConfig) Build() (proto.Message, error) {
|
||||
if err := json.Unmarshal(rawUser, account); err != nil {
|
||||
return nil, errors.New(`VLESS users: invalid user`).Base(err)
|
||||
}
|
||||
// validateOutboundTransportSecurity needs to see these
|
||||
c.Encryption = account.Encryption
|
||||
c.Address = rec.Address
|
||||
if account.Reverse != nil { // may not be reached: error json unmarshal
|
||||
return nil, errors.New(`VLESS users: please use simplified outbound's config style to use "reverse"`)
|
||||
}
|
||||
|
||||
+1
-1
@@ -242,7 +242,7 @@ func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *pr
|
||||
if vlessCfg.Encryption != "" && vlessCfg.Encryption != "none" {
|
||||
return nil
|
||||
}
|
||||
if requiresTransportSecurity(vlessCfg.Vnext[0].Address) {
|
||||
if requiresTransportSecurity(vlessCfg.Address) {
|
||||
return errors.New("vless without TLS or other encryption is prohibited unless the server address is a private IP or domain")
|
||||
}
|
||||
}
|
||||
|
||||
+273
-139
@@ -1,15 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"mvdan.cc/gofumpt/format"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,101 +26,27 @@ var (
|
||||
isFormat bool
|
||||
)
|
||||
|
||||
// envFile returns the name of the Go environment configuration file.
|
||||
// Copy from https://github.com/golang/go/blob/c4f2a9788a7be04daf931ac54382fbe2cb754938/src/cmd/go/internal/cfg/cfg.go#L150-L166
|
||||
func envFile() (string, error) {
|
||||
if file := os.Getenv("GOENV"); file != "" {
|
||||
if file == "off" {
|
||||
return "", errors.New("GOENV=off")
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
dir, err := os.UserConfigDir()
|
||||
func getModuleInfo(pwd string) (modPath, langVersion string, err error) {
|
||||
data, err := os.ReadFile(filepath.Join(pwd, "go.mod"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", "", err
|
||||
}
|
||||
if dir == "" {
|
||||
return "", errors.New("missing user-config dir")
|
||||
}
|
||||
return filepath.Join(dir, "go", "env"), nil
|
||||
}
|
||||
|
||||
// GetRuntimeEnv returns the value of runtime environment variable,
|
||||
// that is set by running following command: `go env -w key=value`.
|
||||
func GetRuntimeEnv(key string) (string, error) {
|
||||
file, err := envFile()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if file == "" {
|
||||
return "", errors.New("missing runtime env file")
|
||||
}
|
||||
var data []byte
|
||||
var runtimeEnv string
|
||||
data, readErr := os.ReadFile(file)
|
||||
if readErr != nil {
|
||||
return "", readErr
|
||||
}
|
||||
envStrings := strings.Split(string(data), "\n")
|
||||
for _, envItem := range envStrings {
|
||||
envItem = strings.TrimSuffix(envItem, "\r")
|
||||
envKeyValue := strings.Split(envItem, "=")
|
||||
if len(envKeyValue) == 2 && strings.TrimSpace(envKeyValue[0]) == key {
|
||||
runtimeEnv = strings.TrimSpace(envKeyValue[1])
|
||||
}
|
||||
}
|
||||
return runtimeEnv, nil
|
||||
}
|
||||
|
||||
// GetGOBIN returns GOBIN environment variable as a string. It will NOT be empty.
|
||||
func GetGOBIN() string {
|
||||
// The one set by user explicitly by `export GOBIN=/path` or `env GOBIN=/path command`
|
||||
GOBIN := os.Getenv("GOBIN")
|
||||
if GOBIN == "" {
|
||||
var err error
|
||||
// The one set by user by running `go env -w GOBIN=/path`
|
||||
GOBIN, err = GetRuntimeEnv("GOBIN")
|
||||
if err != nil {
|
||||
// The default one that Golang uses
|
||||
return filepath.Join(build.Default.GOPATH, "bin")
|
||||
}
|
||||
if GOBIN == "" {
|
||||
return filepath.Join(build.Default.GOPATH, "bin")
|
||||
}
|
||||
return GOBIN
|
||||
}
|
||||
return GOBIN
|
||||
}
|
||||
|
||||
func Run(binary string, args []string) ([]byte, error) {
|
||||
cmd := exec.Command(binary, args...)
|
||||
cmd.Env = append(cmd.Env, os.Environ()...)
|
||||
output, cmdErr := cmd.CombinedOutput()
|
||||
if cmdErr != nil {
|
||||
return nil, cmdErr
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func RunMany(binary string, args, files []string) bool {
|
||||
fmt.Println("Processing with", binary, args, "...")
|
||||
|
||||
formatRequired := false
|
||||
maxTasks := make(chan struct{}, runtime.NumCPU())
|
||||
for _, file := range files {
|
||||
maxTasks <- struct{}{}
|
||||
go func(file string) {
|
||||
output, err := Run(binary, append(args, file))
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
} else if len(output) > 0 {
|
||||
fmt.Println(string(output))
|
||||
formatRequired = true
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
switch fields[0] {
|
||||
case "module":
|
||||
modPath = fields[1]
|
||||
case "go":
|
||||
langVersion = "go" + strings.TrimPrefix(fields[1], "go")
|
||||
}
|
||||
<-maxTasks
|
||||
}(file)
|
||||
}
|
||||
}
|
||||
return formatRequired
|
||||
return modPath, langVersion, nil
|
||||
}
|
||||
|
||||
func formatGoSource(src []byte, opts format.Options) ([]byte, error) {
|
||||
return format.Source(src, opts)
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -150,26 +79,76 @@ func main() {
|
||||
}
|
||||
|
||||
pwd := *directory
|
||||
GOBIN := GetGOBIN()
|
||||
binPath := os.Getenv("PATH")
|
||||
pathSlice := []string{pwd, GOBIN, binPath}
|
||||
binPath = strings.Join(pathSlice, string(os.PathListSeparator))
|
||||
os.Setenv("PATH", binPath)
|
||||
|
||||
suffix := ""
|
||||
if runtime.GOOS == "windows" {
|
||||
suffix = ".exe"
|
||||
}
|
||||
gofmt := "gofumpt" + suffix
|
||||
|
||||
if gofmtPath, err := exec.LookPath(gofmt); err != nil {
|
||||
fmt.Println("Can not find", gofmt, "in system path or current working directory.")
|
||||
modPath, langVersion, modErr := getModuleInfo(pwd)
|
||||
if modErr != nil {
|
||||
fmt.Println("Error reading go.mod:", modErr)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
gofmt = gofmtPath
|
||||
}
|
||||
opts := format.Options{
|
||||
LangVersion: langVersion,
|
||||
ModulePath: modPath,
|
||||
}
|
||||
|
||||
if isFormat {
|
||||
fmt.Println("Formatting Go source files...")
|
||||
} else if isCheck {
|
||||
fmt.Println("Checking files thar are not properly formatted...")
|
||||
}
|
||||
|
||||
jobs := make(chan string, runtime.NumCPU())
|
||||
var wg sync.WaitGroup
|
||||
var formatRequired atomic.Bool
|
||||
var hasErrors atomic.Bool
|
||||
|
||||
for i := 0; i < runtime.NumCPU(); i++ {
|
||||
wg.Go(func() {
|
||||
for path := range jobs {
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading %s: %v\n", path, err)
|
||||
hasErrors.Store(true)
|
||||
continue
|
||||
}
|
||||
|
||||
formatted, err := formatGoSource(src, opts)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error formatting %s: %v\n", path, err)
|
||||
hasErrors.Store(true)
|
||||
continue
|
||||
}
|
||||
|
||||
if !bytes.Equal(src, formatted) {
|
||||
var diffText []byte
|
||||
if isDryrun {
|
||||
newName := filepath.ToSlash(path)
|
||||
oldName := newName + ".orig"
|
||||
diffText = diff(oldName, src, newName, formatted)
|
||||
}
|
||||
if isFormat {
|
||||
info, statErr := os.Stat(path)
|
||||
if statErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error stating %s: %v\n", path, statErr)
|
||||
hasErrors.Store(true)
|
||||
continue
|
||||
}
|
||||
if writeErr := os.WriteFile(path, formatted, info.Mode().Perm()); writeErr != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", path, writeErr)
|
||||
hasErrors.Store(true)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
formatRequired.Store(true)
|
||||
if isDryrun && len(diffText) > 0 {
|
||||
fmt.Printf("%s\n%s", path, diffText)
|
||||
} else {
|
||||
fmt.Println(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
rawFilesSlice := make([]string, 0, 1000)
|
||||
walkErr := filepath.Walk(pwd, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
@@ -186,51 +165,206 @@ func main() {
|
||||
!strings.HasSuffix(filename, ".pb.go") &&
|
||||
!strings.Contains(dir, filepath.Join("testing", "mocks")) &&
|
||||
!strings.Contains(path, filepath.Join("main", "distro", "all", "all.go")) {
|
||||
rawFilesSlice = append(rawFilesSlice, path)
|
||||
jobs <- path
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
close(jobs)
|
||||
wg.Wait()
|
||||
|
||||
if walkErr != nil {
|
||||
fmt.Println(walkErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if isFormat {
|
||||
gofmtArgs := []string{
|
||||
"-l", "-e", "-w",
|
||||
}
|
||||
if hasErrors.Load() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Formatting Go source files...")
|
||||
RunMany(gofmt, gofmtArgs, rawFilesSlice)
|
||||
fmt.Println("Do NOT forget to commit file changes.")
|
||||
if isFormat {
|
||||
if formatRequired.Load() {
|
||||
fmt.Println("Do NOT forget to commit file changes.")
|
||||
}
|
||||
}
|
||||
|
||||
if isCheck {
|
||||
gofmtListArgs := []string{
|
||||
"-l", "-e",
|
||||
}
|
||||
|
||||
fmt.Println("Checking files thar are not properly formatted...")
|
||||
formatRequired := RunMany(gofmt, gofmtListArgs, rawFilesSlice)
|
||||
if formatRequired {
|
||||
if formatRequired.Load() {
|
||||
fmt.Println("Format problem(s) found.")
|
||||
}
|
||||
|
||||
if isDryrun {
|
||||
if formatRequired {
|
||||
gofmtShowArgs := []string{
|
||||
"-d", "-e",
|
||||
}
|
||||
RunMany(gofmt, gofmtShowArgs, rawFilesSlice)
|
||||
}
|
||||
}
|
||||
|
||||
if formatRequired {
|
||||
fmt.Println("Please run 'go install -v mvdan.cc/gofumpt@latest', then run 'go run ./infra/vformat/main.go' to format the Go source files.")
|
||||
fmt.Println("Please run 'go run ./infra/vformat/main.go' to format the Go source files.")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
fmt.Println("All Go source file format check has been passed.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// diff algorithm copied from mvdan.cc/gofumpt/internal/govendor/diff
|
||||
type pair struct{ x, y int }
|
||||
|
||||
func diff(oldName string, old []byte, newName string, new []byte) []byte {
|
||||
if bytes.Equal(old, new) {
|
||||
return nil
|
||||
}
|
||||
x := diffLines(old)
|
||||
y := diffLines(new)
|
||||
|
||||
var out bytes.Buffer
|
||||
fmt.Fprintf(&out, "diff %s %s\n", oldName, newName)
|
||||
fmt.Fprintf(&out, "--- %s\n", oldName)
|
||||
fmt.Fprintf(&out, "+++ %s\n", newName)
|
||||
|
||||
var (
|
||||
done pair
|
||||
chunk pair
|
||||
count pair
|
||||
ctext []string
|
||||
)
|
||||
for _, m := range diffTgs(x, y) {
|
||||
if m.x < done.x {
|
||||
continue
|
||||
}
|
||||
start := m
|
||||
for start.x > done.x && start.y > done.y && x[start.x-1] == y[start.y-1] {
|
||||
start.x--
|
||||
start.y--
|
||||
}
|
||||
end := m
|
||||
for end.x < len(x) && end.y < len(y) && x[end.x] == y[end.y] {
|
||||
end.x++
|
||||
end.y++
|
||||
}
|
||||
|
||||
for _, s := range x[done.x:start.x] {
|
||||
ctext = append(ctext, "-"+s)
|
||||
count.x++
|
||||
}
|
||||
for _, s := range y[done.y:start.y] {
|
||||
ctext = append(ctext, "+"+s)
|
||||
count.y++
|
||||
}
|
||||
|
||||
const C = 3
|
||||
if (end.x < len(x) || end.y < len(y)) &&
|
||||
(end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) {
|
||||
for _, s := range x[start.x:end.x] {
|
||||
ctext = append(ctext, " "+s)
|
||||
count.x++
|
||||
count.y++
|
||||
}
|
||||
done = end
|
||||
continue
|
||||
}
|
||||
|
||||
if len(ctext) > 0 {
|
||||
n := end.x - start.x
|
||||
if n > C {
|
||||
n = C
|
||||
}
|
||||
for _, s := range x[start.x : start.x+n] {
|
||||
ctext = append(ctext, " "+s)
|
||||
count.x++
|
||||
count.y++
|
||||
}
|
||||
done = pair{start.x + n, start.y + n}
|
||||
|
||||
if count.x > 0 {
|
||||
chunk.x++
|
||||
}
|
||||
if count.y > 0 {
|
||||
chunk.y++
|
||||
}
|
||||
fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y)
|
||||
for _, s := range ctext {
|
||||
out.WriteString(s)
|
||||
}
|
||||
count.x = 0
|
||||
count.y = 0
|
||||
ctext = ctext[:0]
|
||||
}
|
||||
|
||||
if end.x >= len(x) && end.y >= len(y) {
|
||||
break
|
||||
}
|
||||
|
||||
chunk = pair{end.x - C, end.y - C}
|
||||
for _, s := range x[chunk.x:end.x] {
|
||||
ctext = append(ctext, " "+s)
|
||||
count.x++
|
||||
count.y++
|
||||
}
|
||||
done = end
|
||||
}
|
||||
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
func diffLines(x []byte) []string {
|
||||
l := strings.SplitAfter(string(x), "\n")
|
||||
if l[len(l)-1] == "" {
|
||||
l = l[:len(l)-1]
|
||||
} else {
|
||||
l[len(l)-1] += "\n\\ No newline at end of file\n"
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func diffTgs(x, y []string) []pair {
|
||||
m := make(map[string]int)
|
||||
for _, s := range x {
|
||||
if c := m[s]; c > -2 {
|
||||
m[s] = c - 1
|
||||
}
|
||||
}
|
||||
for _, s := range y {
|
||||
if c := m[s]; c > -8 {
|
||||
m[s] = c - 4
|
||||
}
|
||||
}
|
||||
|
||||
var xi, yi, inv []int
|
||||
for i, s := range y {
|
||||
if m[s] == -5 {
|
||||
m[s] = len(yi)
|
||||
yi = append(yi, i)
|
||||
}
|
||||
}
|
||||
for i, s := range x {
|
||||
if j, ok := m[s]; ok && j >= 0 {
|
||||
xi = append(xi, i)
|
||||
inv = append(inv, j)
|
||||
}
|
||||
}
|
||||
|
||||
J := inv
|
||||
n := len(xi)
|
||||
T := make([]int, n)
|
||||
L := make([]int, n)
|
||||
for i := range T {
|
||||
T[i] = n + 1
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
k := sort.Search(n, func(k int) bool {
|
||||
return T[k] >= J[i]
|
||||
})
|
||||
T[k] = J[i]
|
||||
L[i] = k + 1
|
||||
}
|
||||
k := 0
|
||||
for _, v := range L {
|
||||
if k < v {
|
||||
k = v
|
||||
}
|
||||
}
|
||||
seq := make([]pair, 2+k)
|
||||
seq[1+k] = pair{len(x), len(y)}
|
||||
lastj := n
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
if L[i] == k && J[i] < lastj {
|
||||
seq[k] = pair{xi[i], yi[J[i]]}
|
||||
k--
|
||||
}
|
||||
}
|
||||
seq[0] = pair{0, 0}
|
||||
return seq
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ import (
|
||||
_ "github.com/xtls/xray-core/transport/internet/tls"
|
||||
_ "github.com/xtls/xray-core/transport/internet/udp"
|
||||
_ "github.com/xtls/xray-core/transport/internet/websocket"
|
||||
_ "github.com/xtls/xray-core/transport/internet/xdrive"
|
||||
|
||||
// Transport headers
|
||||
_ "github.com/xtls/xray-core/transport/internet/headers/http"
|
||||
|
||||
+15
-10
@@ -190,6 +190,12 @@ func (h *Handler) matchFinalRule(network net.Network, address net.Address, port
|
||||
func (h *Handler) Init(config *Config, pm policy.Manager) error {
|
||||
h.config = config
|
||||
h.policyManager = pm
|
||||
if h.usesDialerProxy { // freedom is not the final outbound, final rules do not apply
|
||||
if len(config.FinalRules) > 0 {
|
||||
errors.LogWarning(context.Background(), `The "finalRules" setting is ignored when "sockopt.dialerProxy" is set, since freedom is not the final outbound.`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
h.finalRules = make([]*FinalRule, 0, len(config.FinalRules))
|
||||
for _, rc := range config.FinalRules {
|
||||
rule, err := buildFinalRule(rc)
|
||||
@@ -253,7 +259,10 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
ob.Name = "freedom"
|
||||
ob.CanSpliceCopy = 1
|
||||
inbound := session.InboundFromContext(ctx)
|
||||
defaultRule := getDefaultFinalRule(inbound)
|
||||
var defaultRule *FinalRule
|
||||
if !h.usesDialerProxy { // freedom is not the final outbound, final rules do not apply (and the domain is not resolved)
|
||||
defaultRule = getDefaultFinalRule(inbound)
|
||||
}
|
||||
|
||||
destination := ob.Target
|
||||
origTargetAddr := ob.OriginalTarget.Address
|
||||
@@ -342,15 +351,11 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
return h.blackhole(ctx, input, output, blockedRule, blockedDest)
|
||||
}
|
||||
if destination.Address.Family().IsDomain() && (defaultRule != nil || len(h.finalRules) > 0) {
|
||||
if h.usesDialerProxy {
|
||||
errors.LogInfo(ctx, "skipping final rule check for proxied remote endpoint, original target: ", destination)
|
||||
} else {
|
||||
// pre-check may fail or dialer may select another IP
|
||||
remoteDest := net.DestinationFromAddr(conn.RemoteAddr())
|
||||
if rule := h.matchFinalRule(remoteDest.Network, remoteDest.Address, remoteDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||
conn.Close()
|
||||
return h.blackhole(ctx, input, output, rule, &remoteDest)
|
||||
}
|
||||
// pre-check may fail or dialer may select another IP
|
||||
remoteDest := net.DestinationFromAddr(conn.RemoteAddr())
|
||||
if rule := h.matchFinalRule(remoteDest.Network, remoteDest.Address, remoteDest.Port, defaultRule); rule != nil && rule.action == RuleAction_Block {
|
||||
conn.Close()
|
||||
return h.blackhole(ctx, input, output, rule, &remoteDest)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-3
@@ -37,6 +37,25 @@ type Handler struct {
|
||||
downlinkCounter stats.Counter
|
||||
}
|
||||
|
||||
type tunUDPStatsWriter struct {
|
||||
writer buf.Writer
|
||||
counter stats.Counter
|
||||
}
|
||||
|
||||
func (w *tunUDPStatsWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
for len(mb) > 0 {
|
||||
remaining, packet := buf.SplitFirst(mb)
|
||||
packetSize := packet.Len()
|
||||
if err := w.writer.WriteMultiBuffer(buf.MultiBuffer{packet}); err != nil {
|
||||
buf.ReleaseMulti(remaining)
|
||||
return err
|
||||
}
|
||||
w.counter.Add(int64(packetSize))
|
||||
mb = remaining
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConnectionHandler interface with the only method that stack is going to push new connections to
|
||||
type ConnectionHandler interface {
|
||||
HandleConnection(conn net.Conn, destination net.Destination)
|
||||
@@ -171,7 +190,8 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) {
|
||||
return
|
||||
}
|
||||
source := net.DestinationFromAddr(remote)
|
||||
if t.uplinkCounter != nil || t.downlinkCounter != nil {
|
||||
isUDP := destination.Network == net.Network_UDP
|
||||
if !isUDP && (t.uplinkCounter != nil || t.downlinkCounter != nil) {
|
||||
conn = &stat.CounterConnection{
|
||||
Connection: conn,
|
||||
ReadCounter: t.uplinkCounter,
|
||||
@@ -203,9 +223,18 @@ func (t *Handler) HandleConnection(conn net.Conn, destination net.Destination) {
|
||||
})
|
||||
errors.LogInfo(ctx, "processing from ", source, " to ", destination)
|
||||
|
||||
reader := &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)}
|
||||
writer := buf.NewWriter(conn)
|
||||
if isUDP {
|
||||
reader.Counter = t.uplinkCounter
|
||||
if t.downlinkCounter != nil {
|
||||
writer = &tunUDPStatsWriter{writer: writer, counter: t.downlinkCounter}
|
||||
}
|
||||
}
|
||||
|
||||
link := &transport.Link{
|
||||
Reader: &buf.TimeoutWrapperReader{Reader: buf.NewReader(conn)},
|
||||
Writer: buf.NewWriter(conn),
|
||||
Reader: reader,
|
||||
Writer: writer,
|
||||
}
|
||||
if err := t.dispatcher.DispatchLink(ctx, destination, link); err != nil {
|
||||
errors.LogError(ctx, errors.New("connection closed").Base(err))
|
||||
|
||||
+63
-138
@@ -206,7 +206,7 @@ func (x SocketConfig_TProxyMode) Number() protoreflect.EnumNumber {
|
||||
|
||||
// Deprecated: Use SocketConfig_TProxyMode.Descriptor instead.
|
||||
func (SocketConfig_TProxyMode) EnumDescriptor() ([]byte, []int) {
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{5, 0}
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{4, 0}
|
||||
}
|
||||
|
||||
type TransportConfig struct {
|
||||
@@ -382,66 +382,6 @@ func (x *StreamConfig) GetSocketSettings() *SocketConfig {
|
||||
return nil
|
||||
}
|
||||
|
||||
type UdpHop struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Ports []uint32 `protobuf:"varint,1,rep,packed,name=ports,proto3" json:"ports,omitempty"`
|
||||
IntervalMin int64 `protobuf:"varint,2,opt,name=interval_min,json=intervalMin,proto3" json:"interval_min,omitempty"`
|
||||
IntervalMax int64 `protobuf:"varint,3,opt,name=interval_max,json=intervalMax,proto3" json:"interval_max,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *UdpHop) Reset() {
|
||||
*x = UdpHop{}
|
||||
mi := &file_transport_internet_config_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *UdpHop) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*UdpHop) ProtoMessage() {}
|
||||
|
||||
func (x *UdpHop) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_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 UdpHop.ProtoReflect.Descriptor instead.
|
||||
func (*UdpHop) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *UdpHop) GetPorts() []uint32 {
|
||||
if x != nil {
|
||||
return x.Ports
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *UdpHop) GetIntervalMin() int64 {
|
||||
if x != nil {
|
||||
return x.IntervalMin
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *UdpHop) GetIntervalMax() int64 {
|
||||
if x != nil {
|
||||
return x.IntervalMax
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type QuicParams struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Congestion string `protobuf:"bytes,1,opt,name=congestion,proto3" json:"congestion,omitempty"`
|
||||
@@ -449,25 +389,24 @@ type QuicParams struct {
|
||||
BrutalUp uint64 `protobuf:"varint,3,opt,name=brutal_up,json=brutalUp,proto3" json:"brutal_up,omitempty"`
|
||||
BrutalDown uint64 `protobuf:"varint,4,opt,name=brutal_down,json=brutalDown,proto3" json:"brutal_down,omitempty"`
|
||||
BrutalDisableLossCompensation bool `protobuf:"varint,5,opt,name=brutal_disable_loss_compensation,json=brutalDisableLossCompensation,proto3" json:"brutal_disable_loss_compensation,omitempty"`
|
||||
UdpHop *UdpHop `protobuf:"bytes,6,opt,name=udp_hop,json=udpHop,proto3" json:"udp_hop,omitempty"`
|
||||
InitStreamReceiveWindow uint64 `protobuf:"varint,7,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"`
|
||||
MaxStreamReceiveWindow uint64 `protobuf:"varint,8,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"`
|
||||
InitConnReceiveWindow uint64 `protobuf:"varint,9,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"`
|
||||
MaxConnReceiveWindow uint64 `protobuf:"varint,10,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"`
|
||||
MaxIdleTimeout int64 `protobuf:"varint,11,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"`
|
||||
KeepAlivePeriod int64 `protobuf:"varint,12,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"`
|
||||
DisablePathMtuDiscovery bool `protobuf:"varint,13,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"`
|
||||
DisableChromeParrot bool `protobuf:"varint,14,opt,name=disable_chrome_parrot,json=disableChromeParrot,proto3" json:"disable_chrome_parrot,omitempty"`
|
||||
DisableGSO bool `protobuf:"varint,15,opt,name=disableGSO,proto3" json:"disableGSO,omitempty"`
|
||||
MaxIncomingStreams int64 `protobuf:"varint,16,opt,name=max_incoming_streams,json=maxIncomingStreams,proto3" json:"max_incoming_streams,omitempty"`
|
||||
DisableStatelessReset bool `protobuf:"varint,17,opt,name=disable_stateless_reset,json=disableStatelessReset,proto3" json:"disable_stateless_reset,omitempty"`
|
||||
InitStreamReceiveWindow uint64 `protobuf:"varint,6,opt,name=init_stream_receive_window,json=initStreamReceiveWindow,proto3" json:"init_stream_receive_window,omitempty"`
|
||||
MaxStreamReceiveWindow uint64 `protobuf:"varint,7,opt,name=max_stream_receive_window,json=maxStreamReceiveWindow,proto3" json:"max_stream_receive_window,omitempty"`
|
||||
InitConnReceiveWindow uint64 `protobuf:"varint,8,opt,name=init_conn_receive_window,json=initConnReceiveWindow,proto3" json:"init_conn_receive_window,omitempty"`
|
||||
MaxConnReceiveWindow uint64 `protobuf:"varint,9,opt,name=max_conn_receive_window,json=maxConnReceiveWindow,proto3" json:"max_conn_receive_window,omitempty"`
|
||||
MaxIdleTimeout int64 `protobuf:"varint,10,opt,name=max_idle_timeout,json=maxIdleTimeout,proto3" json:"max_idle_timeout,omitempty"`
|
||||
KeepAlivePeriod int64 `protobuf:"varint,11,opt,name=keep_alive_period,json=keepAlivePeriod,proto3" json:"keep_alive_period,omitempty"`
|
||||
DisablePathMtuDiscovery bool `protobuf:"varint,12,opt,name=disable_path_mtu_discovery,json=disablePathMtuDiscovery,proto3" json:"disable_path_mtu_discovery,omitempty"`
|
||||
DisableChromeParrot bool `protobuf:"varint,13,opt,name=disable_chrome_parrot,json=disableChromeParrot,proto3" json:"disable_chrome_parrot,omitempty"`
|
||||
DisableGSO bool `protobuf:"varint,14,opt,name=disableGSO,proto3" json:"disableGSO,omitempty"`
|
||||
MaxIncomingStreams int64 `protobuf:"varint,15,opt,name=max_incoming_streams,json=maxIncomingStreams,proto3" json:"max_incoming_streams,omitempty"`
|
||||
DisableStatelessReset bool `protobuf:"varint,16,opt,name=disable_stateless_reset,json=disableStatelessReset,proto3" json:"disable_stateless_reset,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *QuicParams) Reset() {
|
||||
*x = QuicParams{}
|
||||
mi := &file_transport_internet_config_proto_msgTypes[3]
|
||||
mi := &file_transport_internet_config_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -479,7 +418,7 @@ func (x *QuicParams) String() string {
|
||||
func (*QuicParams) ProtoMessage() {}
|
||||
|
||||
func (x *QuicParams) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_config_proto_msgTypes[3]
|
||||
mi := &file_transport_internet_config_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -492,7 +431,7 @@ func (x *QuicParams) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use QuicParams.ProtoReflect.Descriptor instead.
|
||||
func (*QuicParams) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{3}
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *QuicParams) GetCongestion() string {
|
||||
@@ -530,13 +469,6 @@ func (x *QuicParams) GetBrutalDisableLossCompensation() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *QuicParams) GetUdpHop() *UdpHop {
|
||||
if x != nil {
|
||||
return x.UdpHop
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *QuicParams) GetInitStreamReceiveWindow() uint64 {
|
||||
if x != nil {
|
||||
return x.InitStreamReceiveWindow
|
||||
@@ -628,7 +560,7 @@ type CustomSockopt struct {
|
||||
|
||||
func (x *CustomSockopt) Reset() {
|
||||
*x = CustomSockopt{}
|
||||
mi := &file_transport_internet_config_proto_msgTypes[4]
|
||||
mi := &file_transport_internet_config_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -640,7 +572,7 @@ func (x *CustomSockopt) String() string {
|
||||
func (*CustomSockopt) ProtoMessage() {}
|
||||
|
||||
func (x *CustomSockopt) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_config_proto_msgTypes[4]
|
||||
mi := &file_transport_internet_config_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -653,7 +585,7 @@ func (x *CustomSockopt) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use CustomSockopt.ProtoReflect.Descriptor instead.
|
||||
func (*CustomSockopt) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{4}
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *CustomSockopt) GetSystem() string {
|
||||
@@ -733,7 +665,7 @@ type SocketConfig struct {
|
||||
|
||||
func (x *SocketConfig) Reset() {
|
||||
*x = SocketConfig{}
|
||||
mi := &file_transport_internet_config_proto_msgTypes[5]
|
||||
mi := &file_transport_internet_config_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -745,7 +677,7 @@ func (x *SocketConfig) String() string {
|
||||
func (*SocketConfig) ProtoMessage() {}
|
||||
|
||||
func (x *SocketConfig) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_config_proto_msgTypes[5]
|
||||
mi := &file_transport_internet_config_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -758,7 +690,7 @@ func (x *SocketConfig) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use SocketConfig.ProtoReflect.Descriptor instead.
|
||||
func (*SocketConfig) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{5}
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *SocketConfig) GetMark() int32 {
|
||||
@@ -920,7 +852,7 @@ type HappyEyeballsConfig struct {
|
||||
|
||||
func (x *HappyEyeballsConfig) Reset() {
|
||||
*x = HappyEyeballsConfig{}
|
||||
mi := &file_transport_internet_config_proto_msgTypes[6]
|
||||
mi := &file_transport_internet_config_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -932,7 +864,7 @@ func (x *HappyEyeballsConfig) String() string {
|
||||
func (*HappyEyeballsConfig) ProtoMessage() {}
|
||||
|
||||
func (x *HappyEyeballsConfig) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_config_proto_msgTypes[6]
|
||||
mi := &file_transport_internet_config_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -945,7 +877,7 @@ func (x *HappyEyeballsConfig) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use HappyEyeballsConfig.ProtoReflect.Descriptor instead.
|
||||
func (*HappyEyeballsConfig) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{6}
|
||||
return file_transport_internet_config_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *HappyEyeballsConfig) GetPrioritizeIpv6() bool {
|
||||
@@ -996,11 +928,7 @@ const file_transport_internet_config_proto_rawDesc = "" +
|
||||
"\btcpmasks\x18\v \x03(\v2 .xray.common.serial.TypedMessageR\btcpmasks\x12D\n" +
|
||||
"\vquic_params\x18\f \x01(\v2#.xray.transport.internet.QuicParamsR\n" +
|
||||
"quicParams\x12N\n" +
|
||||
"\x0fsocket_settings\x18\x06 \x01(\v2%.xray.transport.internet.SocketConfigR\x0esocketSettings\"d\n" +
|
||||
"\x06UdpHop\x12\x14\n" +
|
||||
"\x05ports\x18\x01 \x03(\rR\x05ports\x12!\n" +
|
||||
"\finterval_min\x18\x02 \x01(\x03R\vintervalMin\x12!\n" +
|
||||
"\finterval_max\x18\x03 \x01(\x03R\vintervalMax\"\xc7\x06\n" +
|
||||
"\x0fsocket_settings\x18\x06 \x01(\v2%.xray.transport.internet.SocketConfigR\x0esocketSettings\"\x8d\x06\n" +
|
||||
"\n" +
|
||||
"QuicParams\x12\x1e\n" +
|
||||
"\n" +
|
||||
@@ -1011,22 +939,21 @@ const file_transport_internet_config_proto_rawDesc = "" +
|
||||
"\tbrutal_up\x18\x03 \x01(\x04R\bbrutalUp\x12\x1f\n" +
|
||||
"\vbrutal_down\x18\x04 \x01(\x04R\n" +
|
||||
"brutalDown\x12G\n" +
|
||||
" brutal_disable_loss_compensation\x18\x05 \x01(\bR\x1dbrutalDisableLossCompensation\x128\n" +
|
||||
"\audp_hop\x18\x06 \x01(\v2\x1f.xray.transport.internet.UdpHopR\x06udpHop\x12;\n" +
|
||||
"\x1ainit_stream_receive_window\x18\a \x01(\x04R\x17initStreamReceiveWindow\x129\n" +
|
||||
"\x19max_stream_receive_window\x18\b \x01(\x04R\x16maxStreamReceiveWindow\x127\n" +
|
||||
"\x18init_conn_receive_window\x18\t \x01(\x04R\x15initConnReceiveWindow\x125\n" +
|
||||
"\x17max_conn_receive_window\x18\n" +
|
||||
" \x01(\x04R\x14maxConnReceiveWindow\x12(\n" +
|
||||
"\x10max_idle_timeout\x18\v \x01(\x03R\x0emaxIdleTimeout\x12*\n" +
|
||||
"\x11keep_alive_period\x18\f \x01(\x03R\x0fkeepAlivePeriod\x12;\n" +
|
||||
"\x1adisable_path_mtu_discovery\x18\r \x01(\bR\x17disablePathMtuDiscovery\x122\n" +
|
||||
"\x15disable_chrome_parrot\x18\x0e \x01(\bR\x13disableChromeParrot\x12\x1e\n" +
|
||||
" brutal_disable_loss_compensation\x18\x05 \x01(\bR\x1dbrutalDisableLossCompensation\x12;\n" +
|
||||
"\x1ainit_stream_receive_window\x18\x06 \x01(\x04R\x17initStreamReceiveWindow\x129\n" +
|
||||
"\x19max_stream_receive_window\x18\a \x01(\x04R\x16maxStreamReceiveWindow\x127\n" +
|
||||
"\x18init_conn_receive_window\x18\b \x01(\x04R\x15initConnReceiveWindow\x125\n" +
|
||||
"\x17max_conn_receive_window\x18\t \x01(\x04R\x14maxConnReceiveWindow\x12(\n" +
|
||||
"\x10max_idle_timeout\x18\n" +
|
||||
" \x01(\x03R\x0emaxIdleTimeout\x12*\n" +
|
||||
"\x11keep_alive_period\x18\v \x01(\x03R\x0fkeepAlivePeriod\x12;\n" +
|
||||
"\x1adisable_path_mtu_discovery\x18\f \x01(\bR\x17disablePathMtuDiscovery\x122\n" +
|
||||
"\x15disable_chrome_parrot\x18\r \x01(\bR\x13disableChromeParrot\x12\x1e\n" +
|
||||
"\n" +
|
||||
"disableGSO\x18\x0f \x01(\bR\n" +
|
||||
"disableGSO\x18\x0e \x01(\bR\n" +
|
||||
"disableGSO\x120\n" +
|
||||
"\x14max_incoming_streams\x18\x10 \x01(\x03R\x12maxIncomingStreams\x126\n" +
|
||||
"\x17disable_stateless_reset\x18\x11 \x01(\bR\x15disableStatelessReset\"\x93\x01\n" +
|
||||
"\x14max_incoming_streams\x18\x0f \x01(\x03R\x12maxIncomingStreams\x126\n" +
|
||||
"\x17disable_stateless_reset\x18\x10 \x01(\bR\x15disableStatelessReset\"\x93\x01\n" +
|
||||
"\rCustomSockopt\x12\x16\n" +
|
||||
"\x06system\x18\x01 \x01(\tR\x06system\x12\x18\n" +
|
||||
"\anetwork\x18\x02 \x01(\tR\anetwork\x12\x14\n" +
|
||||
@@ -1110,41 +1037,39 @@ func file_transport_internet_config_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_transport_internet_config_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
|
||||
var file_transport_internet_config_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
|
||||
var file_transport_internet_config_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||
var file_transport_internet_config_proto_goTypes = []any{
|
||||
(DomainStrategy)(0), // 0: xray.transport.internet.DomainStrategy
|
||||
(AddressPortStrategy)(0), // 1: xray.transport.internet.AddressPortStrategy
|
||||
(SocketConfig_TProxyMode)(0), // 2: xray.transport.internet.SocketConfig.TProxyMode
|
||||
(*TransportConfig)(nil), // 3: xray.transport.internet.TransportConfig
|
||||
(*StreamConfig)(nil), // 4: xray.transport.internet.StreamConfig
|
||||
(*UdpHop)(nil), // 5: xray.transport.internet.UdpHop
|
||||
(*QuicParams)(nil), // 6: xray.transport.internet.QuicParams
|
||||
(*CustomSockopt)(nil), // 7: xray.transport.internet.CustomSockopt
|
||||
(*SocketConfig)(nil), // 8: xray.transport.internet.SocketConfig
|
||||
(*HappyEyeballsConfig)(nil), // 9: xray.transport.internet.HappyEyeballsConfig
|
||||
(*serial.TypedMessage)(nil), // 10: xray.common.serial.TypedMessage
|
||||
(*net.IPOrDomain)(nil), // 11: xray.common.net.IPOrDomain
|
||||
(*QuicParams)(nil), // 5: xray.transport.internet.QuicParams
|
||||
(*CustomSockopt)(nil), // 6: xray.transport.internet.CustomSockopt
|
||||
(*SocketConfig)(nil), // 7: xray.transport.internet.SocketConfig
|
||||
(*HappyEyeballsConfig)(nil), // 8: xray.transport.internet.HappyEyeballsConfig
|
||||
(*serial.TypedMessage)(nil), // 9: xray.common.serial.TypedMessage
|
||||
(*net.IPOrDomain)(nil), // 10: xray.common.net.IPOrDomain
|
||||
}
|
||||
var file_transport_internet_config_proto_depIdxs = []int32{
|
||||
10, // 0: xray.transport.internet.TransportConfig.settings:type_name -> xray.common.serial.TypedMessage
|
||||
11, // 1: xray.transport.internet.StreamConfig.address:type_name -> xray.common.net.IPOrDomain
|
||||
9, // 0: xray.transport.internet.TransportConfig.settings:type_name -> xray.common.serial.TypedMessage
|
||||
10, // 1: xray.transport.internet.StreamConfig.address:type_name -> xray.common.net.IPOrDomain
|
||||
3, // 2: xray.transport.internet.StreamConfig.transport_settings:type_name -> xray.transport.internet.TransportConfig
|
||||
10, // 3: xray.transport.internet.StreamConfig.security_settings:type_name -> xray.common.serial.TypedMessage
|
||||
10, // 4: xray.transport.internet.StreamConfig.udpmasks:type_name -> xray.common.serial.TypedMessage
|
||||
10, // 5: xray.transport.internet.StreamConfig.tcpmasks:type_name -> xray.common.serial.TypedMessage
|
||||
6, // 6: xray.transport.internet.StreamConfig.quic_params:type_name -> xray.transport.internet.QuicParams
|
||||
8, // 7: xray.transport.internet.StreamConfig.socket_settings:type_name -> xray.transport.internet.SocketConfig
|
||||
5, // 8: xray.transport.internet.QuicParams.udp_hop:type_name -> xray.transport.internet.UdpHop
|
||||
2, // 9: xray.transport.internet.SocketConfig.tproxy:type_name -> xray.transport.internet.SocketConfig.TProxyMode
|
||||
0, // 10: xray.transport.internet.SocketConfig.domain_strategy:type_name -> xray.transport.internet.DomainStrategy
|
||||
7, // 11: xray.transport.internet.SocketConfig.customSockopt:type_name -> xray.transport.internet.CustomSockopt
|
||||
1, // 12: xray.transport.internet.SocketConfig.address_port_strategy:type_name -> xray.transport.internet.AddressPortStrategy
|
||||
9, // 13: xray.transport.internet.SocketConfig.happy_eyeballs:type_name -> xray.transport.internet.HappyEyeballsConfig
|
||||
14, // [14:14] is the sub-list for method output_type
|
||||
14, // [14:14] is the sub-list for method input_type
|
||||
14, // [14:14] is the sub-list for extension type_name
|
||||
14, // [14:14] is the sub-list for extension extendee
|
||||
0, // [0:14] is the sub-list for field type_name
|
||||
9, // 3: xray.transport.internet.StreamConfig.security_settings:type_name -> xray.common.serial.TypedMessage
|
||||
9, // 4: xray.transport.internet.StreamConfig.udpmasks:type_name -> xray.common.serial.TypedMessage
|
||||
9, // 5: xray.transport.internet.StreamConfig.tcpmasks:type_name -> xray.common.serial.TypedMessage
|
||||
5, // 6: xray.transport.internet.StreamConfig.quic_params:type_name -> xray.transport.internet.QuicParams
|
||||
7, // 7: xray.transport.internet.StreamConfig.socket_settings:type_name -> xray.transport.internet.SocketConfig
|
||||
2, // 8: xray.transport.internet.SocketConfig.tproxy:type_name -> xray.transport.internet.SocketConfig.TProxyMode
|
||||
0, // 9: xray.transport.internet.SocketConfig.domain_strategy:type_name -> xray.transport.internet.DomainStrategy
|
||||
6, // 10: xray.transport.internet.SocketConfig.customSockopt:type_name -> xray.transport.internet.CustomSockopt
|
||||
1, // 11: xray.transport.internet.SocketConfig.address_port_strategy:type_name -> xray.transport.internet.AddressPortStrategy
|
||||
8, // 12: xray.transport.internet.SocketConfig.happy_eyeballs:type_name -> xray.transport.internet.HappyEyeballsConfig
|
||||
13, // [13:13] is the sub-list for method output_type
|
||||
13, // [13:13] is the sub-list for method input_type
|
||||
13, // [13:13] is the sub-list for extension type_name
|
||||
13, // [13:13] is the sub-list for extension extendee
|
||||
0, // [0:13] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_transport_internet_config_proto_init() }
|
||||
@@ -1158,7 +1083,7 @@ func file_transport_internet_config_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_config_proto_rawDesc), len(file_transport_internet_config_proto_rawDesc)),
|
||||
NumEnums: 3,
|
||||
NumMessages: 7,
|
||||
NumMessages: 6,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@@ -64,30 +64,23 @@ message StreamConfig {
|
||||
SocketConfig socket_settings = 6;
|
||||
}
|
||||
|
||||
message UdpHop {
|
||||
repeated uint32 ports = 1;
|
||||
int64 interval_min = 2;
|
||||
int64 interval_max = 3;
|
||||
}
|
||||
|
||||
message QuicParams {
|
||||
string congestion = 1;
|
||||
string bbr_profile = 2;
|
||||
uint64 brutal_up = 3;
|
||||
uint64 brutal_down = 4;
|
||||
bool brutal_disable_loss_compensation = 5;
|
||||
UdpHop udp_hop = 6;
|
||||
uint64 init_stream_receive_window = 7;
|
||||
uint64 max_stream_receive_window = 8;
|
||||
uint64 init_conn_receive_window = 9;
|
||||
uint64 max_conn_receive_window = 10;
|
||||
int64 max_idle_timeout = 11;
|
||||
int64 keep_alive_period = 12;
|
||||
bool disable_path_mtu_discovery = 13;
|
||||
bool disable_chrome_parrot = 14;
|
||||
bool disableGSO = 15;
|
||||
int64 max_incoming_streams = 16;
|
||||
bool disable_stateless_reset = 17;
|
||||
uint64 init_stream_receive_window = 6;
|
||||
uint64 max_stream_receive_window = 7;
|
||||
uint64 init_conn_receive_window = 8;
|
||||
uint64 max_conn_receive_window = 9;
|
||||
int64 max_idle_timeout = 10;
|
||||
int64 keep_alive_period = 11;
|
||||
bool disable_path_mtu_discovery = 12;
|
||||
bool disable_chrome_parrot = 13;
|
||||
bool disableGSO = 14;
|
||||
int64 max_incoming_streams = 15;
|
||||
bool disable_stateless_reset = 16;
|
||||
}
|
||||
|
||||
message CustomSockopt {
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
)
|
||||
|
||||
type Udpmask interface {
|
||||
UDP()
|
||||
|
||||
WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error)
|
||||
WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error)
|
||||
}
|
||||
@@ -21,15 +19,14 @@ type UdpmaskManager struct {
|
||||
}
|
||||
|
||||
func NewUdpmaskManager(udpmasks []Udpmask) *UdpmaskManager {
|
||||
return &UdpmaskManager{
|
||||
udpmasks: udpmasks,
|
||||
}
|
||||
slices.Reverse(udpmasks)
|
||||
return &UdpmaskManager{udpmasks: udpmasks}
|
||||
}
|
||||
|
||||
func (m *UdpmaskManager) WrapPacketConnClient(raw net.PacketConn) (net.PacketConn, error) {
|
||||
var sizes []int
|
||||
var conns []net.PacketConn
|
||||
for i, mask := range slices.Backward(m.udpmasks) {
|
||||
for i, mask := range m.udpmasks {
|
||||
if _, ok := mask.(headerConn); ok {
|
||||
conn, err := mask.WrapPacketConnClient(nil, i, len(m.udpmasks)-1)
|
||||
if err != nil {
|
||||
@@ -62,7 +59,7 @@ func (m *UdpmaskManager) WrapPacketConnClient(raw net.PacketConn) (net.PacketCon
|
||||
func (m *UdpmaskManager) WrapPacketConnServer(raw net.PacketConn) (net.PacketConn, error) {
|
||||
var sizes []int
|
||||
var conns []net.PacketConn
|
||||
for i, mask := range slices.Backward(m.udpmasks) {
|
||||
for i, mask := range m.udpmasks {
|
||||
if _, ok := mask.(headerConn); ok {
|
||||
conn, err := mask.WrapPacketConnServer(nil, i, len(m.udpmasks)-1)
|
||||
if err != nil {
|
||||
@@ -195,8 +192,6 @@ func (c *headerManagerConn) WriteTo(p []byte, addr net.Addr) (n int, err error)
|
||||
}
|
||||
|
||||
type Tcpmask interface {
|
||||
TCP()
|
||||
|
||||
WrapConnClient(net.Conn) (net.Conn, error)
|
||||
WrapConnServer(net.Conn) (net.Conn, error)
|
||||
}
|
||||
@@ -206,14 +201,13 @@ type TcpmaskManager struct {
|
||||
}
|
||||
|
||||
func NewTcpmaskManager(tcpmasks []Tcpmask) *TcpmaskManager {
|
||||
return &TcpmaskManager{
|
||||
tcpmasks: tcpmasks,
|
||||
}
|
||||
slices.Reverse(tcpmasks)
|
||||
return &TcpmaskManager{tcpmasks: tcpmasks}
|
||||
}
|
||||
|
||||
func (m *TcpmaskManager) WrapConnClient(raw net.Conn) (net.Conn, error) {
|
||||
var err error
|
||||
for _, mask := range slices.Backward(m.tcpmasks) {
|
||||
for _, mask := range m.tcpmasks {
|
||||
raw, err = mask.WrapConnClient(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -224,7 +218,7 @@ func (m *TcpmaskManager) WrapConnClient(raw net.Conn) (net.Conn, error) {
|
||||
|
||||
func (m *TcpmaskManager) WrapConnServer(raw net.Conn) (net.Conn, error) {
|
||||
var err error
|
||||
for _, mask := range slices.Backward(m.tcpmasks) {
|
||||
for _, mask := range m.tcpmasks {
|
||||
raw, err = mask.WrapConnServer(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -2,9 +2,6 @@ package fragment
|
||||
|
||||
import "net"
|
||||
|
||||
func (c *Config) TCP() {
|
||||
}
|
||||
|
||||
func (c *Config) WrapConnClient(raw net.Conn) (net.Conn, error) {
|
||||
return NewConnClient(c, raw, false)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *TCPConfig) TCP() {}
|
||||
|
||||
func (c *TCPConfig) WrapConnClient(raw net.Conn) (net.Conn, error) {
|
||||
return NewConnClientTCP(c, raw)
|
||||
}
|
||||
@@ -14,8 +12,6 @@ func (c *TCPConfig) WrapConnServer(raw net.Conn) (net.Conn, error) {
|
||||
return NewConnServerTCP(c, raw)
|
||||
}
|
||||
|
||||
func (c *UDPConfig) UDP() {}
|
||||
|
||||
func (c *UDPConfig) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewConnClientUDP(c, raw)
|
||||
}
|
||||
@@ -24,8 +20,6 @@ func (c *UDPConfig) WrapPacketConnServer(raw net.PacketConn, level int, levelCou
|
||||
return NewConnServerUDP(c, raw)
|
||||
}
|
||||
|
||||
func (c *UDPStandaloneConfig) UDP() {}
|
||||
|
||||
func (c *UDPStandaloneConfig) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewConnClientUDPStandalone(c, raw)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Config) UDP() {}
|
||||
|
||||
func (c *Config) HeaderConn() {}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Config) UDP() {}
|
||||
|
||||
func (c *Config) HeaderConn() {}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Config) UDP() {}
|
||||
|
||||
func (c *Config) HeaderConn() {}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
|
||||
@@ -2,9 +2,6 @@ package noise
|
||||
|
||||
import "net"
|
||||
|
||||
func (c *Config) UDP() {
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewConnClient(c, raw)
|
||||
}
|
||||
|
||||
@@ -5,15 +5,11 @@ import (
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/udphop"
|
||||
)
|
||||
|
||||
func (c *Config) UDP() {}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
_, ok1 := raw.(*internet.FakePacketConn)
|
||||
_, ok2 := raw.(*udphop.UdpHopPacketConn)
|
||||
if level != 0 || ok1 || ok2 {
|
||||
if level != 0 || ok1 {
|
||||
return nil, errors.New("realm requires being at the outermost level")
|
||||
}
|
||||
return NewConnClient(c, raw)
|
||||
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Config) UDP() {}
|
||||
|
||||
func (c *Config) HeaderConn() {}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
@@ -16,8 +14,6 @@ func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount
|
||||
return NewSalamanderConnServer(c, raw)
|
||||
}
|
||||
|
||||
func (c *GeckoConfig) UDP() {}
|
||||
|
||||
func (c *GeckoConfig) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return NewGeckoConnClient(c, raw)
|
||||
}
|
||||
|
||||
@@ -6,12 +6,6 @@ import (
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
func (c *Config) TCP() {
|
||||
}
|
||||
|
||||
func (c *Config) UDP() {
|
||||
}
|
||||
|
||||
// Sudoku in finalmask mode is a pure appearance transform with no standalone handshake.
|
||||
// TCP always keeps classic sudoku on uplink and uses packed downlink optimization on server writes.
|
||||
func (c *Config) WrapConnClient(raw net.Conn) (net.Conn, error) {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package udphop
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
_, ok1 := raw.(*internet.FakePacketConn)
|
||||
if level != 0 || ok1 {
|
||||
return nil, errors.New("udphop requires being at the outermost level")
|
||||
}
|
||||
return NewUDPHopConn(c, raw)
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnServer(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
return nil, errors.New("udphop: client only")
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.5
|
||||
// source: transport/internet/finalmask/udphop/config.proto
|
||||
|
||||
package udphop
|
||||
|
||||
import (
|
||||
internet "github.com/xtls/xray-core/transport/internet"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Sockopt *internet.SocketConfig `protobuf:"bytes,1,opt,name=sockopt,proto3" json:"sockopt,omitempty"`
|
||||
Local bool `protobuf:"varint,2,opt,name=local,proto3" json:"local,omitempty"`
|
||||
Remote bool `protobuf:"varint,3,opt,name=remote,proto3" json:"remote,omitempty"`
|
||||
RemoteOnce bool `protobuf:"varint,4,opt,name=remote_once,json=remoteOnce,proto3" json:"remote_once,omitempty"`
|
||||
IntervalMin int64 `protobuf:"varint,5,opt,name=interval_min,json=intervalMin,proto3" json:"interval_min,omitempty"`
|
||||
IntervalMax int64 `protobuf:"varint,6,opt,name=interval_max,json=intervalMax,proto3" json:"interval_max,omitempty"`
|
||||
RemotePorts []uint32 `protobuf:"varint,7,rep,packed,name=remote_ports,json=remotePorts,proto3" json:"remote_ports,omitempty"`
|
||||
RemoteIPs []string `protobuf:"bytes,8,rep,name=remoteIPs,proto3" json:"remoteIPs,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Config) Reset() {
|
||||
*x = Config{}
|
||||
mi := &file_transport_internet_finalmask_udphop_config_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Config) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Config) ProtoMessage() {}
|
||||
|
||||
func (x *Config) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_finalmask_udphop_config_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
||||
func (*Config) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_finalmask_udphop_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Config) GetSockopt() *internet.SocketConfig {
|
||||
if x != nil {
|
||||
return x.Sockopt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetLocal() bool {
|
||||
if x != nil {
|
||||
return x.Local
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Config) GetRemote() bool {
|
||||
if x != nil {
|
||||
return x.Remote
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Config) GetRemoteOnce() bool {
|
||||
if x != nil {
|
||||
return x.RemoteOnce
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Config) GetIntervalMin() int64 {
|
||||
if x != nil {
|
||||
return x.IntervalMin
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Config) GetIntervalMax() int64 {
|
||||
if x != nil {
|
||||
return x.IntervalMax
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Config) GetRemotePorts() []uint32 {
|
||||
if x != nil {
|
||||
return x.RemotePorts
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetRemoteIPs() []string {
|
||||
if x != nil {
|
||||
return x.RemoteIPs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_transport_internet_finalmask_udphop_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_transport_internet_finalmask_udphop_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"0transport/internet/finalmask/udphop/config.proto\x12(xray.transport.internet.finalmask.udphop\x1a\x1ftransport/internet/config.proto\"\x9f\x02\n" +
|
||||
"\x06Config\x12?\n" +
|
||||
"\asockopt\x18\x01 \x01(\v2%.xray.transport.internet.SocketConfigR\asockopt\x12\x14\n" +
|
||||
"\x05local\x18\x02 \x01(\bR\x05local\x12\x16\n" +
|
||||
"\x06remote\x18\x03 \x01(\bR\x06remote\x12\x1f\n" +
|
||||
"\vremote_once\x18\x04 \x01(\bR\n" +
|
||||
"remoteOnce\x12!\n" +
|
||||
"\finterval_min\x18\x05 \x01(\x03R\vintervalMin\x12!\n" +
|
||||
"\finterval_max\x18\x06 \x01(\x03R\vintervalMax\x12!\n" +
|
||||
"\fremote_ports\x18\a \x03(\rR\vremotePorts\x12\x1c\n" +
|
||||
"\tremoteIPs\x18\b \x03(\tR\tremoteIPsB\x9a\x01\n" +
|
||||
",com.xray.transport.internet.finalmask.udphopP\x01Z=github.com/xtls/xray-core/transport/internet/finalmask/udphop\xaa\x02(Xray.Transport.Internet.Finalmask.Udphopb\x06proto3"
|
||||
|
||||
var (
|
||||
file_transport_internet_finalmask_udphop_config_proto_rawDescOnce sync.Once
|
||||
file_transport_internet_finalmask_udphop_config_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_transport_internet_finalmask_udphop_config_proto_rawDescGZIP() []byte {
|
||||
file_transport_internet_finalmask_udphop_config_proto_rawDescOnce.Do(func() {
|
||||
file_transport_internet_finalmask_udphop_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_udphop_config_proto_rawDesc), len(file_transport_internet_finalmask_udphop_config_proto_rawDesc)))
|
||||
})
|
||||
return file_transport_internet_finalmask_udphop_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_transport_internet_finalmask_udphop_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_transport_internet_finalmask_udphop_config_proto_goTypes = []any{
|
||||
(*Config)(nil), // 0: xray.transport.internet.finalmask.udphop.Config
|
||||
(*internet.SocketConfig)(nil), // 1: xray.transport.internet.SocketConfig
|
||||
}
|
||||
var file_transport_internet_finalmask_udphop_config_proto_depIdxs = []int32{
|
||||
1, // 0: xray.transport.internet.finalmask.udphop.Config.sockopt:type_name -> xray.transport.internet.SocketConfig
|
||||
1, // [1:1] is the sub-list for method output_type
|
||||
1, // [1:1] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_transport_internet_finalmask_udphop_config_proto_init() }
|
||||
func file_transport_internet_finalmask_udphop_config_proto_init() {
|
||||
if File_transport_internet_finalmask_udphop_config_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_udphop_config_proto_rawDesc), len(file_transport_internet_finalmask_udphop_config_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_transport_internet_finalmask_udphop_config_proto_goTypes,
|
||||
DependencyIndexes: file_transport_internet_finalmask_udphop_config_proto_depIdxs,
|
||||
MessageInfos: file_transport_internet_finalmask_udphop_config_proto_msgTypes,
|
||||
}.Build()
|
||||
File_transport_internet_finalmask_udphop_config_proto = out.File
|
||||
file_transport_internet_finalmask_udphop_config_proto_goTypes = nil
|
||||
file_transport_internet_finalmask_udphop_config_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package xray.transport.internet.finalmask.udphop;
|
||||
option csharp_namespace = "Xray.Transport.Internet.Finalmask.Udphop";
|
||||
option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/udphop";
|
||||
option java_package = "com.xray.transport.internet.finalmask.udphop";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "transport/internet/config.proto";
|
||||
|
||||
message Config {
|
||||
xray.transport.internet.SocketConfig sockopt = 1;
|
||||
bool local = 2;
|
||||
bool remote = 3;
|
||||
bool remote_once = 4;
|
||||
int64 interval_min = 5;
|
||||
int64 interval_max = 6;
|
||||
repeated uint32 remote_ports = 7;
|
||||
repeated string remoteIPs = 8;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
package udphop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
goerrors "errors"
|
||||
"io"
|
||||
mrand "math/rand"
|
||||
gonet "net"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/crypto"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/common/net/cnc"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
)
|
||||
|
||||
var pool = sync.Pool{
|
||||
New: func() any {
|
||||
return make([]byte, finalmask.UDPSize)
|
||||
},
|
||||
}
|
||||
|
||||
type packet struct {
|
||||
p []byte
|
||||
addr net.Addr
|
||||
err error
|
||||
}
|
||||
|
||||
type udpHopConn struct {
|
||||
conn net.PacketConn
|
||||
sockopt *internet.SocketConfig
|
||||
local bool
|
||||
remote bool
|
||||
remoteOnce bool
|
||||
|
||||
intervalMin int64
|
||||
intervalMax int64
|
||||
remotePorts []uint32
|
||||
remoteIPs []netip.Prefix
|
||||
|
||||
deadline time.Time
|
||||
readDeadline time.Time
|
||||
writeDeadline time.Time
|
||||
|
||||
pre net.PacketConn
|
||||
cur net.PacketConn
|
||||
addr *net.UDPAddr
|
||||
readCh chan packet
|
||||
closeCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewUDPHopConn(c *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
if c.IntervalMin < 5 || c.IntervalMax < 5 {
|
||||
return nil, errors.New("invalid interval")
|
||||
}
|
||||
remoteIPs := make([]netip.Prefix, 0, len(c.RemoteIPs))
|
||||
for _, ip := range c.RemoteIPs {
|
||||
remoteIPs = append(remoteIPs, netip.MustParsePrefix(ip))
|
||||
}
|
||||
conn := &udpHopConn{
|
||||
conn: raw,
|
||||
sockopt: c.Sockopt,
|
||||
local: c.Local,
|
||||
remote: c.Remote,
|
||||
remoteOnce: c.RemoteOnce,
|
||||
|
||||
intervalMin: c.IntervalMin,
|
||||
intervalMax: c.IntervalMax,
|
||||
remotePorts: c.RemotePorts,
|
||||
remoteIPs: remoteIPs,
|
||||
|
||||
readCh: make(chan packet),
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *udpHopConn) closed() bool {
|
||||
select {
|
||||
case <-c.closeCh:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (c *udpHopConn) hop(addr *net.UDPAddr) {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
newAddr := &net.UDPAddr{IP: addr.IP, Port: addr.Port}
|
||||
newConn := c.conn
|
||||
if c.remote || c.remoteOnce && c.addr == nil {
|
||||
if len(c.remotePorts) > 0 {
|
||||
newAddr.Port = int(c.remotePorts[mrand.Intn(len(c.remotePorts))])
|
||||
}
|
||||
if len(c.remoteIPs) > 0 {
|
||||
newAddr.IP = randPrefix(c.remoteIPs[mrand.Intn(len(c.remoteIPs))])
|
||||
}
|
||||
}
|
||||
if c.local {
|
||||
raw, err := internet.DialSystem(context.Background(), net.UDPDestination(net.IPAddress(newAddr.IP), net.Port(newAddr.Port)), c.sockopt)
|
||||
if err != nil {
|
||||
errors.LogErrorInner(context.Background(), err, "hop err")
|
||||
return
|
||||
}
|
||||
switch c := raw.(type) {
|
||||
case *internet.PacketConnWrapper:
|
||||
newConn = c.PacketConn
|
||||
case *cnc.Connection:
|
||||
newConn = &internet.FakePacketConn{Conn: c}
|
||||
default:
|
||||
panic(reflect.TypeOf(c))
|
||||
}
|
||||
newConn.SetDeadline(c.deadline)
|
||||
newConn.SetReadDeadline(c.readDeadline)
|
||||
newConn.SetWriteDeadline(c.writeDeadline)
|
||||
if c.pre != nil {
|
||||
_ = c.pre.Close()
|
||||
}
|
||||
c.pre = c.cur
|
||||
c.wg.Add(1)
|
||||
go c.recv(newConn)
|
||||
}
|
||||
c.addr = newAddr
|
||||
c.cur = newConn
|
||||
}
|
||||
|
||||
func (c *udpHopConn) recv(conn net.PacketConn) {
|
||||
defer c.wg.Done()
|
||||
|
||||
for {
|
||||
if c.closed() {
|
||||
return
|
||||
}
|
||||
p := pool.Get().([]byte)
|
||||
n, addr, err := conn.ReadFrom(p)
|
||||
if err != nil {
|
||||
pool.Put(p[:cap(p)])
|
||||
if goerrors.Is(err, io.EOF) || goerrors.Is(err, io.ErrClosedPipe) || goerrors.Is(err, gonet.ErrClosed) {
|
||||
break
|
||||
}
|
||||
var netErr net.Error
|
||||
if goerrors.As(err, &netErr) && netErr.Timeout() {
|
||||
select {
|
||||
case c.readCh <- packet{err: err}:
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
errors.LogErrorInner(context.Background(), err, "recv err")
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case c.readCh <- packet{p: p[:n], addr: addr}:
|
||||
case <-c.closeCh:
|
||||
pool.Put(p[:cap(p)])
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *udpHopConn) hopLoop() {
|
||||
ticker := time.NewTicker(time.Second * time.Duration(crypto.RandBetween(c.intervalMin, c.intervalMax+1)))
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
ticker.Reset(time.Second * time.Duration(crypto.RandBetween(c.intervalMin, c.intervalMax+1)))
|
||||
c.mu.Lock()
|
||||
c.hop(c.addr)
|
||||
c.mu.Unlock()
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *udpHopConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
packet, ok := <-c.readCh
|
||||
if ok {
|
||||
if packet.p != nil {
|
||||
n = copy(p, packet.p)
|
||||
pool.Put(packet.p[:cap(packet.p)])
|
||||
}
|
||||
return n, packet.addr, packet.err
|
||||
}
|
||||
return 0, nil, io.EOF
|
||||
}
|
||||
|
||||
func (c *udpHopConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.cur == nil {
|
||||
c.hop(addr.(*net.UDPAddr))
|
||||
if c.cur == nil {
|
||||
return 0, nil
|
||||
}
|
||||
go c.hopLoop()
|
||||
}
|
||||
|
||||
_, err = c.cur.WriteTo(p, c.addr)
|
||||
if err != nil {
|
||||
errors.LogErrorInner(context.Background(), err, "send err")
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (c *udpHopConn) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.closed() {
|
||||
return nil
|
||||
}
|
||||
close(c.closeCh)
|
||||
if c.pre != nil {
|
||||
_ = c.pre.Close()
|
||||
}
|
||||
if c.cur != nil {
|
||||
_ = c.cur.Close()
|
||||
}
|
||||
_ = c.conn.Close()
|
||||
c.wg.Wait()
|
||||
select {
|
||||
case p := <-c.readCh:
|
||||
if p.p != nil {
|
||||
pool.Put(p.p[:cap(p.p)])
|
||||
}
|
||||
default:
|
||||
}
|
||||
close(c.readCh)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *udpHopConn) LocalAddr() net.Addr {
|
||||
return c.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *udpHopConn) SetDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.deadline = t
|
||||
if c.pre != nil {
|
||||
_ = c.pre.SetDeadline(t)
|
||||
}
|
||||
if c.cur != nil {
|
||||
_ = c.cur.SetDeadline(t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *udpHopConn) SetReadDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.readDeadline = t
|
||||
if c.pre != nil {
|
||||
_ = c.pre.SetReadDeadline(t)
|
||||
}
|
||||
if c.cur != nil {
|
||||
_ = c.cur.SetReadDeadline(t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *udpHopConn) SetWriteDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.writeDeadline = t
|
||||
if c.pre != nil {
|
||||
_ = c.pre.SetWriteDeadline(t)
|
||||
}
|
||||
if c.cur != nil {
|
||||
_ = c.cur.SetWriteDeadline(t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func randPrefix(p netip.Prefix) []byte {
|
||||
if p.IsSingleIP() {
|
||||
return p.Addr().AsSlice()
|
||||
}
|
||||
b := p.Addr().AsSlice()
|
||||
prefix := p.Bits()
|
||||
var new [16]byte
|
||||
common.Must2(rand.Read(new[:len(b)]))
|
||||
i := prefix / 8
|
||||
j := prefix % 8
|
||||
if i+1 < len(b) {
|
||||
copy(b[i+1:], new[i+1:])
|
||||
}
|
||||
mask := byte(0xff << (8 - j))
|
||||
b[i] = (b[i] & mask) | (new[i] &^ mask)
|
||||
return b
|
||||
}
|
||||
@@ -4,9 +4,6 @@ import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Config) UDP() {
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
// _, ok1 := raw.(*internet.FakePacketConn)
|
||||
// _, ok2 := raw.(*udphop.UdpHopPacketConn)
|
||||
|
||||
@@ -45,7 +45,8 @@ type xicmpConnClient struct {
|
||||
id int
|
||||
seq int
|
||||
readCh chan packet
|
||||
closedCh chan struct{}
|
||||
closeCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
@@ -81,9 +82,10 @@ func NewConnClient(c *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
id: mathrand.Intn(65536),
|
||||
seq: 1,
|
||||
readCh: make(chan packet),
|
||||
closedCh: make(chan struct{}),
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
conn.wg.Add(2)
|
||||
go conn.recv4()
|
||||
go conn.recv6()
|
||||
|
||||
@@ -96,7 +98,7 @@ func (c *xicmpConnClient) ring(a, b uint16) uint16 {
|
||||
|
||||
func (c *xicmpConnClient) closed() bool {
|
||||
select {
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -104,8 +106,9 @@ func (c *xicmpConnClient) closed() bool {
|
||||
}
|
||||
|
||||
func (c *xicmpConnClient) recv4() {
|
||||
var b [finalmask.UDPSize]byte
|
||||
defer c.wg.Done()
|
||||
|
||||
var b [finalmask.UDPSize]byte
|
||||
for {
|
||||
if c.closed() {
|
||||
return
|
||||
@@ -119,10 +122,11 @@ func (c *xicmpConnClient) recv4() {
|
||||
case c.readCh <- packet{
|
||||
err: err,
|
||||
}:
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
errors.LogErrorInner(context.Background(), err, "recv4 err")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -166,7 +170,7 @@ func (c *xicmpConnClient) recv4() {
|
||||
p: p,
|
||||
addr: addr,
|
||||
}:
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
pool.Put(p)
|
||||
return
|
||||
}
|
||||
@@ -174,11 +178,12 @@ func (c *xicmpConnClient) recv4() {
|
||||
}
|
||||
|
||||
func (c *xicmpConnClient) recv6() {
|
||||
var b [finalmask.UDPSize]byte
|
||||
defer c.wg.Done()
|
||||
|
||||
var b [finalmask.UDPSize]byte
|
||||
for {
|
||||
if c.closed() {
|
||||
break
|
||||
return
|
||||
}
|
||||
|
||||
n, addr, err := c.icmp6.ReadFrom(b[:])
|
||||
@@ -189,10 +194,11 @@ func (c *xicmpConnClient) recv6() {
|
||||
case c.readCh <- packet{
|
||||
err: err,
|
||||
}:
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
errors.LogErrorInner(context.Background(), err, "recv6 err")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -236,7 +242,7 @@ func (c *xicmpConnClient) recv6() {
|
||||
p: p,
|
||||
addr: addr,
|
||||
}:
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
pool.Put(p)
|
||||
return
|
||||
}
|
||||
@@ -244,16 +250,15 @@ func (c *xicmpConnClient) recv6() {
|
||||
}
|
||||
|
||||
func (c *xicmpConnClient) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
select {
|
||||
case packet := <-c.readCh:
|
||||
packet, ok := <-c.readCh
|
||||
if ok {
|
||||
if packet.p != nil {
|
||||
n = copy(p, packet.p)
|
||||
pool.Put(packet.p)
|
||||
}
|
||||
return n, packet.addr, packet.err
|
||||
case <-c.closedCh:
|
||||
return 0, nil, io.EOF
|
||||
}
|
||||
return 0, nil, io.EOF
|
||||
}
|
||||
|
||||
func (c *xicmpConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
@@ -294,10 +299,9 @@ func (c *xicmpConnClient) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errors.LogErrorInner(context.Background(), err, "xicmp write")
|
||||
errors.LogErrorInner(context.Background(), err, "send err")
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
@@ -307,10 +311,19 @@ func (c *xicmpConnClient) Close() error {
|
||||
if c.closed() {
|
||||
return nil
|
||||
}
|
||||
close(c.closedCh)
|
||||
close(c.closeCh)
|
||||
_ = c.icmp4.Close()
|
||||
_ = c.icmp6.Close()
|
||||
_ = c.conn.Close()
|
||||
c.wg.Wait()
|
||||
select {
|
||||
case p := <-c.readCh:
|
||||
if p.p != nil {
|
||||
pool.Put(p.p)
|
||||
}
|
||||
default:
|
||||
}
|
||||
close(c.readCh)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,11 @@ import (
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/udphop"
|
||||
)
|
||||
|
||||
func (c *Config) UDP() {
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(raw net.PacketConn, level int, levelCount int) (net.PacketConn, error) {
|
||||
_, ok1 := raw.(*internet.FakePacketConn)
|
||||
_, ok2 := raw.(*udphop.UdpHopPacketConn)
|
||||
if level != 0 || ok1 || ok2 {
|
||||
if level != 0 || ok1 {
|
||||
return nil, errors.New("xicmp requires being at the outermost level")
|
||||
}
|
||||
return NewConnClient(c, raw)
|
||||
|
||||
@@ -37,14 +37,15 @@ type record struct {
|
||||
}
|
||||
|
||||
type xicmpConnServer struct {
|
||||
conn net.PacketConn
|
||||
icmp4 *icmp.PacketConn
|
||||
icmp6 *icmp.PacketConn
|
||||
ips map[netip.Addr]struct{}
|
||||
rec map[string]record
|
||||
readCh chan packet
|
||||
closedCh chan struct{}
|
||||
mu sync.Mutex
|
||||
conn net.PacketConn
|
||||
icmp4 *icmp.PacketConn
|
||||
icmp6 *icmp.PacketConn
|
||||
ips map[netip.Addr]struct{}
|
||||
rec map[string]record
|
||||
readCh chan packet
|
||||
closeCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
@@ -63,16 +64,17 @@ func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
}
|
||||
|
||||
conn := &xicmpConnServer{
|
||||
conn: raw,
|
||||
icmp4: icmp4,
|
||||
icmp6: icmp6,
|
||||
ips: ips,
|
||||
rec: make(map[string]record),
|
||||
readCh: make(chan packet),
|
||||
closedCh: make(chan struct{}),
|
||||
conn: raw,
|
||||
icmp4: icmp4,
|
||||
icmp6: icmp6,
|
||||
ips: ips,
|
||||
rec: make(map[string]record),
|
||||
readCh: make(chan packet),
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
go conn.clean()
|
||||
conn.wg.Add(2)
|
||||
go conn.recv4()
|
||||
go conn.recv6()
|
||||
|
||||
@@ -81,7 +83,7 @@ func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
|
||||
func (c *xicmpConnServer) closed() bool {
|
||||
select {
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -102,15 +104,16 @@ func (c *xicmpConnServer) clean() {
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *xicmpConnServer) recv4() {
|
||||
var b [finalmask.UDPSize]byte
|
||||
defer c.wg.Done()
|
||||
|
||||
var b [finalmask.UDPSize]byte
|
||||
for {
|
||||
if c.closed() {
|
||||
return
|
||||
@@ -124,10 +127,11 @@ func (c *xicmpConnServer) recv4() {
|
||||
case c.readCh <- packet{
|
||||
err: err,
|
||||
}:
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
errors.LogErrorInner(context.Background(), err, "recv4 err")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -179,7 +183,7 @@ func (c *xicmpConnServer) recv4() {
|
||||
p: p,
|
||||
addr: cAddr,
|
||||
}:
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
pool.Put(p)
|
||||
return
|
||||
}
|
||||
@@ -187,8 +191,9 @@ func (c *xicmpConnServer) recv4() {
|
||||
}
|
||||
|
||||
func (c *xicmpConnServer) recv6() {
|
||||
var b [finalmask.UDPSize]byte
|
||||
defer c.wg.Done()
|
||||
|
||||
var b [finalmask.UDPSize]byte
|
||||
for {
|
||||
if c.closed() {
|
||||
return
|
||||
@@ -202,10 +207,11 @@ func (c *xicmpConnServer) recv6() {
|
||||
case c.readCh <- packet{
|
||||
err: err,
|
||||
}:
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
errors.LogErrorInner(context.Background(), err, "recv6 err")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -257,7 +263,7 @@ func (c *xicmpConnServer) recv6() {
|
||||
p: p,
|
||||
addr: cAddr,
|
||||
}:
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
pool.Put(p)
|
||||
return
|
||||
}
|
||||
@@ -265,16 +271,15 @@ func (c *xicmpConnServer) recv6() {
|
||||
}
|
||||
|
||||
func (c *xicmpConnServer) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
select {
|
||||
case packet := <-c.readCh:
|
||||
packet, ok := <-c.readCh
|
||||
if ok {
|
||||
if packet.p != nil {
|
||||
n = copy(p, packet.p)
|
||||
pool.Put(packet.p)
|
||||
}
|
||||
return n, packet.addr, packet.err
|
||||
case <-c.closedCh:
|
||||
return 0, nil, io.EOF
|
||||
}
|
||||
return 0, nil, io.EOF
|
||||
}
|
||||
|
||||
func (c *xicmpConnServer) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
@@ -310,10 +315,9 @@ func (c *xicmpConnServer) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errors.LogErrorInner(context.Background(), err, "xicmp write")
|
||||
errors.LogErrorInner(context.Background(), err, "send err")
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
@@ -323,10 +327,19 @@ func (c *xicmpConnServer) Close() error {
|
||||
if c.closed() {
|
||||
return nil
|
||||
}
|
||||
close(c.closedCh)
|
||||
close(c.closeCh)
|
||||
_ = c.icmp4.Close()
|
||||
_ = c.icmp6.Close()
|
||||
_ = c.conn.Close()
|
||||
c.wg.Wait()
|
||||
select {
|
||||
case p := <-c.readCh:
|
||||
if p.p != nil {
|
||||
pool.Put(p.p)
|
||||
}
|
||||
default:
|
||||
}
|
||||
close(c.readCh)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -39,16 +39,17 @@ type record struct {
|
||||
}
|
||||
|
||||
type xicmpConnServer struct {
|
||||
conn net.PacketConn
|
||||
icmp4 *icmp.PacketConn
|
||||
icmp6 *icmp.PacketConn
|
||||
ipv4PC *ipv4.PacketConn
|
||||
ipv6PC *ipv6.PacketConn
|
||||
ips map[netip.Addr]struct{}
|
||||
rec map[string]record
|
||||
readCh chan packet
|
||||
closedCh chan struct{}
|
||||
mu sync.Mutex
|
||||
conn net.PacketConn
|
||||
icmp4 *icmp.PacketConn
|
||||
icmp6 *icmp.PacketConn
|
||||
ipv4PC *ipv4.PacketConn
|
||||
ipv6PC *ipv6.PacketConn
|
||||
ips map[netip.Addr]struct{}
|
||||
rec map[string]record
|
||||
readCh chan packet
|
||||
closeCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
@@ -67,21 +68,22 @@ func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
}
|
||||
|
||||
conn := &xicmpConnServer{
|
||||
conn: raw,
|
||||
icmp4: icmp4,
|
||||
icmp6: icmp6,
|
||||
ipv4PC: icmp4.IPv4PacketConn(),
|
||||
ipv6PC: icmp6.IPv6PacketConn(),
|
||||
ips: ips,
|
||||
rec: make(map[string]record),
|
||||
readCh: make(chan packet),
|
||||
closedCh: make(chan struct{}),
|
||||
conn: raw,
|
||||
icmp4: icmp4,
|
||||
icmp6: icmp6,
|
||||
ipv4PC: icmp4.IPv4PacketConn(),
|
||||
ipv6PC: icmp6.IPv6PacketConn(),
|
||||
ips: ips,
|
||||
rec: make(map[string]record),
|
||||
readCh: make(chan packet),
|
||||
closeCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
common.Must(conn.ipv4PC.SetControlMessage(ipv4.FlagDst, true))
|
||||
common.Must(conn.ipv6PC.SetControlMessage(ipv6.FlagDst, true))
|
||||
|
||||
go conn.clean()
|
||||
conn.wg.Add(2)
|
||||
go conn.recv4()
|
||||
go conn.recv6()
|
||||
|
||||
@@ -90,7 +92,7 @@ func NewConnServer(c *Config, raw net.PacketConn) (net.PacketConn, error) {
|
||||
|
||||
func (c *xicmpConnServer) closed() bool {
|
||||
select {
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -111,15 +113,16 @@ func (c *xicmpConnServer) clean() {
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *xicmpConnServer) recv4() {
|
||||
var b [finalmask.UDPSize]byte
|
||||
defer c.wg.Done()
|
||||
|
||||
var b [finalmask.UDPSize]byte
|
||||
for {
|
||||
if c.closed() {
|
||||
return
|
||||
@@ -133,10 +136,11 @@ func (c *xicmpConnServer) recv4() {
|
||||
case c.readCh <- packet{
|
||||
err: err,
|
||||
}:
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
errors.LogErrorInner(context.Background(), err, "recv4 err")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -189,7 +193,7 @@ func (c *xicmpConnServer) recv4() {
|
||||
p: p,
|
||||
addr: cAddr,
|
||||
}:
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
pool.Put(p)
|
||||
return
|
||||
}
|
||||
@@ -197,8 +201,9 @@ func (c *xicmpConnServer) recv4() {
|
||||
}
|
||||
|
||||
func (c *xicmpConnServer) recv6() {
|
||||
var b [finalmask.UDPSize]byte
|
||||
defer c.wg.Done()
|
||||
|
||||
var b [finalmask.UDPSize]byte
|
||||
for {
|
||||
if c.closed() {
|
||||
return
|
||||
@@ -212,10 +217,11 @@ func (c *xicmpConnServer) recv6() {
|
||||
case c.readCh <- packet{
|
||||
err: err,
|
||||
}:
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
errors.LogErrorInner(context.Background(), err, "recv6 err")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -268,7 +274,7 @@ func (c *xicmpConnServer) recv6() {
|
||||
p: p,
|
||||
addr: cAddr,
|
||||
}:
|
||||
case <-c.closedCh:
|
||||
case <-c.closeCh:
|
||||
pool.Put(p)
|
||||
return
|
||||
}
|
||||
@@ -276,16 +282,15 @@ func (c *xicmpConnServer) recv6() {
|
||||
}
|
||||
|
||||
func (c *xicmpConnServer) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
select {
|
||||
case packet := <-c.readCh:
|
||||
packet, ok := <-c.readCh
|
||||
if ok {
|
||||
if packet.p != nil {
|
||||
n = copy(p, packet.p)
|
||||
pool.Put(packet.p)
|
||||
}
|
||||
return n, packet.addr, packet.err
|
||||
case <-c.closedCh:
|
||||
return 0, nil, io.EOF
|
||||
}
|
||||
return 0, nil, io.EOF
|
||||
}
|
||||
|
||||
func (c *xicmpConnServer) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
@@ -321,10 +326,9 @@ func (c *xicmpConnServer) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errors.LogErrorInner(context.Background(), err, "xicmp write")
|
||||
errors.LogErrorInner(context.Background(), err, "send err")
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
@@ -334,10 +338,19 @@ func (c *xicmpConnServer) Close() error {
|
||||
if c.closed() {
|
||||
return nil
|
||||
}
|
||||
close(c.closedCh)
|
||||
close(c.closeCh)
|
||||
_ = c.icmp4.Close()
|
||||
_ = c.icmp6.Close()
|
||||
_ = c.conn.Close()
|
||||
c.wg.Wait()
|
||||
select {
|
||||
case p := <-c.readCh:
|
||||
if p.p != nil {
|
||||
pool.Put(p.p)
|
||||
}
|
||||
default:
|
||||
}
|
||||
close(c.readCh)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,6 @@ import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Config) TCP() {
|
||||
}
|
||||
|
||||
func (c *Config) WrapConnClient(conn net.Conn) (net.Conn, error) {
|
||||
profiles, err := profilesFromConfig(c.Profiles)
|
||||
if err != nil {
|
||||
|
||||
@@ -103,14 +103,11 @@ func (c *InterConn) Update() {
|
||||
|
||||
func (c *InterConn) Read(p []byte) (int, error) {
|
||||
b, ok := <-c.ch
|
||||
if !ok {
|
||||
return 0, io.EOF
|
||||
if ok {
|
||||
c.Update()
|
||||
return copy(p, b), nil
|
||||
}
|
||||
if len(p) < len(b) {
|
||||
return 0, io.ErrShortBuffer
|
||||
}
|
||||
c.Update()
|
||||
return copy(p, b), nil
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (c *InterConn) Write(p []byte) (int, error) {
|
||||
|
||||
@@ -3,7 +3,6 @@ package hysteria
|
||||
import (
|
||||
"context"
|
||||
go_tls "crypto/tls"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
@@ -22,7 +21,6 @@ import (
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/congestion"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/udphop"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
)
|
||||
@@ -78,7 +76,6 @@ func (c *client) dial(ctx context.Context) error {
|
||||
if quicParams == nil {
|
||||
quicParams = &internet.QuicParams{
|
||||
BbrProfile: string(bbr.ProfileStandard),
|
||||
UdpHop: &internet.UdpHop{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,35 +112,8 @@ func (c *client) dial(ctx context.Context) error {
|
||||
// quicConfig.KeepAlivePeriod = 10 * time.Second
|
||||
// }
|
||||
|
||||
udpHopDialer := func(addr *net.UDPAddr) (net.PacketConn, error) {
|
||||
conn, err := internet.DialSystem(ctx, net.UDPDestination(net.IPAddress(addr.IP), net.Port(addr.Port)), c.socketConfig)
|
||||
if err != nil {
|
||||
errors.LogInfoInner(context.Background(), err, "skip hop: failed to dial to dest")
|
||||
return nil, errors.New("")
|
||||
}
|
||||
|
||||
var pktConn net.PacketConn
|
||||
|
||||
switch c := conn.(type) {
|
||||
case *internet.PacketConnWrapper:
|
||||
pktConn = c.PacketConn
|
||||
case *cnc.Connection:
|
||||
pktConn = &internet.FakePacketConn{Conn: c}
|
||||
default:
|
||||
panic(reflect.TypeOf(c))
|
||||
}
|
||||
|
||||
return pktConn, nil
|
||||
}
|
||||
|
||||
var pktConn net.PacketConn
|
||||
var udpAddr *net.UDPAddr
|
||||
var index int
|
||||
|
||||
if len(quicParams.UdpHop.Ports) > 0 {
|
||||
index = rand.Intn(len(quicParams.UdpHop.Ports))
|
||||
c.dest.Port = net.Port(quicParams.UdpHop.Ports[index])
|
||||
}
|
||||
|
||||
raw, err := internet.DialSystem(ctx, c.dest, c.socketConfig)
|
||||
if err != nil {
|
||||
@@ -160,10 +130,6 @@ func (c *client) dial(ctx context.Context) error {
|
||||
panic(reflect.TypeOf(c))
|
||||
}
|
||||
|
||||
if len(quicParams.UdpHop.Ports) > 0 {
|
||||
pktConn = udphop.NewUDPHopPacketConn(udphop.ToAddrs(udpAddr.IP, quicParams.UdpHop.Ports), time.Duration(quicParams.UdpHop.IntervalMin)*time.Second, time.Duration(quicParams.UdpHop.IntervalMax)*time.Second, udpHopDialer, pktConn, index)
|
||||
}
|
||||
|
||||
if c.udpmaskManager != nil {
|
||||
newConn, err := c.udpmaskManager.WrapPacketConnClient(pktConn)
|
||||
if err != nil {
|
||||
|
||||
@@ -281,7 +281,6 @@ func Listen(ctx context.Context, address net.Address, port net.Port, streamSetti
|
||||
if quicParams == nil {
|
||||
quicParams = &internet.QuicParams{
|
||||
BbrProfile: string(bbr.ProfileStandard),
|
||||
UdpHop: &internet.UdpHop{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
package udphop
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
)
|
||||
|
||||
const (
|
||||
packetQueueSize = 1024
|
||||
udpBufferSize = finalmask.UDPSize
|
||||
|
||||
defaultHopInterval = 30 * time.Second
|
||||
)
|
||||
|
||||
type UdpHopPacketConn struct {
|
||||
Addrs []net.Addr
|
||||
HopIntervalMin time.Duration
|
||||
HopIntervalMax time.Duration
|
||||
ListenUDPFunc func(addr *net.UDPAddr) (net.PacketConn, error)
|
||||
|
||||
connMutex sync.RWMutex
|
||||
prevConn net.PacketConn
|
||||
currentConn net.PacketConn
|
||||
addrIndex int
|
||||
|
||||
deadline time.Time
|
||||
readDeadline time.Time
|
||||
writeDeadline time.Time
|
||||
|
||||
recvQueue chan *udpPacket
|
||||
closeChan chan struct{}
|
||||
closed bool
|
||||
|
||||
bufPool sync.Pool
|
||||
}
|
||||
|
||||
type udpPacket struct {
|
||||
Buf []byte
|
||||
N int
|
||||
Addr net.Addr
|
||||
Err error
|
||||
}
|
||||
|
||||
func NewUDPHopPacketConn(addrs []net.Addr, hopIntervalMin time.Duration, hopIntervalMax time.Duration, listenUDPFunc func(addr *net.UDPAddr) (net.PacketConn, error), currentConn net.PacketConn, addrIndex int) net.PacketConn {
|
||||
if len(addrs) == 0 {
|
||||
panic("len(addrs) == 0")
|
||||
}
|
||||
if hopIntervalMin == 0 {
|
||||
hopIntervalMin = defaultHopInterval
|
||||
}
|
||||
if hopIntervalMax == 0 {
|
||||
hopIntervalMax = defaultHopInterval
|
||||
}
|
||||
if hopIntervalMin < 5*time.Second {
|
||||
panic("hopIntervalMin < 5*time.Second")
|
||||
}
|
||||
if hopIntervalMax < 5*time.Second {
|
||||
panic("hopIntervalMax < 5*time.Second")
|
||||
}
|
||||
if hopIntervalMax < hopIntervalMin {
|
||||
panic("hopIntervalMax < hopIntervalMin")
|
||||
}
|
||||
if listenUDPFunc == nil {
|
||||
panic("listenUDPFunc is nil")
|
||||
}
|
||||
hConn := &UdpHopPacketConn{
|
||||
Addrs: addrs,
|
||||
HopIntervalMin: hopIntervalMin,
|
||||
HopIntervalMax: hopIntervalMax,
|
||||
ListenUDPFunc: listenUDPFunc,
|
||||
prevConn: nil,
|
||||
currentConn: currentConn,
|
||||
addrIndex: addrIndex,
|
||||
recvQueue: make(chan *udpPacket, packetQueueSize),
|
||||
closeChan: make(chan struct{}),
|
||||
bufPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make([]byte, udpBufferSize)
|
||||
},
|
||||
},
|
||||
}
|
||||
go hConn.recvLoop(hConn.currentConn)
|
||||
go hConn.hopLoop()
|
||||
return hConn
|
||||
}
|
||||
|
||||
func (u *UdpHopPacketConn) recvLoop(conn net.PacketConn) {
|
||||
for {
|
||||
buf := u.bufPool.Get().([]byte)
|
||||
n, addr, err := conn.ReadFrom(buf)
|
||||
if err != nil {
|
||||
u.bufPool.Put(buf)
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
u.recvQueue <- &udpPacket{nil, 0, nil, netErr}
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
select {
|
||||
case u.recvQueue <- &udpPacket{buf, n, addr, nil}:
|
||||
default:
|
||||
u.bufPool.Put(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UdpHopPacketConn) hopLoop() {
|
||||
timer := time.NewTimer(u.nextHopInterval())
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
u.hop()
|
||||
timer.Reset(u.nextHopInterval())
|
||||
case <-u.closeChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UdpHopPacketConn) nextHopInterval() time.Duration {
|
||||
if u.HopIntervalMin == u.HopIntervalMax {
|
||||
return u.HopIntervalMin
|
||||
}
|
||||
return u.HopIntervalMin + time.Duration(rand.Int63n(int64(u.HopIntervalMax-u.HopIntervalMin)+1))
|
||||
}
|
||||
|
||||
func (u *UdpHopPacketConn) hop() {
|
||||
u.connMutex.Lock()
|
||||
defer u.connMutex.Unlock()
|
||||
if u.closed {
|
||||
return
|
||||
}
|
||||
addrIndex := rand.Intn(len(u.Addrs))
|
||||
newConn, err := u.ListenUDPFunc(u.Addrs[addrIndex].(*net.UDPAddr))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if u.prevConn != nil {
|
||||
_ = u.prevConn.Close()
|
||||
}
|
||||
u.prevConn = u.currentConn
|
||||
u.addrIndex = addrIndex
|
||||
u.currentConn = newConn
|
||||
if !u.deadline.IsZero() {
|
||||
_ = u.currentConn.SetDeadline(u.deadline)
|
||||
}
|
||||
if !u.readDeadline.IsZero() {
|
||||
_ = u.currentConn.SetReadDeadline(u.readDeadline)
|
||||
}
|
||||
if !u.writeDeadline.IsZero() {
|
||||
_ = u.currentConn.SetWriteDeadline(u.writeDeadline)
|
||||
}
|
||||
go u.recvLoop(newConn)
|
||||
}
|
||||
|
||||
func (u *UdpHopPacketConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
|
||||
for {
|
||||
select {
|
||||
case p := <-u.recvQueue:
|
||||
if p.Err != nil {
|
||||
return 0, nil, p.Err
|
||||
}
|
||||
n := copy(b, p.Buf[:p.N])
|
||||
u.bufPool.Put(p.Buf)
|
||||
return n, p.Addr, nil
|
||||
case <-u.closeChan:
|
||||
return 0, nil, net.ErrClosed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (u *UdpHopPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
||||
u.connMutex.RLock()
|
||||
defer u.connMutex.RUnlock()
|
||||
if u.closed {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
return u.currentConn.WriteTo(b, u.Addrs[u.addrIndex])
|
||||
}
|
||||
|
||||
func (u *UdpHopPacketConn) Close() error {
|
||||
u.connMutex.Lock()
|
||||
defer u.connMutex.Unlock()
|
||||
if u.closed {
|
||||
return nil
|
||||
}
|
||||
if u.prevConn != nil {
|
||||
_ = u.prevConn.Close()
|
||||
}
|
||||
err := u.currentConn.Close()
|
||||
close(u.closeChan)
|
||||
u.closed = true
|
||||
u.Addrs = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *UdpHopPacketConn) LocalAddr() net.Addr {
|
||||
u.connMutex.RLock()
|
||||
defer u.connMutex.RUnlock()
|
||||
return u.currentConn.LocalAddr()
|
||||
}
|
||||
|
||||
func (u *UdpHopPacketConn) SetDeadline(t time.Time) error {
|
||||
u.connMutex.Lock()
|
||||
defer u.connMutex.Unlock()
|
||||
u.deadline = t
|
||||
u.readDeadline = t
|
||||
u.writeDeadline = t
|
||||
if u.prevConn != nil {
|
||||
_ = u.prevConn.SetDeadline(t)
|
||||
}
|
||||
return u.currentConn.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (u *UdpHopPacketConn) SetReadDeadline(t time.Time) error {
|
||||
u.connMutex.Lock()
|
||||
defer u.connMutex.Unlock()
|
||||
u.deadline = time.Time{}
|
||||
u.readDeadline = t
|
||||
if u.prevConn != nil {
|
||||
_ = u.prevConn.SetReadDeadline(t)
|
||||
}
|
||||
return u.currentConn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (u *UdpHopPacketConn) SetWriteDeadline(t time.Time) error {
|
||||
u.connMutex.Lock()
|
||||
defer u.connMutex.Unlock()
|
||||
u.deadline = time.Time{}
|
||||
u.writeDeadline = t
|
||||
if u.prevConn != nil {
|
||||
_ = u.prevConn.SetWriteDeadline(t)
|
||||
}
|
||||
return u.currentConn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func ToAddrs(ip net.IP, ports []uint32) []net.Addr {
|
||||
var addrs []net.Addr
|
||||
for _, port := range ports {
|
||||
addr := &net.UDPAddr{
|
||||
IP: ip,
|
||||
Port: int(port),
|
||||
}
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
gotls "crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
@@ -28,7 +27,6 @@ import (
|
||||
"github.com/xtls/xray-core/transport/internet/browser_dialer"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/congestion"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/congestion/bbr"
|
||||
"github.com/xtls/xray-core/transport/internet/hysteria/udphop"
|
||||
"github.com/xtls/xray-core/transport/internet/reality"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
@@ -162,7 +160,6 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea
|
||||
if quicParams == nil {
|
||||
quicParams = &internet.QuicParams{
|
||||
BbrProfile: string(bbr.ProfileStandard),
|
||||
UdpHop: &internet.UdpHop{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,35 +195,8 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea
|
||||
QUICConfig: quicConfig,
|
||||
TLSClientConfig: gotlsConfig,
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *gotls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
||||
udpHopDialer := func(addr *net.UDPAddr) (net.PacketConn, error) {
|
||||
conn, err := internet.DialSystem(ctx, net.UDPDestination(net.IPAddress(addr.IP), net.Port(addr.Port)), streamSettings.SocketSettings)
|
||||
if err != nil {
|
||||
errors.LogInfoInner(context.Background(), err, "skip hop: failed to dial to dest")
|
||||
return nil, errors.New("")
|
||||
}
|
||||
|
||||
var pktConn net.PacketConn
|
||||
|
||||
switch c := conn.(type) {
|
||||
case *internet.PacketConnWrapper:
|
||||
pktConn = c.PacketConn
|
||||
case *cnc.Connection:
|
||||
pktConn = &internet.FakePacketConn{Conn: c}
|
||||
default:
|
||||
panic(reflect.TypeOf(c))
|
||||
}
|
||||
|
||||
return pktConn, nil
|
||||
}
|
||||
|
||||
var pktConn net.PacketConn
|
||||
var udpAddr *net.UDPAddr
|
||||
var index int
|
||||
|
||||
if len(quicParams.UdpHop.Ports) > 0 {
|
||||
index = rand.Intn(len(quicParams.UdpHop.Ports))
|
||||
dest.Port = net.Port(quicParams.UdpHop.Ports[index])
|
||||
}
|
||||
|
||||
raw, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
|
||||
if err != nil {
|
||||
@@ -243,10 +213,6 @@ func createHTTPClient(dest net.Destination, streamSettings *internet.MemoryStrea
|
||||
panic(reflect.TypeOf(c))
|
||||
}
|
||||
|
||||
if len(quicParams.UdpHop.Ports) > 0 {
|
||||
pktConn = udphop.NewUDPHopPacketConn(udphop.ToAddrs(udpAddr.IP, quicParams.UdpHop.Ports), time.Duration(quicParams.UdpHop.IntervalMin)*time.Second, time.Duration(quicParams.UdpHop.IntervalMax)*time.Second, udpHopDialer, pktConn, index)
|
||||
}
|
||||
|
||||
if streamSettings.UdpmaskManager != nil {
|
||||
newConn, err := streamSettings.UdpmaskManager.WrapPacketConnClient(pktConn)
|
||||
if err != nil {
|
||||
|
||||
@@ -493,7 +493,6 @@ func ListenXH(ctx context.Context, address net.Address, port net.Port, streamSet
|
||||
if quicParams == nil {
|
||||
quicParams = &internet.QuicParams{
|
||||
BbrProfile: string(bbr.ProfileStandard),
|
||||
UdpHop: &internet.UdpHop{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
gotls "crypto/tls"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/reality"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
type serviceTransport struct {
|
||||
plain http.RoundTripper
|
||||
secure http.RoundTripper
|
||||
}
|
||||
|
||||
func (t *serviceTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
if r.URL.Scheme == "https" {
|
||||
return t.secure.RoundTrip(r)
|
||||
}
|
||||
return t.plain.RoundTrip(r)
|
||||
}
|
||||
|
||||
func newServiceClient(streamSettings *internet.MemoryStreamConfig, timeout time.Duration, maxConns int) *http.Client {
|
||||
var (
|
||||
tlsConfig *tls.Config
|
||||
realityConfig *reality.Config
|
||||
sockopt *internet.SocketConfig
|
||||
fronting *net.Destination
|
||||
)
|
||||
if streamSettings != nil {
|
||||
tlsConfig = tls.ConfigFromStreamSettings(streamSettings)
|
||||
realityConfig = reality.ConfigFromStreamSettings(streamSettings)
|
||||
sockopt = streamSettings.SocketSettings
|
||||
fronting = streamSettings.Destination
|
||||
}
|
||||
overHTTP2 := allowsHTTP2(tlsConfig, realityConfig)
|
||||
|
||||
dial := func(ctx context.Context, addr string) (net.Conn, net.Destination, error) {
|
||||
host, err := net.ParseDestination("tcp:" + addr)
|
||||
if err != nil {
|
||||
return nil, host, errors.New("bad address: ", addr).Base(err)
|
||||
}
|
||||
|
||||
target := host
|
||||
if fronting != nil {
|
||||
target.Address = fronting.Address
|
||||
if fronting.Port != 0 {
|
||||
target.Port = fronting.Port
|
||||
}
|
||||
}
|
||||
|
||||
conn, err := internet.DialSystem(ctx, target, sockopt)
|
||||
if err != nil {
|
||||
return nil, host, err
|
||||
}
|
||||
if streamSettings != nil && streamSettings.TcpmaskManager != nil {
|
||||
masked, err := streamSettings.TcpmaskManager.WrapConnClient(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, host, errors.New("mask err").Base(err)
|
||||
}
|
||||
conn = masked
|
||||
}
|
||||
return conn, host, nil
|
||||
}
|
||||
|
||||
dialPlain := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
conn, _, err := dial(ctx, addr)
|
||||
return conn, err
|
||||
}
|
||||
|
||||
dialTLS := func(ctx context.Context, addr string) (net.Conn, error) {
|
||||
conn, host, err := dial(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if realityConfig != nil {
|
||||
return reality.UClient(conn, realityConfig, ctx, host)
|
||||
}
|
||||
|
||||
gotlsConfig := &gotls.Config{ServerName: host.Address.String()}
|
||||
if tlsConfig != nil {
|
||||
gotlsConfig = tlsConfig.GetTLSConfig(tls.WithDestination(host))
|
||||
}
|
||||
if len(gotlsConfig.NextProtos) != 1 {
|
||||
if overHTTP2 {
|
||||
gotlsConfig.NextProtos = []string{"h2"}
|
||||
} else {
|
||||
gotlsConfig.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
}
|
||||
|
||||
if tlsConfig != nil {
|
||||
if fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint); fingerprint != nil {
|
||||
uconn := tls.UClient(conn, gotlsConfig, fingerprint)
|
||||
if err := uconn.(*tls.UConn).HandshakeContext(ctx); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return uconn, nil
|
||||
}
|
||||
}
|
||||
return tls.Client(conn, gotlsConfig), nil
|
||||
}
|
||||
|
||||
var secure http.RoundTripper
|
||||
if overHTTP2 {
|
||||
secure = &http2.Transport{
|
||||
DialTLSContext: func(ctx context.Context, network, addr string, cfg *gotls.Config) (net.Conn, error) {
|
||||
return dialTLS(ctx, addr)
|
||||
},
|
||||
IdleConnTimeout: net.ConnIdleTimeout,
|
||||
}
|
||||
} else {
|
||||
secure = &http.Transport{
|
||||
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialTLS(ctx, addr)
|
||||
},
|
||||
IdleConnTimeout: net.ConnIdleTimeout,
|
||||
MaxIdleConns: maxConns,
|
||||
MaxIdleConnsPerHost: maxConns,
|
||||
MaxConnsPerHost: maxConns,
|
||||
}
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Transport: &serviceTransport{
|
||||
plain: &http.Transport{
|
||||
DialContext: dialPlain,
|
||||
IdleConnTimeout: net.ConnIdleTimeout,
|
||||
MaxIdleConns: maxConns,
|
||||
MaxIdleConnsPerHost: maxConns,
|
||||
MaxConnsPerHost: maxConns,
|
||||
},
|
||||
secure: secure,
|
||||
},
|
||||
Timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func allowsHTTP2(tlsConfig *tls.Config, realityConfig *reality.Config) bool {
|
||||
if realityConfig != nil {
|
||||
return true
|
||||
}
|
||||
if tlsConfig == nil {
|
||||
return false
|
||||
}
|
||||
return len(tlsConfig.NextProtocol) == 1 && tlsConfig.NextProtocol[0] == "h2"
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
gotls "crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
xnet "github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
)
|
||||
|
||||
func recordingTLSListener(t *testing.T, sni *string, mu *sync.Mutex) net.Listener {
|
||||
t.Helper()
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cfg := &gotls.Config{
|
||||
GetConfigForClient: func(hello *gotls.ClientHelloInfo) (*gotls.Config, error) {
|
||||
mu.Lock()
|
||||
*sni = hello.ServerName
|
||||
mu.Unlock()
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
tconn := gotls.Server(conn, cfg)
|
||||
tconn.HandshakeContext(context.Background())
|
||||
tconn.Close()
|
||||
}
|
||||
}()
|
||||
t.Cleanup(func() { ln.Close() })
|
||||
return ln
|
||||
}
|
||||
|
||||
func sniForSettings(t *testing.T, serverName string) string {
|
||||
t.Helper()
|
||||
|
||||
var (
|
||||
sni string
|
||||
mu sync.Mutex
|
||||
)
|
||||
ln := recordingTLSListener(t, &sni, &mu)
|
||||
addr := ln.Addr().(*net.TCPAddr)
|
||||
|
||||
settings := &internet.MemoryStreamConfig{
|
||||
ProtocolName: protocolName,
|
||||
Destination: &xnet.Destination{
|
||||
Address: xnet.ParseAddress(addr.IP.String()),
|
||||
Port: xnet.Port(addr.Port),
|
||||
Network: xnet.Network_TCP,
|
||||
},
|
||||
SecuritySettings: &tls.Config{ServerName: serverName},
|
||||
}
|
||||
|
||||
prev := driveFilesURL
|
||||
driveFilesURL = "https://www.googleapis.com/drive/v3/files"
|
||||
defer func() { driveFilesURL = prev }()
|
||||
|
||||
client := newServiceClient(settings, 5*time.Second, 8)
|
||||
req, err := http.NewRequest(http.MethodGet, driveFilesURL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("request: %v", err)
|
||||
}
|
||||
client.Do(req)
|
||||
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
mu.Lock()
|
||||
got := sni
|
||||
mu.Unlock()
|
||||
if got != "" {
|
||||
return got
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestServiceSNIDefaultsToHost(t *testing.T) {
|
||||
if got := sniForSettings(t, ""); got != "www.googleapis.com" {
|
||||
t.Fatalf("SNI defaulted to %q, want the host www.googleapis.com, not address", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceSNIOverride(t *testing.T) {
|
||||
if got := sniForSettings(t, "www.google.com"); got != "www.google.com" {
|
||||
t.Fatalf("explicit serverName gave SNI %q, want www.google.com", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.5
|
||||
// source: transport/internet/xdrive/config.proto
|
||||
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
RemoteFolder string `protobuf:"bytes,1,opt,name=remote_folder,json=remoteFolder,proto3" json:"remote_folder,omitempty"`
|
||||
Service string `protobuf:"bytes,2,opt,name=service,proto3" json:"service,omitempty"`
|
||||
Secrets []string `protobuf:"bytes,3,rep,name=secrets,proto3" json:"secrets,omitempty"`
|
||||
SegmentBytes uint32 `protobuf:"varint,4,opt,name=segment_bytes,json=segmentBytes,proto3" json:"segment_bytes,omitempty"`
|
||||
FlushIntervalMs uint32 `protobuf:"varint,5,opt,name=flush_interval_ms,json=flushIntervalMs,proto3" json:"flush_interval_ms,omitempty"`
|
||||
PollIntervalMs uint32 `protobuf:"varint,6,opt,name=poll_interval_ms,json=pollIntervalMs,proto3" json:"poll_interval_ms,omitempty"`
|
||||
MaxPollIntervalMs uint32 `protobuf:"varint,7,opt,name=max_poll_interval_ms,json=maxPollIntervalMs,proto3" json:"max_poll_interval_ms,omitempty"`
|
||||
SessionTtlSeconds uint32 `protobuf:"varint,8,opt,name=session_ttl_seconds,json=sessionTtlSeconds,proto3" json:"session_ttl_seconds,omitempty"`
|
||||
Concurrency uint32 `protobuf:"varint,9,opt,name=concurrency,proto3" json:"concurrency,omitempty"`
|
||||
EagerWindowMs uint32 `protobuf:"varint,10,opt,name=eager_window_ms,json=eagerWindowMs,proto3" json:"eager_window_ms,omitempty"`
|
||||
HoleTimeoutMs uint32 `protobuf:"varint,11,opt,name=hole_timeout_ms,json=holeTimeoutMs,proto3" json:"hole_timeout_ms,omitempty"`
|
||||
Template string `protobuf:"bytes,12,opt,name=template,proto3" json:"template,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Config) Reset() {
|
||||
*x = Config{}
|
||||
mi := &file_transport_internet_xdrive_config_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Config) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Config) ProtoMessage() {}
|
||||
|
||||
func (x *Config) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_transport_internet_xdrive_config_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
|
||||
func (*Config) Descriptor() ([]byte, []int) {
|
||||
return file_transport_internet_xdrive_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Config) GetRemoteFolder() string {
|
||||
if x != nil {
|
||||
return x.RemoteFolder
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetService() string {
|
||||
if x != nil {
|
||||
return x.Service
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Config) GetSecrets() []string {
|
||||
if x != nil {
|
||||
return x.Secrets
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Config) GetSegmentBytes() uint32 {
|
||||
if x != nil {
|
||||
return x.SegmentBytes
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Config) GetFlushIntervalMs() uint32 {
|
||||
if x != nil {
|
||||
return x.FlushIntervalMs
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Config) GetPollIntervalMs() uint32 {
|
||||
if x != nil {
|
||||
return x.PollIntervalMs
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Config) GetMaxPollIntervalMs() uint32 {
|
||||
if x != nil {
|
||||
return x.MaxPollIntervalMs
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Config) GetSessionTtlSeconds() uint32 {
|
||||
if x != nil {
|
||||
return x.SessionTtlSeconds
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Config) GetConcurrency() uint32 {
|
||||
if x != nil {
|
||||
return x.Concurrency
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Config) GetEagerWindowMs() uint32 {
|
||||
if x != nil {
|
||||
return x.EagerWindowMs
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Config) GetHoleTimeoutMs() uint32 {
|
||||
if x != nil {
|
||||
return x.HoleTimeoutMs
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Config) GetTemplate() string {
|
||||
if x != nil {
|
||||
return x.Template
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_transport_internet_xdrive_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_transport_internet_xdrive_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"&transport/internet/xdrive/config.proto\x12\x1exray.transport.internet.xdrive\"\xcb\x03\n" +
|
||||
"\x06Config\x12#\n" +
|
||||
"\rremote_folder\x18\x01 \x01(\tR\fremoteFolder\x12\x18\n" +
|
||||
"\aservice\x18\x02 \x01(\tR\aservice\x12\x18\n" +
|
||||
"\asecrets\x18\x03 \x03(\tR\asecrets\x12#\n" +
|
||||
"\rsegment_bytes\x18\x04 \x01(\rR\fsegmentBytes\x12*\n" +
|
||||
"\x11flush_interval_ms\x18\x05 \x01(\rR\x0fflushIntervalMs\x12(\n" +
|
||||
"\x10poll_interval_ms\x18\x06 \x01(\rR\x0epollIntervalMs\x12/\n" +
|
||||
"\x14max_poll_interval_ms\x18\a \x01(\rR\x11maxPollIntervalMs\x12.\n" +
|
||||
"\x13session_ttl_seconds\x18\b \x01(\rR\x11sessionTtlSeconds\x12 \n" +
|
||||
"\vconcurrency\x18\t \x01(\rR\vconcurrency\x12&\n" +
|
||||
"\x0feager_window_ms\x18\n" +
|
||||
" \x01(\rR\reagerWindowMs\x12&\n" +
|
||||
"\x0fhole_timeout_ms\x18\v \x01(\rR\rholeTimeoutMs\x12\x1a\n" +
|
||||
"\btemplate\x18\f \x01(\tR\btemplateB5Z3github.com/xtls/xray-core/transport/internet/xdriveb\x06proto3"
|
||||
|
||||
var (
|
||||
file_transport_internet_xdrive_config_proto_rawDescOnce sync.Once
|
||||
file_transport_internet_xdrive_config_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_transport_internet_xdrive_config_proto_rawDescGZIP() []byte {
|
||||
file_transport_internet_xdrive_config_proto_rawDescOnce.Do(func() {
|
||||
file_transport_internet_xdrive_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_xdrive_config_proto_rawDesc), len(file_transport_internet_xdrive_config_proto_rawDesc)))
|
||||
})
|
||||
return file_transport_internet_xdrive_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_transport_internet_xdrive_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_transport_internet_xdrive_config_proto_goTypes = []any{
|
||||
(*Config)(nil), // 0: xray.transport.internet.xdrive.Config
|
||||
}
|
||||
var file_transport_internet_xdrive_config_proto_depIdxs = []int32{
|
||||
0, // [0:0] is the sub-list for method output_type
|
||||
0, // [0:0] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_transport_internet_xdrive_config_proto_init() }
|
||||
func file_transport_internet_xdrive_config_proto_init() {
|
||||
if File_transport_internet_xdrive_config_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_xdrive_config_proto_rawDesc), len(file_transport_internet_xdrive_config_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_transport_internet_xdrive_config_proto_goTypes,
|
||||
DependencyIndexes: file_transport_internet_xdrive_config_proto_depIdxs,
|
||||
MessageInfos: file_transport_internet_xdrive_config_proto_msgTypes,
|
||||
}.Build()
|
||||
File_transport_internet_xdrive_config_proto = out.File
|
||||
file_transport_internet_xdrive_config_proto_goTypes = nil
|
||||
file_transport_internet_xdrive_config_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package xray.transport.internet.xdrive;
|
||||
option go_package = "github.com/xtls/xray-core/transport/internet/xdrive";
|
||||
|
||||
message Config {
|
||||
string remote_folder = 1;
|
||||
string service = 2;
|
||||
repeated string secrets = 3;
|
||||
uint32 segment_bytes = 4;
|
||||
uint32 flush_interval_ms = 5;
|
||||
uint32 poll_interval_ms = 6;
|
||||
uint32 max_poll_interval_ms = 7;
|
||||
uint32 session_ttl_seconds = 8;
|
||||
uint32 concurrency = 9;
|
||||
uint32 eager_window_ms = 10;
|
||||
uint32 hole_timeout_ms = 11;
|
||||
string template = 12;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
)
|
||||
|
||||
var placeholderAddr = &net.TCPAddr{IP: net.IP{127, 0, 0, 1}, Port: 0}
|
||||
|
||||
type Conn struct {
|
||||
cancel context.CancelFunc
|
||||
writer *walWriter
|
||||
reader *walReader
|
||||
onClose func()
|
||||
|
||||
readBuf []byte
|
||||
|
||||
deadlineMu sync.Mutex
|
||||
readDeadline time.Time
|
||||
writeDeadline time.Time
|
||||
|
||||
closeOnce sync.Once
|
||||
closeErr error
|
||||
}
|
||||
|
||||
func newConn(ctx context.Context, storage Storage, writePrefix, readPrefix string, p params, onClose func()) *Conn {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &Conn{
|
||||
cancel: cancel,
|
||||
writer: newWALWriter(ctx, storage, writePrefix, p),
|
||||
reader: newWALReader(ctx, storage, readPrefix, p),
|
||||
onClose: onClose,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) Read(b []byte) (int, error) {
|
||||
if len(c.readBuf) == 0 {
|
||||
data, err := c.receive()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
c.readBuf = data
|
||||
}
|
||||
n := copy(b, c.readBuf)
|
||||
c.readBuf = c.readBuf[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *Conn) receive() ([]byte, error) {
|
||||
deadline := c.getDeadline(true)
|
||||
if deadline.IsZero() {
|
||||
data, ok := <-c.reader.ch
|
||||
if !ok {
|
||||
return nil, c.reader.Err()
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
if !time.Now().Before(deadline) {
|
||||
return nil, os.ErrDeadlineExceeded
|
||||
}
|
||||
timer := time.NewTimer(time.Until(deadline))
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case data, ok := <-c.reader.ch:
|
||||
if !ok {
|
||||
return nil, c.reader.Err()
|
||||
}
|
||||
return data, nil
|
||||
case <-timer.C:
|
||||
return nil, os.ErrDeadlineExceeded
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) Write(b []byte) (int, error) {
|
||||
if deadline := c.getDeadline(false); !deadline.IsZero() && !time.Now().Before(deadline) {
|
||||
return 0, os.ErrDeadlineExceeded
|
||||
}
|
||||
n, err := c.writer.Write(b)
|
||||
if err == nil {
|
||||
c.reader.Wake()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
c.closeOnce.Do(func() {
|
||||
c.closeErr = c.writer.Close()
|
||||
c.cancel()
|
||||
if c.onClose != nil {
|
||||
c.onClose()
|
||||
}
|
||||
})
|
||||
return c.closeErr
|
||||
}
|
||||
|
||||
func (c *Conn) LocalAddr() net.Addr {
|
||||
return placeholderAddr
|
||||
}
|
||||
|
||||
func (c *Conn) RemoteAddr() net.Addr {
|
||||
return placeholderAddr
|
||||
}
|
||||
|
||||
func (c *Conn) getDeadline(read bool) time.Time {
|
||||
c.deadlineMu.Lock()
|
||||
defer c.deadlineMu.Unlock()
|
||||
if read {
|
||||
return c.readDeadline
|
||||
}
|
||||
return c.writeDeadline
|
||||
}
|
||||
|
||||
func (c *Conn) SetDeadline(t time.Time) error {
|
||||
c.deadlineMu.Lock()
|
||||
defer c.deadlineMu.Unlock()
|
||||
c.readDeadline = t
|
||||
c.writeDeadline = t
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) SetReadDeadline(t time.Time) error {
|
||||
c.deadlineMu.Lock()
|
||||
defer c.deadlineMu.Unlock()
|
||||
c.readDeadline = t
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) SetWriteDeadline(t time.Time) error {
|
||||
c.deadlineMu.Lock()
|
||||
defer c.deadlineMu.Unlock()
|
||||
c.writeDeadline = t
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/dice"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
const (
|
||||
flatSeparator = "~"
|
||||
|
||||
driveBoundary = "xdrive-boundary"
|
||||
drivePageSize = 1000
|
||||
driveMaxAttempts = 8
|
||||
driveMaxInflight = 32
|
||||
driveInlineLimit = 12000
|
||||
driveTimeout = 60 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
driveMaxBackoff = 8 * time.Second
|
||||
driveTokenURL = "https://oauth2.googleapis.com/token"
|
||||
driveFilesURL = "https://www.googleapis.com/drive/v3/files"
|
||||
driveUploadURL = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name"
|
||||
driveInitialBackoff = 200 * time.Millisecond
|
||||
)
|
||||
|
||||
type driveStorage struct {
|
||||
folder string
|
||||
clientID string
|
||||
clientSecret string
|
||||
refreshToken string
|
||||
client *http.Client
|
||||
tokenURL string
|
||||
filesURL string
|
||||
uploadURL string
|
||||
backoff time.Duration
|
||||
|
||||
tokenMu sync.Mutex
|
||||
token string
|
||||
tokenExpiry time.Time
|
||||
|
||||
inflight chan struct{}
|
||||
|
||||
idMu sync.Mutex
|
||||
ids map[string]string
|
||||
}
|
||||
|
||||
func newDriveStorage(streamSettings *internet.MemoryStreamConfig, config *Config) (*driveStorage, error) {
|
||||
if config.RemoteFolder == "" {
|
||||
return nil, errors.New(`empty "remoteFolder", it must be a Google Drive folder id`)
|
||||
}
|
||||
if len(config.Secrets) != 3 {
|
||||
return nil, errors.New("Google Drive needs 3 secrets in order of ClientID, ClientSecret, RefreshToken")
|
||||
}
|
||||
for i, secret := range config.Secrets {
|
||||
if secret == "" {
|
||||
return nil, errors.New("Google Drive secret ", i, " is empty")
|
||||
}
|
||||
}
|
||||
|
||||
return &driveStorage{
|
||||
folder: config.RemoteFolder,
|
||||
clientID: config.Secrets[0],
|
||||
clientSecret: config.Secrets[1],
|
||||
refreshToken: config.Secrets[2],
|
||||
client: newServiceClient(streamSettings, driveTimeout, driveMaxInflight),
|
||||
tokenURL: driveTokenURL,
|
||||
filesURL: driveFilesURL,
|
||||
uploadURL: driveUploadURL,
|
||||
backoff: driveInitialBackoff,
|
||||
inflight: make(chan struct{}, driveMaxInflight),
|
||||
ids: make(map[string]string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func flatten(name string) string {
|
||||
return strings.ReplaceAll(name, "/", flatSeparator)
|
||||
}
|
||||
|
||||
func quoteDriveValue(value string) string {
|
||||
return strings.NewReplacer(`\`, `\\`, `'`, `\'`).Replace(value)
|
||||
}
|
||||
|
||||
func (s *driveStorage) accessToken(ctx context.Context) (string, error) {
|
||||
s.tokenMu.Lock()
|
||||
defer s.tokenMu.Unlock()
|
||||
|
||||
if s.token != "" && time.Now().Before(s.tokenExpiry) {
|
||||
return s.token, nil
|
||||
}
|
||||
|
||||
form := url.Values{
|
||||
"client_id": {s.clientID},
|
||||
"client_secret": {s.clientSecret},
|
||||
"refresh_token": {s.refreshToken},
|
||||
"grant_type": {"refresh_token"},
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.tokenURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return "", errors.New("failed to build the token request").Base(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return "", errors.New("failed to refresh the access token").Base(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return "", errors.New("failed to read the token response").Base(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", errors.New("the token endpoint answered ", resp.StatusCode, ": ", string(body))
|
||||
}
|
||||
|
||||
var parsed struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||
return "", errors.New("failed to parse the token response").Base(err)
|
||||
}
|
||||
if parsed.AccessToken == "" {
|
||||
return "", errors.New("the token endpoint returned no access token")
|
||||
}
|
||||
|
||||
lifetime := parsed.ExpiresIn
|
||||
if lifetime > 60 {
|
||||
lifetime -= 60
|
||||
}
|
||||
s.token = parsed.AccessToken
|
||||
s.tokenExpiry = time.Now().Add(time.Duration(lifetime) * time.Second)
|
||||
return s.token, nil
|
||||
}
|
||||
|
||||
func jitter(backoff time.Duration) time.Duration {
|
||||
half := backoff / 2
|
||||
if half <= 0 {
|
||||
return backoff
|
||||
}
|
||||
return half + time.Duration(dice.Roll(int(half)))
|
||||
}
|
||||
|
||||
func rateLimited(payload []byte) bool {
|
||||
var parsed struct {
|
||||
Error struct {
|
||||
Status string `json:"status"`
|
||||
Errors []struct {
|
||||
Reason string `json:"reason"`
|
||||
} `json:"errors"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if json.Unmarshal(payload, &parsed) != nil {
|
||||
return false
|
||||
}
|
||||
for _, item := range parsed.Error.Errors {
|
||||
switch item.Reason {
|
||||
case "rateLimitExceeded", "userRateLimitExceeded", "sharingRateLimitExceeded":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return parsed.Error.Status == "RESOURCE_EXHAUSTED"
|
||||
}
|
||||
|
||||
func retryableStatus(status int) bool {
|
||||
switch status {
|
||||
case http.StatusTooManyRequests, http.StatusInternalServerError,
|
||||
http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *driveStorage) do(ctx context.Context, method, target, contentType string, body []byte) (int, []byte, error) {
|
||||
backoff := s.backoff
|
||||
var lastErr error
|
||||
|
||||
for attempt := 0; attempt < driveMaxAttempts; attempt++ {
|
||||
if attempt > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return 0, nil, ctx.Err()
|
||||
case <-time.After(jitter(backoff)):
|
||||
}
|
||||
backoff *= 2
|
||||
if backoff > driveMaxBackoff {
|
||||
backoff = driveMaxBackoff
|
||||
}
|
||||
}
|
||||
|
||||
token, err := s.accessToken(ctx)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case s.inflight <- struct{}{}:
|
||||
case <-ctx.Done():
|
||||
return 0, nil, ctx.Err()
|
||||
}
|
||||
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
reader = bytes.NewReader(body)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, target, reader)
|
||||
if err != nil {
|
||||
return 0, nil, errors.New("failed to build a Drive request").Base(err)
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
<-s.inflight
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return 0, nil, ctx.Err()
|
||||
}
|
||||
lastErr = errors.New("Drive request failed").Base(err)
|
||||
errors.LogWarningInner(ctx, err, "retrying a failed Drive request, attempt ",
|
||||
attempt+1, " of ", driveMaxAttempts)
|
||||
continue
|
||||
}
|
||||
payload, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return 0, nil, ctx.Err()
|
||||
}
|
||||
lastErr = errors.New("failed to read the Drive response").Base(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
s.invalidateToken()
|
||||
lastErr = errors.New("Drive rejected the access token")
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode == http.StatusForbidden && rateLimited(payload) {
|
||||
lastErr = errors.New("Drive is rate limiting: ", string(payload))
|
||||
errors.LogWarning(ctx, "rate limited by Drive, attempt ",
|
||||
attempt+1, " of ", driveMaxAttempts)
|
||||
continue
|
||||
}
|
||||
if retryableStatus(resp.StatusCode) {
|
||||
lastErr = errors.New("Drive answered ", resp.StatusCode, ": ", string(payload))
|
||||
errors.LogWarning(ctx, "retrying after Drive answered ", resp.StatusCode,
|
||||
", attempt ", attempt+1, " of ", driveMaxAttempts)
|
||||
continue
|
||||
}
|
||||
return resp.StatusCode, payload, nil
|
||||
}
|
||||
|
||||
return 0, nil, lastErr
|
||||
}
|
||||
|
||||
func (s *driveStorage) invalidateToken() {
|
||||
s.tokenMu.Lock()
|
||||
s.token = ""
|
||||
s.tokenMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *driveStorage) rememberID(name, id string) {
|
||||
s.idMu.Lock()
|
||||
s.ids[name] = id
|
||||
s.idMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *driveStorage) forgetID(name string) {
|
||||
s.idMu.Lock()
|
||||
delete(s.ids, name)
|
||||
s.idMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *driveStorage) cachedID(name string) (string, bool) {
|
||||
s.idMu.Lock()
|
||||
defer s.idMu.Unlock()
|
||||
id, ok := s.ids[name]
|
||||
return id, ok
|
||||
}
|
||||
|
||||
type driveFile struct {
|
||||
id string
|
||||
description string
|
||||
}
|
||||
|
||||
func (s *driveStorage) query(ctx context.Context, condition string) (map[string]driveFile, error) {
|
||||
found := make(map[string]driveFile)
|
||||
pageToken := ""
|
||||
|
||||
for {
|
||||
params := url.Values{
|
||||
"q": {"'" + quoteDriveValue(s.folder) + "' in parents and trashed = false and " + condition},
|
||||
"fields": {"nextPageToken,files(id,name,description)"},
|
||||
"pageSize": {fmt.Sprint(drivePageSize)},
|
||||
"supportsAllDrives": {"true"},
|
||||
"includeItemsFromAllDrives": {"true"},
|
||||
}
|
||||
if pageToken != "" {
|
||||
params.Set("pageToken", pageToken)
|
||||
}
|
||||
|
||||
status, payload, err := s.do(ctx, http.MethodGet, s.filesURL+"?"+params.Encode(), "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
return nil, errors.New("Drive listing answered ", status, ": ", string(payload))
|
||||
}
|
||||
|
||||
var parsed struct {
|
||||
NextPageToken string `json:"nextPageToken"`
|
||||
Files []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
} `json:"files"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &parsed); err != nil {
|
||||
return nil, errors.New("failed to parse the Drive listing").Base(err)
|
||||
}
|
||||
|
||||
for _, file := range parsed.Files {
|
||||
found[file.Name] = driveFile{id: file.ID, description: file.Description}
|
||||
s.rememberID(file.Name, file.ID)
|
||||
}
|
||||
|
||||
pageToken = parsed.NextPageToken
|
||||
if pageToken == "" {
|
||||
return found, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *driveStorage) resolveID(ctx context.Context, flat string) (string, error) {
|
||||
if id, ok := s.cachedID(flat); ok {
|
||||
return id, nil
|
||||
}
|
||||
found, err := s.query(ctx, "name = '"+quoteDriveValue(flat)+"'")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if file, ok := found[flat]; ok {
|
||||
return file.id, nil
|
||||
}
|
||||
return "", errNotFound
|
||||
}
|
||||
|
||||
func (s *driveStorage) Put(ctx context.Context, name string, data []byte) error {
|
||||
if len(data) <= driveInlineLimit {
|
||||
return s.putInline(ctx, name, data)
|
||||
}
|
||||
return s.putMedia(ctx, name, data)
|
||||
}
|
||||
|
||||
func (s *driveStorage) putInline(ctx context.Context, name string, data []byte) error {
|
||||
flat := flatten(name)
|
||||
|
||||
body, err := json.Marshal(map[string]interface{}{
|
||||
"name": flat,
|
||||
"parents": []string{s.folder},
|
||||
"description": base64.StdEncoding.EncodeToString(data),
|
||||
})
|
||||
if err != nil {
|
||||
return errors.New("failed to build the inline metadata").Base(err)
|
||||
}
|
||||
|
||||
status, payload, err := s.do(ctx, http.MethodPost, s.filesURL+"?fields=id",
|
||||
"application/json; charset=UTF-8", body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
return errors.New("Drive rejected the inline upload of ", name,
|
||||
" with ", status, ": ", string(payload))
|
||||
}
|
||||
|
||||
var parsed struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &parsed); err != nil {
|
||||
return errors.New("failed to parse the Drive upload response").Base(err)
|
||||
}
|
||||
if parsed.ID != "" {
|
||||
s.rememberID(flat, parsed.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *driveStorage) putMedia(ctx context.Context, name string, data []byte) error {
|
||||
flat := flatten(name)
|
||||
|
||||
metadata, err := json.Marshal(map[string]interface{}{
|
||||
"name": flat,
|
||||
"parents": []string{s.folder},
|
||||
})
|
||||
if err != nil {
|
||||
return errors.New("failed to build the upload metadata").Base(err)
|
||||
}
|
||||
|
||||
var body bytes.Buffer
|
||||
fmt.Fprintf(&body, "--%s\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n", driveBoundary)
|
||||
body.Write(metadata)
|
||||
fmt.Fprintf(&body, "\r\n--%s\r\nContent-Type: application/octet-stream\r\n\r\n", driveBoundary)
|
||||
body.Write(data)
|
||||
fmt.Fprintf(&body, "\r\n--%s--\r\n", driveBoundary)
|
||||
|
||||
status, payload, err := s.do(ctx, http.MethodPost, s.uploadURL,
|
||||
"multipart/related; boundary="+driveBoundary, body.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
return errors.New("Drive upload of ", name, " answered ", status, ": ", string(payload))
|
||||
}
|
||||
|
||||
var parsed struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &parsed); err != nil {
|
||||
return errors.New("failed to parse the Drive upload response").Base(err)
|
||||
}
|
||||
if parsed.ID != "" {
|
||||
s.rememberID(flat, parsed.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *driveStorage) Get(ctx context.Context, name string) ([]byte, error) {
|
||||
flat := flatten(name)
|
||||
id, err := s.resolveID(ctx, flat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
status, payload, err := s.do(ctx, http.MethodGet,
|
||||
s.filesURL+"/"+url.PathEscape(id)+"?alt=media&supportsAllDrives=true", "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch status {
|
||||
case http.StatusOK:
|
||||
if len(payload) > 0 {
|
||||
return payload, nil
|
||||
}
|
||||
return s.getInline(ctx, id)
|
||||
case http.StatusNotFound:
|
||||
s.forgetID(flat)
|
||||
return nil, errNotFound
|
||||
default:
|
||||
return nil, errors.New("Drive download of ", name, " answered ", status, ": ", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *driveStorage) getInline(ctx context.Context, id string) ([]byte, error) {
|
||||
status, payload, err := s.do(ctx, http.MethodGet,
|
||||
s.filesURL+"/"+url.PathEscape(id)+"?fields=description&supportsAllDrives=true", "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
return nil, errors.New("Drive answered ", status, " for inline data: ", string(payload))
|
||||
}
|
||||
|
||||
var parsed struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &parsed); err != nil {
|
||||
return nil, errors.New("failed to parse the inline data").Base(err)
|
||||
}
|
||||
if parsed.Description == "" {
|
||||
return nil, nil
|
||||
}
|
||||
data, err := base64.StdEncoding.DecodeString(parsed.Description)
|
||||
if err != nil {
|
||||
return nil, errors.New("the inline data is not valid base64").Base(err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *driveStorage) deleteID(ctx context.Context, flat, id string) error {
|
||||
status, payload, err := s.do(ctx, http.MethodDelete,
|
||||
s.filesURL+"/"+url.PathEscape(id)+"?supportsAllDrives=true", "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.forgetID(flat)
|
||||
switch status {
|
||||
case http.StatusOK, http.StatusNoContent, http.StatusNotFound:
|
||||
return nil
|
||||
default:
|
||||
return errors.New("Drive deletion of ", flat, " answered ", status, ": ", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *driveStorage) Delete(ctx context.Context, name string) error {
|
||||
flat := flatten(name)
|
||||
|
||||
if id, err := s.resolveID(ctx, flat); err == nil {
|
||||
if err := s.deleteID(ctx, flat, id); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != errNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
children, err := s.query(ctx, "name contains '"+quoteDriveValue(flat+flatSeparator)+"'")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for childName, file := range children {
|
||||
if err := s.deleteID(ctx, childName, file.id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *driveStorage) List(ctx context.Context, prefix string) ([]Entry, error) {
|
||||
flat := flatten(prefix) + flatSeparator
|
||||
|
||||
found, err := s.query(ctx, "name contains '"+quoteDriveValue(flat)+"'")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
seen := make(map[string]bool, len(found))
|
||||
entries := make([]Entry, 0, len(found))
|
||||
for name, file := range found {
|
||||
rest := strings.TrimPrefix(name, flat)
|
||||
if rest == "" {
|
||||
continue
|
||||
}
|
||||
direct := true
|
||||
if cut := strings.Index(rest, flatSeparator); cut >= 0 {
|
||||
rest = rest[:cut]
|
||||
direct = false
|
||||
}
|
||||
if seen[rest] {
|
||||
continue
|
||||
}
|
||||
seen[rest] = true
|
||||
|
||||
entry := Entry{Name: rest}
|
||||
if direct && file.description != "" {
|
||||
if data, err := base64.StdEncoding.DecodeString(file.description); err == nil {
|
||||
entry.Inline = data
|
||||
}
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (s *driveStorage) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
const liveSecretsEnv = "XRAY_XDRIVE_DRIVE_SECRETS"
|
||||
|
||||
func liveDriveConfig(t *testing.T) *Config {
|
||||
t.Helper()
|
||||
|
||||
path := os.Getenv(liveSecretsEnv)
|
||||
if path == "" {
|
||||
t.Skipf("set %s to run this test", liveSecretsEnv)
|
||||
}
|
||||
|
||||
payload, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("reading %s: %v", path, err)
|
||||
}
|
||||
var secrets struct {
|
||||
Folder string `json:"folder"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &secrets); err != nil {
|
||||
t.Fatalf("parsing %s: %v", path, err)
|
||||
}
|
||||
|
||||
config := &Config{
|
||||
RemoteFolder: secrets.Folder,
|
||||
Service: "Google Drive",
|
||||
Secrets: []string{secrets.ClientID, secrets.ClientSecret, secrets.RefreshToken},
|
||||
SegmentBytes: 256 * 1024,
|
||||
FlushIntervalMs: 100,
|
||||
PollIntervalMs: 500,
|
||||
MaxPollIntervalMs: 2000,
|
||||
SessionTtlSeconds: 120,
|
||||
}
|
||||
if raw := os.Getenv("XRAY_XDRIVE_LIVE_SEGMENT"); raw != "" {
|
||||
config.SegmentBytes = uint32(envInt(t, "XRAY_XDRIVE_LIVE_SEGMENT"))
|
||||
}
|
||||
if raw := os.Getenv("XRAY_XDRIVE_LIVE_CONCURRENCY"); raw != "" {
|
||||
config.Concurrency = uint32(envInt(t, "XRAY_XDRIVE_LIVE_CONCURRENCY"))
|
||||
}
|
||||
|
||||
storage, err := newDriveStorage(nil, config)
|
||||
if err != nil {
|
||||
t.Fatalf("newDriveStorage: %v", err)
|
||||
}
|
||||
defer storage.Close()
|
||||
for _, dir := range []string{sessionsDir, streamsDir} {
|
||||
if err := storage.Delete(context.Background(), dir); err != nil {
|
||||
t.Fatalf("clearing %s: %v", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func envInt(t *testing.T, name string) int {
|
||||
t.Helper()
|
||||
|
||||
parsed, err := strconv.Atoi(os.Getenv(name))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func TestLiveDriveStorage(t *testing.T) {
|
||||
config := liveDriveConfig(t)
|
||||
storage, err := newDriveStorage(nil, config)
|
||||
if err != nil {
|
||||
t.Fatalf("newDriveStorage: %v", err)
|
||||
}
|
||||
defer storage.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
name := "streams/livetest/c2s/000000000.seg"
|
||||
payload := []byte("xdrive over a real remote storage service")
|
||||
defer storage.Delete(ctx, "streams/livetest")
|
||||
|
||||
start := time.Now()
|
||||
if err := storage.Put(ctx, name, payload); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
t.Logf("Put took %v", time.Since(start))
|
||||
|
||||
start = time.Now()
|
||||
names, err := storage.List(ctx, "streams/livetest/c2s")
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
t.Logf("List took %v", time.Since(start))
|
||||
if len(names) != 1 || names[0].Name != "000000000.seg" {
|
||||
t.Fatalf("List returned %v, want one segment", names)
|
||||
}
|
||||
|
||||
start = time.Now()
|
||||
got, err := storage.Get(ctx, name)
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
t.Logf("Get took %v", time.Since(start))
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatalf("Get returned %q, want %q", got, payload)
|
||||
}
|
||||
|
||||
if _, err := storage.Get(ctx, "streams/livetest/c2s/000000009.seg"); err != errNotFound {
|
||||
t.Fatalf("Get returned %v, want errNotFound", err)
|
||||
}
|
||||
|
||||
if err := storage.Delete(ctx, "streams/livetest"); err != nil {
|
||||
t.Fatalf("Delete: %v", err)
|
||||
}
|
||||
names, err = storage.List(ctx, "streams/livetest/c2s")
|
||||
if err != nil {
|
||||
t.Fatalf("List after delete: %v", err)
|
||||
}
|
||||
if len(names) != 0 {
|
||||
t.Fatalf("List after delete returned %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveDriveTransport(t *testing.T) {
|
||||
config := liveDriveConfig(t)
|
||||
streamSettings := &internet.MemoryStreamConfig{
|
||||
ProtocolName: protocolName,
|
||||
ProtocolSettings: config,
|
||||
}
|
||||
|
||||
client, server, cleanup := pairWith(t, streamSettings)
|
||||
defer cleanup()
|
||||
|
||||
start := time.Now()
|
||||
if _, err := client.Write([]byte("ping")); err != nil {
|
||||
t.Fatalf("client write: %v", err)
|
||||
}
|
||||
expectRead(t, server, "ping")
|
||||
t.Logf("client to server round took %v", time.Since(start))
|
||||
|
||||
start = time.Now()
|
||||
if _, err := server.Write([]byte("pong")); err != nil {
|
||||
t.Fatalf("server write: %v", err)
|
||||
}
|
||||
expectRead(t, client, "pong")
|
||||
t.Logf("server to client round took %v", time.Since(start))
|
||||
|
||||
size := 400000
|
||||
if raw := os.Getenv("XRAY_XDRIVE_LIVE_BYTES"); raw != "" {
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("XRAY_XDRIVE_LIVE_BYTES: %v", err)
|
||||
}
|
||||
size = parsed
|
||||
}
|
||||
payload := make([]byte, size)
|
||||
if _, err := rand.Read(payload); err != nil {
|
||||
t.Fatalf("rand: %v", err)
|
||||
}
|
||||
|
||||
start = time.Now()
|
||||
go func() {
|
||||
client.Write(payload)
|
||||
}()
|
||||
if err := server.SetReadDeadline(time.Now().Add(5 * time.Minute)); err != nil {
|
||||
t.Fatalf("SetReadDeadline: %v", err)
|
||||
}
|
||||
got := make([]byte, len(payload))
|
||||
if _, err := io.ReadFull(server, got); err != nil {
|
||||
t.Fatalf("ReadFull: %v", err)
|
||||
}
|
||||
elapsed := time.Since(start)
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatal("payload mismatch")
|
||||
}
|
||||
t.Logf("%d bytes took %v (%.1f KiB/s)", len(payload), elapsed,
|
||||
float64(len(payload))/1024/elapsed.Seconds())
|
||||
}
|
||||
|
||||
func TestLiveDriveParallelPut(t *testing.T) {
|
||||
config := liveDriveConfig(t)
|
||||
storage, err := newDriveStorage(nil, config)
|
||||
if err != nil {
|
||||
t.Fatalf("newDriveStorage: %v", err)
|
||||
}
|
||||
defer storage.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
defer storage.Delete(ctx, "streams/benchtest")
|
||||
|
||||
chunk := make([]byte, 256*1024)
|
||||
if _, err := rand.Read(chunk); err != nil {
|
||||
t.Fatalf("rand: %v", err)
|
||||
}
|
||||
|
||||
if err := storage.Put(ctx, "streams/benchtest/warmup", chunk); err != nil {
|
||||
t.Fatalf("warmup: %v", err)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
for i := 0; i < 4; i++ {
|
||||
if err := storage.Put(ctx, fmt.Sprintf("streams/benchtest/seq%d", i), chunk); err != nil {
|
||||
t.Fatalf("sequential put: %v", err)
|
||||
}
|
||||
}
|
||||
sequential := time.Since(start)
|
||||
t.Logf("4 sequential puts of 256 KiB: %v (%.1f KiB/s)",
|
||||
sequential, float64(4*len(chunk))/1024/sequential.Seconds())
|
||||
|
||||
start = time.Now()
|
||||
var wg sync.WaitGroup
|
||||
failures := make([]error, 8)
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
failures[i] = storage.Put(ctx, fmt.Sprintf("streams/benchtest/par%d", i), chunk)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
parallel := time.Since(start)
|
||||
for _, err := range failures {
|
||||
if err != nil {
|
||||
t.Fatalf("parallel put: %v", err)
|
||||
}
|
||||
}
|
||||
t.Logf("8 parallel puts of 256 KiB: %v (%.1f KiB/s)",
|
||||
parallel, float64(8*len(chunk))/1024/parallel.Seconds())
|
||||
|
||||
start = time.Now()
|
||||
names, err := storage.List(ctx, "streams/benchtest")
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
t.Logf("List of %d objects took %v", len(names), time.Since(start))
|
||||
|
||||
start = time.Now()
|
||||
wg = sync.WaitGroup{}
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
storage.Get(ctx, fmt.Sprintf("streams/benchtest/par%d", i))
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
download := time.Since(start)
|
||||
t.Logf("8 parallel gets of 256 KiB: %v (%.1f KiB/s)",
|
||||
download, float64(8*len(chunk))/1024/download.Seconds())
|
||||
}
|
||||
|
||||
func TestLiveDriveSegmentSweep(t *testing.T) {
|
||||
config := liveDriveConfig(t)
|
||||
storage, err := newDriveStorage(nil, config)
|
||||
if err != nil {
|
||||
t.Fatalf("newDriveStorage: %v", err)
|
||||
}
|
||||
defer storage.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
defer storage.Delete(ctx, "streams/sweeptest")
|
||||
|
||||
const total = 1024 * 1024
|
||||
for _, size := range []int{64 * 1024, 128 * 1024, 256 * 1024, 512 * 1024} {
|
||||
chunk := make([]byte, size)
|
||||
if _, err := rand.Read(chunk); err != nil {
|
||||
t.Fatalf("rand: %v", err)
|
||||
}
|
||||
count := total / size
|
||||
|
||||
start := time.Now()
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < count; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
storage.Put(ctx, fmt.Sprintf("streams/sweeptest/s%d-%d", size, i), chunk)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
t.Logf("%4d KiB x %2d = 1 MiB in %8v -> %6.1f KiB/s",
|
||||
size/1024, count, elapsed.Round(time.Millisecond),
|
||||
float64(total)/1024/elapsed.Seconds())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveDriveListLag(t *testing.T) {
|
||||
config := liveDriveConfig(t)
|
||||
storage, err := newDriveStorage(nil, config)
|
||||
if err != nil {
|
||||
t.Fatalf("newDriveStorage: %v", err)
|
||||
}
|
||||
defer storage.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
defer storage.Delete(ctx, "streams/lagtest")
|
||||
|
||||
const rounds = 6
|
||||
var worst time.Duration
|
||||
|
||||
for i := 0; i < rounds; i++ {
|
||||
name := fmt.Sprintf("streams/lagtest/round%d/000000000.seg", i)
|
||||
if err := storage.Put(ctx, name, []byte("probe")); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
var lag time.Duration
|
||||
for {
|
||||
names, err := storage.List(ctx, fmt.Sprintf("streams/lagtest/round%d", i))
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(names) == 1 {
|
||||
lag = time.Since(start)
|
||||
break
|
||||
}
|
||||
if time.Since(start) > 30*time.Second {
|
||||
t.Fatalf("round %d: the object never showed up in a listing", i)
|
||||
}
|
||||
}
|
||||
if lag > worst {
|
||||
worst = lag
|
||||
}
|
||||
t.Logf("round %d: the object became listable after %v", i, lag.Round(time.Millisecond))
|
||||
}
|
||||
t.Logf("worst listing lag: %v", worst.Round(time.Millisecond))
|
||||
}
|
||||
@@ -0,0 +1,690 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
type fakeFile struct {
|
||||
id string
|
||||
name string
|
||||
data []byte
|
||||
description string
|
||||
}
|
||||
|
||||
type fakeDrive struct {
|
||||
server *httptest.Server
|
||||
|
||||
mu sync.Mutex
|
||||
files map[string]*fakeFile
|
||||
nextID int
|
||||
failOnce map[string]bool
|
||||
failStatus map[string]int
|
||||
failBody map[string]string
|
||||
tokens int
|
||||
hosts map[string]bool
|
||||
}
|
||||
|
||||
func newFakeDrive(t *testing.T) *fakeDrive {
|
||||
t.Helper()
|
||||
|
||||
drive := &fakeDrive{
|
||||
files: make(map[string]*fakeFile),
|
||||
failOnce: make(map[string]bool),
|
||||
failStatus: make(map[string]int),
|
||||
failBody: make(map[string]string),
|
||||
hosts: make(map[string]bool),
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
record := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
drive.mu.Lock()
|
||||
drive.hosts[r.Host] = true
|
||||
drive.mu.Unlock()
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
mux.HandleFunc("/token", record(drive.handleToken))
|
||||
mux.HandleFunc("/upload", record(drive.handleUpload))
|
||||
mux.HandleFunc("/files", record(drive.handleFiles))
|
||||
mux.HandleFunc("/files/", record(drive.handleFile))
|
||||
drive.server = httptest.NewServer(mux)
|
||||
|
||||
resetSharedStorage()
|
||||
|
||||
previous := []string{driveTokenURL, driveFilesURL, driveUploadURL}
|
||||
previousBackoff := driveInitialBackoff
|
||||
driveTokenURL = drive.server.URL + "/token"
|
||||
driveFilesURL = drive.server.URL + "/files"
|
||||
driveUploadURL = drive.server.URL + "/upload"
|
||||
driveInitialBackoff = 5 * time.Millisecond
|
||||
|
||||
t.Cleanup(func() {
|
||||
driveTokenURL, driveFilesURL, driveUploadURL = previous[0], previous[1], previous[2]
|
||||
driveInitialBackoff = previousBackoff
|
||||
drive.server.Close()
|
||||
resetSharedStorage()
|
||||
})
|
||||
return drive
|
||||
}
|
||||
|
||||
func (d *fakeDrive) handleToken(w http.ResponseWriter, r *http.Request) {
|
||||
d.mu.Lock()
|
||||
d.tokens++
|
||||
d.mu.Unlock()
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"access_token": "fake-token",
|
||||
"expires_in": 3600,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *fakeDrive) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
if d.shouldFail("upload") {
|
||||
if status, body := d.failure("upload"); status != 0 {
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
_, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
reader := multipart.NewReader(r.Body, params["boundary"])
|
||||
|
||||
metaPart, err := reader.NextPart()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var metadata struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := json.NewDecoder(metaPart).Decode(&metadata); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if dataPart, err := reader.NextPart(); err == nil {
|
||||
data, _ = io.ReadAll(dataPart)
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
d.nextID++
|
||||
id := fmt.Sprintf("id-%d", d.nextID)
|
||||
d.files[id] = &fakeFile{id: id, name: metadata.Name, data: data}
|
||||
d.mu.Unlock()
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{"id": id, "name": metadata.Name})
|
||||
}
|
||||
|
||||
func (d *fakeDrive) handleFiles(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
d.handleCreate(w, r)
|
||||
return
|
||||
}
|
||||
d.handleList(w, r)
|
||||
}
|
||||
|
||||
func (d *fakeDrive) handleCreate(w http.ResponseWriter, r *http.Request) {
|
||||
if d.shouldFail("upload") {
|
||||
if status, body := d.failure("upload"); status != 0 {
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
var meta struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&meta); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
d.nextID++
|
||||
id := fmt.Sprintf("id-%d", d.nextID)
|
||||
d.files[id] = &fakeFile{id: id, name: meta.Name, description: meta.Description}
|
||||
d.mu.Unlock()
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{"id": id, "name": meta.Name})
|
||||
}
|
||||
|
||||
func (d *fakeDrive) handleList(w http.ResponseWriter, r *http.Request) {
|
||||
if d.shouldFail("list") {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
|
||||
query := r.URL.Query().Get("q")
|
||||
exact, prefix := parseFakeQuery(query)
|
||||
|
||||
type entry struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
result := struct {
|
||||
Files []entry `json:"files"`
|
||||
}{}
|
||||
|
||||
d.mu.Lock()
|
||||
for _, file := range d.files {
|
||||
match := false
|
||||
switch {
|
||||
case exact != "":
|
||||
match = file.name == exact
|
||||
case prefix != "":
|
||||
match = strings.HasPrefix(file.name, prefix)
|
||||
}
|
||||
if match {
|
||||
result.Files = append(result.Files, entry{
|
||||
ID: file.id, Name: file.name, Description: file.description,
|
||||
})
|
||||
}
|
||||
}
|
||||
d.mu.Unlock()
|
||||
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
func (d *fakeDrive) handleFile(w http.ResponseWriter, r *http.Request) {
|
||||
id := strings.TrimPrefix(r.URL.Path, "/files/")
|
||||
|
||||
d.mu.Lock()
|
||||
file, ok := d.files[id]
|
||||
if ok && r.Method == http.MethodDelete {
|
||||
delete(d.files, id)
|
||||
}
|
||||
d.mu.Unlock()
|
||||
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodDelete {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
if strings.Contains(r.URL.RawQuery, "fields=description") {
|
||||
json.NewEncoder(w).Encode(map[string]string{"description": file.description})
|
||||
return
|
||||
}
|
||||
w.Write(file.data)
|
||||
}
|
||||
|
||||
func (d *fakeDrive) shouldFail(kind string) bool {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if d.failOnce[kind] {
|
||||
d.failOnce[kind] = false
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *fakeDrive) failOnceWith(kind string, status int, body string) {
|
||||
d.mu.Lock()
|
||||
d.failOnce[kind] = true
|
||||
d.failStatus[kind] = status
|
||||
d.failBody[kind] = body
|
||||
d.mu.Unlock()
|
||||
}
|
||||
|
||||
func (d *fakeDrive) failure(kind string) (int, string) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if status, ok := d.failStatus[kind]; ok {
|
||||
return status, d.failBody[kind]
|
||||
}
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
func (d *fakeDrive) failNext(kind string) {
|
||||
d.mu.Lock()
|
||||
d.failOnce[kind] = true
|
||||
d.mu.Unlock()
|
||||
}
|
||||
|
||||
func (d *fakeDrive) seenHosts() []string {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
out := make([]string, 0, len(d.hosts))
|
||||
for h := range d.hosts {
|
||||
out = append(out, h)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (d *fakeDrive) count() int {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
return len(d.files)
|
||||
}
|
||||
|
||||
func parseFakeQuery(query string) (exact, prefix string) {
|
||||
if value, ok := cutQuoted(query, "name = '"); ok {
|
||||
return value, ""
|
||||
}
|
||||
if value, ok := cutQuoted(query, "name contains '"); ok {
|
||||
return "", value
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func cutQuoted(query, marker string) (string, bool) {
|
||||
start := strings.Index(query, marker)
|
||||
if start < 0 {
|
||||
return "", false
|
||||
}
|
||||
rest := query[start+len(marker):]
|
||||
end := strings.Index(rest, "'")
|
||||
if end < 0 {
|
||||
return "", false
|
||||
}
|
||||
return rest[:end], true
|
||||
}
|
||||
|
||||
func driveSettings() *internet.MemoryStreamConfig {
|
||||
return &internet.MemoryStreamConfig{
|
||||
ProtocolName: protocolName,
|
||||
ProtocolSettings: &Config{
|
||||
RemoteFolder: "folder-id",
|
||||
Service: "Google Drive",
|
||||
Secrets: []string{"client", "secret", "refresh"},
|
||||
FlushIntervalMs: 5,
|
||||
PollIntervalMs: 5,
|
||||
MaxPollIntervalMs: 20,
|
||||
SessionTtlSeconds: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newDriveBackend(t *testing.T) *driveStorage {
|
||||
t.Helper()
|
||||
|
||||
storage, err := newDriveStorage(driveSettings(), driveSettings().ProtocolSettings.(*Config))
|
||||
if err != nil {
|
||||
t.Fatalf("newDriveStorage: %v", err)
|
||||
}
|
||||
return storage
|
||||
}
|
||||
|
||||
func TestDriveSecrets(t *testing.T) {
|
||||
if _, err := newDriveStorage(nil, &Config{RemoteFolder: "f", Secrets: []string{"a", "b"}}); err == nil {
|
||||
t.Fatal("accepted two secrets")
|
||||
}
|
||||
if _, err := newDriveStorage(nil, &Config{Secrets: []string{"a", "b", "c"}}); err == nil {
|
||||
t.Fatal("accepted an empty remoteFolder")
|
||||
}
|
||||
if _, err := newDriveStorage(nil, &Config{RemoteFolder: "f", Secrets: []string{"a", "", "c"}}); err == nil {
|
||||
t.Fatal("accepted an empty secret")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveRoundTrip(t *testing.T) {
|
||||
drive := newFakeDrive(t)
|
||||
storage := newDriveBackend(t)
|
||||
ctx := context.Background()
|
||||
|
||||
if err := storage.Put(ctx, "streams/abc/c2s/000000000.seg", []byte("hello")); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
|
||||
data, err := storage.Get(ctx, "streams/abc/c2s/000000000.seg")
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
if string(data) != "hello" {
|
||||
t.Fatalf("Get returned %q, want %q", data, "hello")
|
||||
}
|
||||
|
||||
names, err := storage.List(ctx, "streams/abc/c2s")
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(names) != 1 || names[0].Name != "000000000.seg" {
|
||||
t.Fatalf("List returned %v, want 1 segment", names)
|
||||
}
|
||||
|
||||
if _, err := storage.Get(ctx, "streams/abc/c2s/000000009.seg"); err != errNotFound {
|
||||
t.Fatalf("Get returned %v, want errNotFound", err)
|
||||
}
|
||||
|
||||
if err := storage.Delete(ctx, "streams/abc/c2s/000000000.seg"); err != nil {
|
||||
t.Fatalf("Delete: %v", err)
|
||||
}
|
||||
if drive.count() != 0 {
|
||||
t.Fatalf("fake drive still holds %d files", drive.count())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveListChildren(t *testing.T) {
|
||||
newFakeDrive(t)
|
||||
storage := newDriveBackend(t)
|
||||
ctx := context.Background()
|
||||
|
||||
for _, name := range []string{
|
||||
"streams/one/c2s/000000000.seg",
|
||||
"streams/one/s2c/000000000.seg",
|
||||
"streams/two/c2s/000000000.seg",
|
||||
} {
|
||||
if err := storage.Put(ctx, name, []byte("x")); err != nil {
|
||||
t.Fatalf("Put %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
names, err := storage.List(ctx, "streams")
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(names) != 2 {
|
||||
t.Fatalf("List returned %v, want 2 sessions", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveDeleteSession(t *testing.T) {
|
||||
drive := newFakeDrive(t)
|
||||
storage := newDriveBackend(t)
|
||||
ctx := context.Background()
|
||||
|
||||
for _, name := range []string{
|
||||
"streams/one/c2s/000000000.seg",
|
||||
"streams/one/c2s/000000001.end",
|
||||
"streams/one/s2c/000000000.seg",
|
||||
"streams/two/c2s/000000000.seg",
|
||||
} {
|
||||
if err := storage.Put(ctx, name, []byte("x")); err != nil {
|
||||
t.Fatalf("Put %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := storage.Delete(ctx, "streams/one"); err != nil {
|
||||
t.Fatalf("Delete: %v", err)
|
||||
}
|
||||
if drive.count() != 1 {
|
||||
t.Fatalf("fake drive holds %d files, want 1", drive.count())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveRetry(t *testing.T) {
|
||||
drive := newFakeDrive(t)
|
||||
storage := newDriveBackend(t)
|
||||
ctx := context.Background()
|
||||
|
||||
drive.failNext("upload")
|
||||
if err := storage.Put(ctx, "sessions/abc", nil); err != nil {
|
||||
t.Fatalf("Put did not survive a 429: %v", err)
|
||||
}
|
||||
|
||||
drive.failNext("list")
|
||||
if _, err := storage.List(ctx, "sessions"); err != nil {
|
||||
t.Fatalf("List did not survive a 503: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveTokenCache(t *testing.T) {
|
||||
drive := newFakeDrive(t)
|
||||
storage := newDriveBackend(t)
|
||||
ctx := context.Background()
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
if err := storage.Put(ctx, fmt.Sprintf("sessions/s%d", i), nil); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
}
|
||||
if drive.tokens != 1 {
|
||||
t.Fatalf("token endpoint hit %d times, want 1", drive.tokens)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveTransport(t *testing.T) {
|
||||
newFakeDrive(t)
|
||||
|
||||
client, server, cleanup := pairWith(t, driveSettings())
|
||||
defer cleanup()
|
||||
|
||||
if _, err := client.Write([]byte("ping")); err != nil {
|
||||
t.Fatalf("client write: %v", err)
|
||||
}
|
||||
expectRead(t, server, "ping")
|
||||
|
||||
if _, err := server.Write([]byte("pong")); err != nil {
|
||||
t.Fatalf("server write: %v", err)
|
||||
}
|
||||
expectRead(t, client, "pong")
|
||||
}
|
||||
|
||||
func TestDriveLargeTransfer(t *testing.T) {
|
||||
newFakeDrive(t)
|
||||
|
||||
client, server, cleanup := pairWith(t, driveSettings())
|
||||
defer cleanup()
|
||||
|
||||
payload := make([]byte, 300000)
|
||||
for i := range payload {
|
||||
payload[i] = byte(i % 251)
|
||||
}
|
||||
|
||||
go func() {
|
||||
client.Write(payload)
|
||||
}()
|
||||
|
||||
if err := server.SetReadDeadline(time.Now().Add(60 * time.Second)); err != nil {
|
||||
t.Fatalf("SetReadDeadline: %v", err)
|
||||
}
|
||||
got := make([]byte, len(payload))
|
||||
if _, err := io.ReadFull(server, got); err != nil {
|
||||
t.Fatalf("ReadFull: %v", err)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != payload[i] {
|
||||
t.Fatalf("payload mismatch at byte %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimited(t *testing.T) {
|
||||
limited := []string{
|
||||
`{"error":{"code":403,"errors":[{"reason":"userRateLimitExceeded"}]}}`,
|
||||
`{"error":{"code":403,"errors":[{"reason":"rateLimitExceeded"}]}}`,
|
||||
`{"error":{"code":403,"errors":[{"reason":"sharingRateLimitExceeded"}]}}`,
|
||||
`{"error":{"status":"RESOURCE_EXHAUSTED"}}`,
|
||||
}
|
||||
for _, payload := range limited {
|
||||
if !rateLimited([]byte(payload)) {
|
||||
t.Fatalf("rateLimited missed %s", payload)
|
||||
}
|
||||
}
|
||||
|
||||
permanent := []string{
|
||||
`{"error":{"code":403,"errors":[{"reason":"insufficientFilePermissions"}]}}`,
|
||||
`{"error":{"code":403,"errors":[{"reason":"storageQuotaExceeded"}]}}`,
|
||||
`not json at all`,
|
||||
}
|
||||
for _, payload := range permanent {
|
||||
if rateLimited([]byte(payload)) {
|
||||
t.Fatalf("rateLimited treated %s as temporary", payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveRetryRateLimit(t *testing.T) {
|
||||
drive := newFakeDrive(t)
|
||||
storage := newDriveBackend(t)
|
||||
|
||||
drive.failOnceWith("upload", http.StatusForbidden,
|
||||
`{"error":{"code":403,"errors":[{"reason":"userRateLimitExceeded"}]}}`)
|
||||
|
||||
if err := storage.Put(context.Background(), "sessions/abc", nil); err != nil {
|
||||
t.Fatalf("Put did not survive a 403: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveSharedClient(t *testing.T) {
|
||||
newFakeDrive(t)
|
||||
|
||||
first, err := newStorage(driveSettings())
|
||||
if err != nil {
|
||||
t.Fatalf("newStorage: %v", err)
|
||||
}
|
||||
second, err := newStorage(driveSettings())
|
||||
if err != nil {
|
||||
t.Fatalf("newStorage: %v", err)
|
||||
}
|
||||
if first != second {
|
||||
t.Fatal("same settings did not share one storage")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveInlineListing(t *testing.T) {
|
||||
drive := newFakeDrive(t)
|
||||
storage := newDriveBackend(t)
|
||||
ctx := context.Background()
|
||||
|
||||
payload := []byte("small enough to ride along with the listing")
|
||||
if err := storage.Put(ctx, "streams/abc/c2s/000000000.seg", payload); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
|
||||
entries, err := storage.List(ctx, "streams/abc/c2s")
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(entries) != 1 {
|
||||
t.Fatalf("List returned %v, want 1 entry", entries)
|
||||
}
|
||||
if string(entries[0].Inline) != string(payload) {
|
||||
t.Fatalf("listing carried %q, want %q", entries[0].Inline, payload)
|
||||
}
|
||||
|
||||
d := drive
|
||||
d.mu.Lock()
|
||||
var stored *fakeFile
|
||||
for _, f := range d.files {
|
||||
stored = f
|
||||
}
|
||||
d.mu.Unlock()
|
||||
if len(stored.data) != 0 {
|
||||
t.Fatal("small payload was uploaded as content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveLargeContent(t *testing.T) {
|
||||
drive := newFakeDrive(t)
|
||||
storage := newDriveBackend(t)
|
||||
ctx := context.Background()
|
||||
|
||||
payload := make([]byte, driveInlineLimit+1)
|
||||
for i := range payload {
|
||||
payload[i] = byte(i)
|
||||
}
|
||||
if err := storage.Put(ctx, "streams/abc/c2s/000000000.seg", payload); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
|
||||
entries, err := storage.List(ctx, "streams/abc/c2s")
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(entries) != 1 || entries[0].Inline != nil {
|
||||
t.Fatalf("large payload was inlined, got %v", entries)
|
||||
}
|
||||
|
||||
got, err := storage.Get(ctx, "streams/abc/c2s/000000000.seg")
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
if len(got) != len(payload) {
|
||||
t.Fatalf("Get returned %d bytes, want %d", len(got), len(payload))
|
||||
}
|
||||
_ = drive
|
||||
}
|
||||
|
||||
func TestDriveInlineGet(t *testing.T) {
|
||||
newFakeDrive(t)
|
||||
storage := newDriveBackend(t)
|
||||
ctx := context.Background()
|
||||
|
||||
payload := []byte("only in the description")
|
||||
if err := storage.Put(ctx, "sessions/abc", payload); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
|
||||
got, err := storage.Get(ctx, "sessions/abc")
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
if string(got) != string(payload) {
|
||||
t.Fatalf("Get returned %q, want %q", got, payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveFronting(t *testing.T) {
|
||||
drive := newFakeDrive(t)
|
||||
|
||||
fake, err := url.Parse(drive.server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("parse: %v", err)
|
||||
}
|
||||
port, err := net.PortFromString(fake.Port())
|
||||
if err != nil {
|
||||
t.Fatalf("port: %v", err)
|
||||
}
|
||||
|
||||
driveTokenURL = "http://www.googleapis.com/token"
|
||||
driveFilesURL = "http://www.googleapis.com/files"
|
||||
driveUploadURL = "http://www.googleapis.com/upload"
|
||||
|
||||
settings := driveSettings()
|
||||
settings.Destination = &net.Destination{
|
||||
Address: net.ParseAddress(fake.Hostname()),
|
||||
Port: port,
|
||||
Network: net.Network_TCP,
|
||||
}
|
||||
|
||||
storage, err := newDriveStorage(settings, settings.ProtocolSettings.(*Config))
|
||||
if err != nil {
|
||||
t.Fatalf("newDriveStorage: %v", err)
|
||||
}
|
||||
|
||||
if err := storage.Put(context.Background(), "sessions/fronted", []byte("x")); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
if drive.count() != 1 {
|
||||
t.Fatalf("fake drive holds %d files, want 1", drive.count())
|
||||
}
|
||||
|
||||
for _, host := range drive.seenHosts() {
|
||||
if host != "www.googleapis.com" {
|
||||
t.Fatalf("inner host was %q, want www.googleapis.com regardless of address", host)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func jsonWalk(payload []byte, path string) interface{} {
|
||||
var root interface{}
|
||||
if json.Unmarshal(payload, &root) != nil {
|
||||
return nil
|
||||
}
|
||||
node := root
|
||||
for _, key := range strings.Split(path, ".") {
|
||||
obj, ok := node.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
node, ok = obj[key]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
func jsonString(payload []byte, path string) string {
|
||||
if s, ok := jsonWalk(payload, path).(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func jsonNumber(payload []byte, path string) int64 {
|
||||
if f, ok := jsonWalk(payload, path).(float64); ok {
|
||||
return int64(f)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
const tempPrefix = ".xdrive-tmp-"
|
||||
|
||||
type localStorage struct {
|
||||
root string
|
||||
}
|
||||
|
||||
func newLocalStorage(root string) (*localStorage, error) {
|
||||
if root == "" {
|
||||
return nil, errors.New(`empty "remoteFolder"`)
|
||||
}
|
||||
if err := os.MkdirAll(root, 0o700); err != nil {
|
||||
return nil, errors.New("failed to create remote folder").Base(err)
|
||||
}
|
||||
return &localStorage{root: root}, nil
|
||||
}
|
||||
|
||||
func (s *localStorage) resolve(name string) (string, error) {
|
||||
clean := path.Clean("/" + name)
|
||||
if clean == "/" {
|
||||
return "", errors.New("invalid object name: ", name)
|
||||
}
|
||||
if strings.HasPrefix(path.Base(clean), tempPrefix) {
|
||||
return "", errors.New("reserved object name: ", name)
|
||||
}
|
||||
return filepath.Join(s.root, filepath.FromSlash(clean[1:])), nil
|
||||
}
|
||||
|
||||
func (s *localStorage) Put(ctx context.Context, name string, data []byte) error {
|
||||
full, err := s.resolve(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir := filepath.Dir(full)
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return errors.New("failed to create folder ", dir).Base(err)
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp(dir, tempPrefix+"*")
|
||||
if err != nil {
|
||||
return errors.New("failed to create temp file in ", dir).Base(err)
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
defer os.Remove(tmpName)
|
||||
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
tmp.Close()
|
||||
return errors.New("failed to write ", name).Base(err)
|
||||
}
|
||||
if err := tmp.Close(); err != nil {
|
||||
return errors.New("failed to close ", name).Base(err)
|
||||
}
|
||||
if err := os.Rename(tmpName, full); err != nil {
|
||||
return errors.New("failed to commit ", name).Base(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *localStorage) Get(ctx context.Context, name string) ([]byte, error) {
|
||||
full, err := s.resolve(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := os.ReadFile(full)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, errNotFound
|
||||
}
|
||||
return nil, errors.New("failed to read ", name).Base(err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *localStorage) Delete(ctx context.Context, name string) error {
|
||||
full, err := s.resolve(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.RemoveAll(full); err != nil {
|
||||
return errors.New("failed to delete ", name).Base(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *localStorage) List(ctx context.Context, prefix string) ([]Entry, error) {
|
||||
full, err := s.resolve(prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries, err := os.ReadDir(full)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.New("failed to list ", prefix).Base(err)
|
||||
}
|
||||
found := make([]Entry, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(entry.Name(), tempPrefix) {
|
||||
continue
|
||||
}
|
||||
found = append(found, Entry{Name: entry.Name()})
|
||||
}
|
||||
return found, nil
|
||||
}
|
||||
|
||||
func (s *localStorage) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package xdrive
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
defaultSegmentBytes = 512 * 1024
|
||||
defaultFlushInterval = 20 * time.Millisecond
|
||||
defaultMinPollInterval = 50 * time.Millisecond
|
||||
defaultMaxPollInterval = 500 * time.Millisecond
|
||||
defaultEagerWindow = 2 * time.Second
|
||||
defaultHoleTimeout = 30 * time.Second
|
||||
defaultSessionTTL = 5 * time.Minute
|
||||
defaultConcurrency = 8
|
||||
|
||||
maxSegmentBytes = 16 * 1024 * 1024
|
||||
maxConcurrency = 64
|
||||
)
|
||||
|
||||
type params struct {
|
||||
segmentBytes int
|
||||
flushInterval time.Duration
|
||||
minPollInterval time.Duration
|
||||
maxPollInterval time.Duration
|
||||
eagerWindow time.Duration
|
||||
holeTimeout time.Duration
|
||||
sessionTTL time.Duration
|
||||
concurrency int
|
||||
}
|
||||
|
||||
func millis(value uint32, fallback time.Duration) time.Duration {
|
||||
if value == 0 {
|
||||
return fallback
|
||||
}
|
||||
return time.Duration(value) * time.Millisecond
|
||||
}
|
||||
|
||||
func seconds(value uint32, fallback time.Duration) time.Duration {
|
||||
if value == 0 {
|
||||
return fallback
|
||||
}
|
||||
return time.Duration(value) * time.Second
|
||||
}
|
||||
|
||||
func capped(value uint32, fallback, limit int) int {
|
||||
if value == 0 {
|
||||
return fallback
|
||||
}
|
||||
if int(value) > limit {
|
||||
return limit
|
||||
}
|
||||
return int(value)
|
||||
}
|
||||
|
||||
func paramsFromConfig(c *Config) params {
|
||||
p := params{
|
||||
segmentBytes: capped(c.SegmentBytes, defaultSegmentBytes, maxSegmentBytes),
|
||||
flushInterval: millis(c.FlushIntervalMs, defaultFlushInterval),
|
||||
minPollInterval: millis(c.PollIntervalMs, defaultMinPollInterval),
|
||||
maxPollInterval: millis(c.MaxPollIntervalMs, defaultMaxPollInterval),
|
||||
eagerWindow: millis(c.EagerWindowMs, defaultEagerWindow),
|
||||
holeTimeout: millis(c.HoleTimeoutMs, defaultHoleTimeout),
|
||||
sessionTTL: seconds(c.SessionTtlSeconds, defaultSessionTTL),
|
||||
concurrency: capped(c.Concurrency, defaultConcurrency, maxConcurrency),
|
||||
}
|
||||
if p.maxPollInterval < p.minPollInterval {
|
||||
p.maxPollInterval = p.minPollInterval
|
||||
}
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
var errNotFound = errors.New("object not found")
|
||||
|
||||
type Entry struct {
|
||||
Name string
|
||||
Inline []byte
|
||||
}
|
||||
|
||||
type Storage interface {
|
||||
Put(ctx context.Context, name string, data []byte) error
|
||||
Get(ctx context.Context, name string) ([]byte, error)
|
||||
Delete(ctx context.Context, name string) error
|
||||
List(ctx context.Context, prefix string) ([]Entry, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
func newStorage(streamSettings *internet.MemoryStreamConfig) (Storage, error) {
|
||||
config, err := streamConfig(streamSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch config.Service {
|
||||
case "local":
|
||||
return newLocalStorage(config.RemoteFolder)
|
||||
case "Google Drive":
|
||||
return sharedStorage(streamSettings, config, func() (Storage, error) {
|
||||
return newDriveStorage(streamSettings, config)
|
||||
})
|
||||
case "template":
|
||||
return sharedStorage(streamSettings, config, func() (Storage, error) {
|
||||
return newTemplateStorage(streamSettings, config)
|
||||
})
|
||||
default:
|
||||
return nil, errors.New("unsupported service: ", config.Service)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
sharedMu sync.Mutex
|
||||
shared = make(map[string]Storage)
|
||||
)
|
||||
|
||||
func shareKey(streamSettings *internet.MemoryStreamConfig, config *Config) string {
|
||||
parts := []string{config.Service, config.RemoteFolder}
|
||||
parts = append(parts, config.Secrets...)
|
||||
if streamSettings != nil {
|
||||
parts = append(parts, streamSettings.SecurityType)
|
||||
if streamSettings.Destination != nil {
|
||||
parts = append(parts, streamSettings.Destination.NetAddr())
|
||||
}
|
||||
}
|
||||
sum := sha256.Sum256([]byte(strings.Join(parts, "\x00")))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func resetSharedStorage() {
|
||||
sharedMu.Lock()
|
||||
shared = make(map[string]Storage)
|
||||
sharedMu.Unlock()
|
||||
}
|
||||
|
||||
func sharedStorage(streamSettings *internet.MemoryStreamConfig, config *Config, build func() (Storage, error)) (Storage, error) {
|
||||
key := shareKey(streamSettings, config)
|
||||
|
||||
sharedMu.Lock()
|
||||
defer sharedMu.Unlock()
|
||||
|
||||
if storage, ok := shared[key]; ok {
|
||||
return storage, nil
|
||||
}
|
||||
storage, err := build()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shared[key] = storage
|
||||
return storage, nil
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
type authTemplate struct {
|
||||
Type string `json:"type"`
|
||||
Header map[string]string `json:"header"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
TokenURL string `json:"tokenUrl"`
|
||||
Form map[string]string `json:"form"`
|
||||
TokenPath string `json:"tokenPath"`
|
||||
ExpiryPath string `json:"expiryPath"`
|
||||
}
|
||||
|
||||
type opTemplate struct {
|
||||
Method string `json:"method"`
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Body string `json:"body"`
|
||||
NamesRegex string `json:"namesRegex"`
|
||||
}
|
||||
|
||||
type retryTemplate struct {
|
||||
Status []int `json:"status"`
|
||||
RateReason string `json:"rateReason"`
|
||||
}
|
||||
|
||||
type storageTemplate struct {
|
||||
Flatten bool `json:"flatten"`
|
||||
Concurrency int `json:"concurrency"`
|
||||
Auth authTemplate `json:"auth"`
|
||||
Put opTemplate `json:"put"`
|
||||
Get opTemplate `json:"get"`
|
||||
Delete opTemplate `json:"delete"`
|
||||
List opTemplate `json:"list"`
|
||||
Retry retryTemplate `json:"retry"`
|
||||
|
||||
names *regexp.Regexp
|
||||
}
|
||||
|
||||
type templateStorage struct {
|
||||
tmpl *storageTemplate
|
||||
client *http.Client
|
||||
folder string
|
||||
secrets []string
|
||||
|
||||
inflight chan struct{}
|
||||
|
||||
tokenMu sync.Mutex
|
||||
token string
|
||||
tokenExpiry time.Time
|
||||
}
|
||||
|
||||
func newTemplateStorage(streamSettings *internet.MemoryStreamConfig, config *Config) (*templateStorage, error) {
|
||||
tmpl := &storageTemplate{}
|
||||
if err := json.Unmarshal([]byte(config.Template), tmpl); err != nil {
|
||||
return nil, errors.New("invalid template").Base(err)
|
||||
}
|
||||
if tmpl.Put.URL == "" || tmpl.Get.URL == "" || tmpl.List.URL == "" || tmpl.Delete.URL == "" {
|
||||
return nil, errors.New("template needs put, get, list and delete operations")
|
||||
}
|
||||
if tmpl.List.NamesRegex == "" {
|
||||
return nil, errors.New("template list needs a namesRegex")
|
||||
}
|
||||
re, err := regexp.Compile(tmpl.List.NamesRegex)
|
||||
if err != nil {
|
||||
return nil, errors.New("bad namesRegex").Base(err)
|
||||
}
|
||||
if re.NumSubexp() < 1 {
|
||||
return nil, errors.New("namesRegex needs one capture group")
|
||||
}
|
||||
tmpl.names = re
|
||||
|
||||
conc := tmpl.Concurrency
|
||||
if conc <= 0 {
|
||||
conc = driveMaxInflight
|
||||
}
|
||||
if conc > maxTemplateConcurrency {
|
||||
conc = maxTemplateConcurrency
|
||||
}
|
||||
|
||||
return &templateStorage{
|
||||
tmpl: tmpl,
|
||||
client: newServiceClient(streamSettings, driveTimeout, conc),
|
||||
folder: config.RemoteFolder,
|
||||
secrets: config.Secrets,
|
||||
inflight: make(chan struct{}, conc),
|
||||
}, nil
|
||||
}
|
||||
|
||||
const maxTemplateConcurrency = 256
|
||||
|
||||
func (s *templateStorage) baseVars() map[string]string {
|
||||
vars := map[string]string{"folder": s.folder}
|
||||
for i, secret := range s.secrets {
|
||||
vars["secret"+itoa(i)] = secret
|
||||
}
|
||||
return vars
|
||||
}
|
||||
|
||||
func subst(tmpl string, vars map[string]string) string {
|
||||
if tmpl == "" || !strings.ContainsRune(tmpl, '{') {
|
||||
return tmpl
|
||||
}
|
||||
out := tmpl
|
||||
for k, v := range vars {
|
||||
out = strings.ReplaceAll(out, "{"+k+"}", v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func itoa(i int) string {
|
||||
if i == 0 {
|
||||
return "0"
|
||||
}
|
||||
var b [20]byte
|
||||
pos := len(b)
|
||||
for i > 0 {
|
||||
pos--
|
||||
b[pos] = byte('0' + i%10)
|
||||
i /= 10
|
||||
}
|
||||
return string(b[pos:])
|
||||
}
|
||||
|
||||
func (s *templateStorage) storedName(name string) string {
|
||||
if s.tmpl.Flatten {
|
||||
return flatten(name)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func (s *templateStorage) retryable(status int, payload []byte) bool {
|
||||
for _, code := range s.tmpl.Retry.Status {
|
||||
if status == code {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if status == http.StatusForbidden && s.tmpl.Retry.RateReason != "" {
|
||||
if reason := jsonString(payload, s.tmpl.Retry.RateReason); reason != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *templateStorage) authHeaders(ctx context.Context, vars map[string]string) (map[string]string, error) {
|
||||
switch s.tmpl.Auth.Type {
|
||||
case "", "none":
|
||||
return nil, nil
|
||||
case "static", "oauth2":
|
||||
if s.tmpl.Auth.Type == "oauth2" {
|
||||
token, err := s.accessToken(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vars["token"] = token
|
||||
}
|
||||
headers := make(map[string]string, len(s.tmpl.Auth.Header))
|
||||
for k, v := range s.tmpl.Auth.Header {
|
||||
headers[k] = subst(v, vars)
|
||||
}
|
||||
return headers, nil
|
||||
case "basic":
|
||||
user := subst(s.tmpl.Auth.Username, vars)
|
||||
pass := subst(s.tmpl.Auth.Password, vars)
|
||||
enc := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass))
|
||||
return map[string]string{"Authorization": "Basic " + enc}, nil
|
||||
default:
|
||||
return nil, errors.New("unsupported auth type: ", s.tmpl.Auth.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *templateStorage) accessToken(ctx context.Context) (string, error) {
|
||||
s.tokenMu.Lock()
|
||||
defer s.tokenMu.Unlock()
|
||||
|
||||
if s.token != "" && time.Now().Before(s.tokenExpiry) {
|
||||
return s.token, nil
|
||||
}
|
||||
|
||||
form := make(map[string]string, len(s.tmpl.Auth.Form))
|
||||
vars := s.baseVars()
|
||||
values := strings.Builder{}
|
||||
first := true
|
||||
for k, v := range s.tmpl.Auth.Form {
|
||||
form[k] = subst(v, vars)
|
||||
if !first {
|
||||
values.WriteByte('&')
|
||||
}
|
||||
first = false
|
||||
values.WriteString(k)
|
||||
values.WriteByte('=')
|
||||
values.WriteString(form[k])
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.tmpl.Auth.TokenURL,
|
||||
strings.NewReader(values.String()))
|
||||
if err != nil {
|
||||
return "", errors.New("failed to build the token request").Base(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return "", errors.New("failed to fetch the token").Base(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
payload, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return "", errors.New("failed to read the token response").Base(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", errors.New("the token endpoint answered ", resp.StatusCode, ": ", string(payload))
|
||||
}
|
||||
|
||||
path := s.tmpl.Auth.TokenPath
|
||||
if path == "" {
|
||||
path = "access_token"
|
||||
}
|
||||
token := jsonString(payload, path)
|
||||
if token == "" {
|
||||
return "", errors.New("the token response has no token at ", path)
|
||||
}
|
||||
|
||||
lifetime := int64(3600)
|
||||
if s.tmpl.Auth.ExpiryPath != "" {
|
||||
if n := jsonNumber(payload, s.tmpl.Auth.ExpiryPath); n > 0 {
|
||||
lifetime = n
|
||||
}
|
||||
}
|
||||
if lifetime > 60 {
|
||||
lifetime -= 60
|
||||
}
|
||||
s.token = token
|
||||
s.tokenExpiry = time.Now().Add(time.Duration(lifetime) * time.Second)
|
||||
return s.token, nil
|
||||
}
|
||||
|
||||
func (s *templateStorage) invalidateToken() {
|
||||
s.tokenMu.Lock()
|
||||
s.token = ""
|
||||
s.tokenMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *templateStorage) do(ctx context.Context, op *opTemplate, vars map[string]string, body []byte) (int, []byte, error) {
|
||||
backoff := driveInitialBackoff
|
||||
var lastErr error
|
||||
|
||||
for attempt := 0; attempt < driveMaxAttempts; attempt++ {
|
||||
if attempt > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return 0, nil, ctx.Err()
|
||||
case <-time.After(jitter(backoff)):
|
||||
}
|
||||
backoff *= 2
|
||||
if backoff > driveMaxBackoff {
|
||||
backoff = driveMaxBackoff
|
||||
}
|
||||
}
|
||||
|
||||
authHeaders, err := s.authHeaders(ctx, vars)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
|
||||
method := op.Method
|
||||
if method == "" {
|
||||
method = http.MethodGet
|
||||
}
|
||||
|
||||
var reader io.Reader
|
||||
if body != nil {
|
||||
reader = bytes.NewReader(body)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, subst(op.URL, vars), reader)
|
||||
if err != nil {
|
||||
return 0, nil, errors.New("failed to build request").Base(err)
|
||||
}
|
||||
for k, v := range authHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
for k, v := range op.Headers {
|
||||
req.Header.Set(k, subst(v, vars))
|
||||
}
|
||||
|
||||
select {
|
||||
case s.inflight <- struct{}{}:
|
||||
case <-ctx.Done():
|
||||
return 0, nil, ctx.Err()
|
||||
}
|
||||
resp, err := s.client.Do(req)
|
||||
<-s.inflight
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return 0, nil, ctx.Err()
|
||||
}
|
||||
lastErr = errors.New("request failed").Base(err)
|
||||
continue
|
||||
}
|
||||
payload, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return 0, nil, ctx.Err()
|
||||
}
|
||||
lastErr = errors.New("failed to read response").Base(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized && s.tmpl.Auth.Type == "oauth2" {
|
||||
s.invalidateToken()
|
||||
lastErr = errors.New("the service rejected the token")
|
||||
continue
|
||||
}
|
||||
if s.retryable(resp.StatusCode, payload) {
|
||||
lastErr = errors.New("the service answered ", resp.StatusCode)
|
||||
continue
|
||||
}
|
||||
return resp.StatusCode, payload, nil
|
||||
}
|
||||
return 0, nil, lastErr
|
||||
}
|
||||
|
||||
func (s *templateStorage) Put(ctx context.Context, name string, data []byte) error {
|
||||
vars := s.baseVars()
|
||||
vars["name"] = s.storedName(name)
|
||||
|
||||
body := data
|
||||
if s.tmpl.Put.Body != "" {
|
||||
vars["data"] = base64.StdEncoding.EncodeToString(data)
|
||||
body = []byte(subst(s.tmpl.Put.Body, vars))
|
||||
}
|
||||
|
||||
status, payload, err := s.do(ctx, &s.tmpl.Put, vars, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return errors.New("put of ", name, " answered ", status, ": ", string(payload))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *templateStorage) Get(ctx context.Context, name string) ([]byte, error) {
|
||||
vars := s.baseVars()
|
||||
vars["name"] = s.storedName(name)
|
||||
|
||||
status, payload, err := s.do(ctx, &s.tmpl.Get, vars, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case status >= 200 && status < 300:
|
||||
return payload, nil
|
||||
case status == http.StatusNotFound:
|
||||
return nil, errNotFound
|
||||
default:
|
||||
return nil, errors.New("get of ", name, " answered ", status, ": ", string(payload))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *templateStorage) Delete(ctx context.Context, name string) error {
|
||||
vars := s.baseVars()
|
||||
vars["name"] = s.storedName(name)
|
||||
|
||||
status, payload, err := s.do(ctx, &s.tmpl.Delete, vars, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status == http.StatusNotFound || (status >= 200 && status < 300) {
|
||||
return nil
|
||||
}
|
||||
return errors.New("delete of ", name, " answered ", status, ": ", string(payload))
|
||||
}
|
||||
|
||||
func (s *templateStorage) List(ctx context.Context, prefix string) ([]Entry, error) {
|
||||
vars := s.baseVars()
|
||||
flat := s.storedName(prefix)
|
||||
vars["prefix"] = flat
|
||||
|
||||
status, payload, err := s.do(ctx, &s.tmpl.List, vars, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status == http.StatusNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return nil, errors.New("list of ", prefix, " answered ", status, ": ", string(payload))
|
||||
}
|
||||
|
||||
matches := s.tmpl.names.FindAllStringSubmatch(string(payload), -1)
|
||||
if !s.tmpl.Flatten {
|
||||
entries := make([]Entry, 0, len(matches))
|
||||
for _, m := range matches {
|
||||
entries = append(entries, Entry{Name: m[1]})
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
want := flat + flatSeparator
|
||||
seen := make(map[string]bool, len(matches))
|
||||
entries := make([]Entry, 0, len(matches))
|
||||
for _, m := range matches {
|
||||
name := m[1]
|
||||
if !strings.HasPrefix(name, want) {
|
||||
continue
|
||||
}
|
||||
rest := strings.TrimPrefix(name, want)
|
||||
if rest == "" {
|
||||
continue
|
||||
}
|
||||
if cut := strings.Index(rest, flatSeparator); cut >= 0 {
|
||||
rest = rest[:cut]
|
||||
}
|
||||
if seen[rest] {
|
||||
continue
|
||||
}
|
||||
seen[rest] = true
|
||||
entries = append(entries, Entry{Name: rest})
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (s *templateStorage) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
type fakeStore struct {
|
||||
server *httptest.Server
|
||||
|
||||
mu sync.Mutex
|
||||
objects map[string][]byte
|
||||
needAuth string
|
||||
sawAuth string
|
||||
tokens int
|
||||
}
|
||||
|
||||
func newFakeStore(t *testing.T) *fakeStore {
|
||||
t.Helper()
|
||||
|
||||
store := &fakeStore{objects: make(map[string][]byte)}
|
||||
store.server = httptest.NewServer(http.HandlerFunc(store.handle))
|
||||
t.Cleanup(store.server.Close)
|
||||
return store
|
||||
}
|
||||
|
||||
func (s *fakeStore) handle(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/token" {
|
||||
s.mu.Lock()
|
||||
s.tokens++
|
||||
s.mu.Unlock()
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"access_token": "tok-fake", "expires_in": 3600,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if auth := r.Header.Get("Authorization"); auth != "" {
|
||||
s.mu.Lock()
|
||||
s.sawAuth = auth
|
||||
s.mu.Unlock()
|
||||
}
|
||||
if s.needAuth != "" && r.Header.Get("Authorization") != s.needAuth {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
key := strings.TrimPrefix(r.URL.Path, "/folder/")
|
||||
|
||||
switch r.Method {
|
||||
case "PROPFIND":
|
||||
s.mu.Lock()
|
||||
var b strings.Builder
|
||||
for name := range s.objects {
|
||||
fmt.Fprintf(&b, "<d:href>/folder/%s</d:href>\n", name)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
w.Write([]byte(b.String()))
|
||||
case http.MethodPut:
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
s.mu.Lock()
|
||||
s.objects[key] = body
|
||||
s.mu.Unlock()
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
case http.MethodGet:
|
||||
s.mu.Lock()
|
||||
data, ok := s.objects[key]
|
||||
s.mu.Unlock()
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Write(data)
|
||||
case http.MethodDelete:
|
||||
s.mu.Lock()
|
||||
delete(s.objects, key)
|
||||
s.mu.Unlock()
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *fakeStore) count() int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return len(s.objects)
|
||||
}
|
||||
|
||||
func (s *fakeStore) seenAuth() string {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.sawAuth
|
||||
}
|
||||
|
||||
func templateSettings(store *fakeStore, auth map[string]interface{}, secrets []string) *internet.MemoryStreamConfig {
|
||||
base := store.server.URL
|
||||
tmpl := map[string]interface{}{
|
||||
"flatten": true,
|
||||
"auth": auth,
|
||||
"put": map[string]interface{}{"method": "PUT", "url": base + "/folder/{name}"},
|
||||
"get": map[string]interface{}{"method": "GET", "url": base + "/folder/{name}"},
|
||||
"delete": map[string]interface{}{"method": "DELETE", "url": base + "/folder/{name}"},
|
||||
"list": map[string]interface{}{
|
||||
"method": "PROPFIND",
|
||||
"url": base + "/folder/",
|
||||
"namesRegex": `<d:href>/folder/([^<]+)</d:href>`,
|
||||
},
|
||||
"retry": map[string]interface{}{"status": []int{429, 500, 502, 503}},
|
||||
}
|
||||
raw, _ := json.Marshal(tmpl)
|
||||
return &internet.MemoryStreamConfig{
|
||||
ProtocolName: protocolName,
|
||||
ProtocolSettings: &Config{
|
||||
RemoteFolder: "folder",
|
||||
Service: "template",
|
||||
Secrets: secrets,
|
||||
Template: string(raw),
|
||||
FlushIntervalMs: 5,
|
||||
PollIntervalMs: 5,
|
||||
MaxPollIntervalMs: 20,
|
||||
SessionTtlSeconds: 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newTemplateBackend(t *testing.T, store *fakeStore, auth map[string]interface{}, secrets []string) *templateStorage {
|
||||
t.Helper()
|
||||
settings := templateSettings(store, auth, secrets)
|
||||
storage, err := newTemplateStorage(settings, settings.ProtocolSettings.(*Config))
|
||||
if err != nil {
|
||||
t.Fatalf("newTemplateStorage: %v", err)
|
||||
}
|
||||
return storage
|
||||
}
|
||||
|
||||
func TestTemplateRoundTrip(t *testing.T) {
|
||||
store := newFakeStore(t)
|
||||
storage := newTemplateBackend(t, store, map[string]interface{}{"type": "none"}, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
if err := storage.Put(ctx, "streams/abc/c2s/000000000.seg", []byte("hello")); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
data, err := storage.Get(ctx, "streams/abc/c2s/000000000.seg")
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
if string(data) != "hello" {
|
||||
t.Fatalf("Get returned %q, want hello", data)
|
||||
}
|
||||
|
||||
entries, err := storage.List(ctx, "streams/abc/c2s")
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(entries) != 1 || entries[0].Name != "000000000.seg" {
|
||||
t.Fatalf("List returned %v, want one segment", entries)
|
||||
}
|
||||
|
||||
if _, err := storage.Get(ctx, "streams/abc/c2s/000000009.seg"); err != errNotFound {
|
||||
t.Fatalf("Get of a missing object returned %v, want errNotFound", err)
|
||||
}
|
||||
|
||||
if err := storage.Delete(ctx, "streams/abc/c2s/000000000.seg"); err != nil {
|
||||
t.Fatalf("Delete: %v", err)
|
||||
}
|
||||
if store.count() != 0 {
|
||||
t.Fatalf("store still holds %d objects", store.count())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateListReturnsDirectChildren(t *testing.T) {
|
||||
store := newFakeStore(t)
|
||||
storage := newTemplateBackend(t, store, map[string]interface{}{"type": "none"}, nil)
|
||||
ctx := context.Background()
|
||||
|
||||
for _, name := range []string{
|
||||
"streams/one/c2s/000000000.seg",
|
||||
"streams/one/s2c/000000000.seg",
|
||||
"streams/two/c2s/000000000.seg",
|
||||
} {
|
||||
if err := storage.Put(ctx, name, []byte("x")); err != nil {
|
||||
t.Fatalf("Put %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
entries, err := storage.List(ctx, "streams")
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(entries) != 2 {
|
||||
t.Fatalf("List returned %v, want the two session ids", entries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateBasicAuth(t *testing.T) {
|
||||
store := newFakeStore(t)
|
||||
store.needAuth = "Basic dXNlcjpwYXNz"
|
||||
auth := map[string]interface{}{"type": "basic", "username": "{secret0}", "password": "{secret1}"}
|
||||
storage := newTemplateBackend(t, store, auth, []string{"user", "pass"})
|
||||
|
||||
if err := storage.Put(context.Background(), "sessions/a", []byte("x")); err != nil {
|
||||
t.Fatalf("Put with basic auth: %v", err)
|
||||
}
|
||||
if store.seenAuth() != "Basic dXNlcjpwYXNz" {
|
||||
t.Fatalf("server saw auth %q", store.seenAuth())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateOAuth(t *testing.T) {
|
||||
store := newFakeStore(t)
|
||||
store.needAuth = "Bearer tok-fake"
|
||||
auth := map[string]interface{}{
|
||||
"type": "oauth2",
|
||||
"tokenUrl": store.server.URL + "/token",
|
||||
"form": map[string]interface{}{"grant_type": "refresh_token", "refresh_token": "{secret0}"},
|
||||
"header": map[string]interface{}{"Authorization": "Bearer {token}"},
|
||||
}
|
||||
storage := newTemplateBackend(t, store, auth, []string{"refresh"})
|
||||
ctx := context.Background()
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
if err := storage.Put(ctx, fmt.Sprintf("sessions/s%d", i), nil); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
}
|
||||
if store.tokens != 1 {
|
||||
t.Fatalf("token endpoint was hit %d times, want 1", store.tokens)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateTransport(t *testing.T) {
|
||||
store := newFakeStore(t)
|
||||
settings := templateSettings(store, map[string]interface{}{"type": "none"}, nil)
|
||||
|
||||
client, server, cleanup := pairWith(t, settings)
|
||||
defer cleanup()
|
||||
|
||||
if _, err := client.Write([]byte("ping")); err != nil {
|
||||
t.Fatalf("client write: %v", err)
|
||||
}
|
||||
expectRead(t, server, "ping")
|
||||
|
||||
if _, err := server.Write([]byte("pong")); err != nil {
|
||||
t.Fatalf("server write: %v", err)
|
||||
}
|
||||
expectRead(t, client, "pong")
|
||||
|
||||
payload := make([]byte, 300000)
|
||||
for i := range payload {
|
||||
payload[i] = byte(i % 251)
|
||||
}
|
||||
go func() { client.Write(payload) }()
|
||||
|
||||
if err := server.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
|
||||
t.Fatalf("SetReadDeadline: %v", err)
|
||||
}
|
||||
got := make([]byte, len(payload))
|
||||
if _, err := io.ReadFull(server, got); err != nil {
|
||||
t.Fatalf("ReadFull: %v", err)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != payload[i] {
|
||||
t.Fatalf("payload mismatch at byte %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateConcurrency(t *testing.T) {
|
||||
store := newFakeStore(t)
|
||||
settings := templateSettings(store, map[string]interface{}{"type": "none"}, nil)
|
||||
|
||||
var tmpl map[string]interface{}
|
||||
cfg := settings.ProtocolSettings.(*Config)
|
||||
if err := json.Unmarshal([]byte(cfg.Template), &tmpl); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
tmpl["concurrency"] = 4
|
||||
raw, _ := json.Marshal(tmpl)
|
||||
cfg.Template = string(raw)
|
||||
|
||||
storage, err := newTemplateStorage(settings, cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("newTemplateStorage: %v", err)
|
||||
}
|
||||
if cap(storage.inflight) != 4 {
|
||||
t.Fatalf("inflight cap is %d, want 4 from the template", cap(storage.inflight))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateConcurrencyDefault(t *testing.T) {
|
||||
store := newFakeStore(t)
|
||||
storage := newTemplateBackend(t, store, map[string]interface{}{"type": "none"}, nil)
|
||||
if cap(storage.inflight) != driveMaxInflight {
|
||||
t.Fatalf("default inflight cap is %d, want %d", cap(storage.inflight), driveMaxInflight)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
maxCoalescedTicks = 8
|
||||
|
||||
segSuffix = ".seg"
|
||||
endSuffix = ".end"
|
||||
errSuffix = ".err"
|
||||
)
|
||||
|
||||
func objectName(prefix string, seq int64, suffix string) string {
|
||||
return fmt.Sprintf("%s/%09d%s", prefix, seq, suffix)
|
||||
}
|
||||
|
||||
func parseEntry(name string) (int64, bool) {
|
||||
dot := strings.LastIndexByte(name, '.')
|
||||
if dot < 0 {
|
||||
return 0, false
|
||||
}
|
||||
switch name[dot:] {
|
||||
case segSuffix, endSuffix, errSuffix:
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
seq, err := strconv.ParseInt(name[:dot], 10, 64)
|
||||
if err != nil || seq < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return seq, true
|
||||
}
|
||||
|
||||
type walWriter struct {
|
||||
ctx context.Context
|
||||
storage Storage
|
||||
prefix string
|
||||
params
|
||||
sem chan struct{}
|
||||
wg sync.WaitGroup
|
||||
|
||||
mu sync.Mutex
|
||||
buf []byte
|
||||
seq int64
|
||||
lastSize int
|
||||
held int
|
||||
closed bool
|
||||
err error
|
||||
}
|
||||
|
||||
func newWALWriter(ctx context.Context, storage Storage, prefix string, p params) *walWriter {
|
||||
w := &walWriter{
|
||||
ctx: ctx,
|
||||
storage: storage,
|
||||
prefix: prefix,
|
||||
params: p,
|
||||
sem: make(chan struct{}, p.concurrency),
|
||||
}
|
||||
go w.flushLoop()
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *walWriter) Write(p []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
if w.closed {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
|
||||
w.buf = append(w.buf, p...)
|
||||
for len(w.buf) >= w.segmentBytes {
|
||||
if err := w.flushLocked(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *walWriter) flushLoop() {
|
||||
ticker := time.NewTicker(w.flushInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
w.mu.Lock()
|
||||
if !w.closed && w.err == nil && len(w.buf) > 0 && w.readyToFlush() {
|
||||
w.flushLocked()
|
||||
}
|
||||
done := w.closed || w.err != nil
|
||||
w.mu.Unlock()
|
||||
if done {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *walWriter) readyToFlush() bool {
|
||||
grew := len(w.buf) > w.lastSize
|
||||
w.lastSize = len(w.buf)
|
||||
|
||||
if grew && w.held < maxCoalescedTicks {
|
||||
w.held++
|
||||
return false
|
||||
}
|
||||
w.held = 0
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *walWriter) flushLocked() error {
|
||||
if len(w.buf) == 0 {
|
||||
return nil
|
||||
}
|
||||
if w.err != nil {
|
||||
return w.err
|
||||
}
|
||||
|
||||
n := len(w.buf)
|
||||
if n > w.segmentBytes {
|
||||
n = w.segmentBytes
|
||||
}
|
||||
|
||||
chunk := make([]byte, n)
|
||||
copy(chunk, w.buf[:n])
|
||||
seq := w.seq
|
||||
w.seq++
|
||||
|
||||
if n == len(w.buf) {
|
||||
w.buf = w.buf[:0]
|
||||
} else {
|
||||
w.buf = append(w.buf[:0], w.buf[n:]...)
|
||||
}
|
||||
w.lastSize = len(w.buf)
|
||||
w.held = 0
|
||||
|
||||
w.wg.Add(1)
|
||||
go w.upload(seq, chunk)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *walWriter) upload(seq int64, chunk []byte) {
|
||||
defer w.wg.Done()
|
||||
|
||||
select {
|
||||
case w.sem <- struct{}{}:
|
||||
case <-w.ctx.Done():
|
||||
return
|
||||
}
|
||||
defer func() { <-w.sem }()
|
||||
|
||||
if err := w.storage.Put(w.ctx, objectName(w.prefix, seq, segSuffix), chunk); err != nil {
|
||||
w.mu.Lock()
|
||||
if w.err == nil {
|
||||
w.err = errors.New("failed to store segment").Base(err)
|
||||
}
|
||||
w.mu.Unlock()
|
||||
w.storage.Put(w.ctx, objectName(w.prefix, seq, errSuffix), nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *walWriter) Close() error {
|
||||
w.mu.Lock()
|
||||
if w.closed {
|
||||
w.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
w.closed = true
|
||||
for len(w.buf) > 0 && w.err == nil {
|
||||
w.flushLocked()
|
||||
}
|
||||
w.mu.Unlock()
|
||||
|
||||
w.wg.Wait()
|
||||
|
||||
w.mu.Lock()
|
||||
err, seq := w.err, w.seq
|
||||
w.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return w.storage.Put(w.ctx, objectName(w.prefix, seq, endSuffix), nil)
|
||||
}
|
||||
|
||||
type walReader struct {
|
||||
ctx context.Context
|
||||
storage Storage
|
||||
prefix string
|
||||
params
|
||||
seq int64
|
||||
|
||||
ch chan []byte
|
||||
discards chan string
|
||||
wake chan struct{}
|
||||
holeSince time.Time
|
||||
|
||||
errMu sync.Mutex
|
||||
err error
|
||||
}
|
||||
|
||||
func newWALReader(ctx context.Context, storage Storage, prefix string, p params) *walReader {
|
||||
r := &walReader{
|
||||
ctx: ctx,
|
||||
storage: storage,
|
||||
prefix: prefix,
|
||||
params: p,
|
||||
ch: make(chan []byte, p.concurrency),
|
||||
discards: make(chan string, 4*p.concurrency),
|
||||
wake: make(chan struct{}, 1),
|
||||
}
|
||||
go r.run()
|
||||
go r.discardLoop()
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *walReader) Wake() {
|
||||
select {
|
||||
case r.wake <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (r *walReader) run() {
|
||||
defer close(r.ch)
|
||||
|
||||
delay := r.minPollInterval
|
||||
active := time.Now()
|
||||
for {
|
||||
polled := time.Now()
|
||||
advanced, eof, err := r.poll()
|
||||
if err != nil {
|
||||
r.setErr(err)
|
||||
return
|
||||
}
|
||||
if eof {
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case advanced:
|
||||
active = time.Now()
|
||||
delay = r.minPollInterval
|
||||
case time.Since(active) < r.eagerWindow:
|
||||
delay = r.minPollInterval
|
||||
default:
|
||||
delay *= 2
|
||||
if delay > r.maxPollInterval {
|
||||
delay = r.maxPollInterval
|
||||
}
|
||||
}
|
||||
|
||||
timer := time.NewTimer(delay)
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
case <-timer.C:
|
||||
case <-r.wake:
|
||||
timer.Stop()
|
||||
active = time.Now()
|
||||
delay = r.minPollInterval
|
||||
if rest := r.minPollInterval - time.Since(polled); rest > 0 {
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
return
|
||||
case <-time.After(rest):
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *walReader) poll() (advanced, eof bool, err error) {
|
||||
listed, err := r.storage.List(r.ctx, r.prefix)
|
||||
if err != nil {
|
||||
return false, false, err
|
||||
}
|
||||
if len(listed) == 0 {
|
||||
return false, false, nil
|
||||
}
|
||||
|
||||
pending := make(map[int64]Entry, len(listed))
|
||||
ahead := false
|
||||
for _, entry := range listed {
|
||||
seq, ok := parseEntry(entry.Name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
pending[seq] = entry
|
||||
if seq > r.seq {
|
||||
ahead = true
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := pending[r.seq]; !ok && ahead {
|
||||
if r.holeSince.IsZero() {
|
||||
r.holeSince = time.Now()
|
||||
} else if time.Since(r.holeSince) >= r.holeTimeout {
|
||||
return false, false, errors.New("segment ", r.seq,
|
||||
" never arrived while later ones did, the peer lost it")
|
||||
}
|
||||
} else {
|
||||
r.holeSince = time.Time{}
|
||||
}
|
||||
|
||||
for {
|
||||
if entry, ok := pending[r.seq]; ok && strings.HasSuffix(entry.Name, errSuffix) {
|
||||
r.discard(r.prefix + "/" + entry.Name)
|
||||
return advanced, false, errors.New("the peer could not store segment ", r.seq)
|
||||
}
|
||||
|
||||
batch, done := r.nextBatch(pending)
|
||||
if done {
|
||||
return advanced, true, nil
|
||||
}
|
||||
if len(batch) == 0 {
|
||||
return advanced, false, nil
|
||||
}
|
||||
|
||||
chunks, err := r.fetch(batch)
|
||||
if err != nil {
|
||||
if err == errNotFound {
|
||||
return advanced, false, nil
|
||||
}
|
||||
return advanced, false, err
|
||||
}
|
||||
|
||||
for i, chunk := range chunks {
|
||||
select {
|
||||
case r.ch <- chunk:
|
||||
case <-r.ctx.Done():
|
||||
return advanced, true, nil
|
||||
}
|
||||
r.seq++
|
||||
advanced = true
|
||||
r.discard(r.prefix + "/" + batch[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *walReader) nextBatch(pending map[int64]Entry) (batch []Entry, done bool) {
|
||||
for i := 0; i < r.concurrency; i++ {
|
||||
entry, ok := pending[r.seq+int64(i)]
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
if !strings.HasSuffix(entry.Name, segSuffix) {
|
||||
if i == 0 && strings.HasSuffix(entry.Name, endSuffix) {
|
||||
r.discard(r.prefix + "/" + entry.Name)
|
||||
return nil, true
|
||||
}
|
||||
break
|
||||
}
|
||||
batch = append(batch, entry)
|
||||
}
|
||||
return batch, false
|
||||
}
|
||||
|
||||
func (r *walReader) fetch(batch []Entry) ([][]byte, error) {
|
||||
chunks := make([][]byte, len(batch))
|
||||
failures := make([]error, len(batch))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i, entry := range batch {
|
||||
if entry.Inline != nil {
|
||||
chunks[i] = entry.Inline
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(i int, name string) {
|
||||
defer wg.Done()
|
||||
chunks[i], failures[i] = r.storage.Get(r.ctx, r.prefix+"/"+name)
|
||||
}(i, entry.Name)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for i := range batch {
|
||||
if failures[i] != nil {
|
||||
return nil, failures[i]
|
||||
}
|
||||
}
|
||||
return chunks, nil
|
||||
}
|
||||
|
||||
func (r *walReader) discard(name string) {
|
||||
select {
|
||||
case r.discards <- name:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (r *walReader) discardLoop() {
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
sem := make(chan struct{}, r.concurrency)
|
||||
for {
|
||||
select {
|
||||
case <-r.ctx.Done():
|
||||
return
|
||||
case name := <-r.discards:
|
||||
sem <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func(name string) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
r.storage.Delete(r.ctx, name)
|
||||
}(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *walReader) setErr(err error) {
|
||||
r.errMu.Lock()
|
||||
defer r.errMu.Unlock()
|
||||
if r.err == nil {
|
||||
r.err = err
|
||||
}
|
||||
}
|
||||
|
||||
func (r *walReader) Err() error {
|
||||
r.errMu.Lock()
|
||||
defer r.errMu.Unlock()
|
||||
if r.err != nil {
|
||||
return r.err
|
||||
}
|
||||
return io.EOF
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
)
|
||||
|
||||
const (
|
||||
protocolName = "xdrive"
|
||||
sessionsDir = "sessions"
|
||||
streamsDir = "streams"
|
||||
uplinkDir = "c2s"
|
||||
downlinkDir = "s2c"
|
||||
)
|
||||
|
||||
func init() {
|
||||
common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} {
|
||||
return new(Config)
|
||||
}))
|
||||
common.Must(internet.RegisterTransportDialer(protocolName, Dial))
|
||||
common.Must(internet.RegisterTransportListener(protocolName, Serve))
|
||||
}
|
||||
|
||||
func newSessionID() (string, error) {
|
||||
buf := make([]byte, 16)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", errors.New("failed to generate session id").Base(err)
|
||||
}
|
||||
return hex.EncodeToString(buf), nil
|
||||
}
|
||||
|
||||
func announceName(session string, at time.Time) string {
|
||||
return fmt.Sprintf("%s/%d-%s", sessionsDir, at.UnixNano(), session)
|
||||
}
|
||||
|
||||
func parseAnnounce(entry string) (string, time.Time, bool) {
|
||||
dash := strings.IndexByte(entry, '-')
|
||||
if dash <= 0 || dash == len(entry)-1 {
|
||||
return "", time.Time{}, false
|
||||
}
|
||||
nanos, err := strconv.ParseInt(entry[:dash], 10, 64)
|
||||
if err != nil {
|
||||
return "", time.Time{}, false
|
||||
}
|
||||
return entry[dash+1:], time.Unix(0, nanos), true
|
||||
}
|
||||
|
||||
func sessionPrefix(session string) string {
|
||||
return streamsDir + "/" + session
|
||||
}
|
||||
|
||||
func uplinkPrefix(session string) string {
|
||||
return sessionPrefix(session) + "/" + uplinkDir
|
||||
}
|
||||
|
||||
func downlinkPrefix(session string) string {
|
||||
return sessionPrefix(session) + "/" + downlinkDir
|
||||
}
|
||||
|
||||
func streamConfig(streamSettings *internet.MemoryStreamConfig) (*Config, error) {
|
||||
config, ok := streamSettings.ProtocolSettings.(*Config)
|
||||
if !ok || config == nil {
|
||||
return nil, errors.New("invalid protocol settings")
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) {
|
||||
config, err := streamConfig(streamSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
storage, err := newStorage(streamSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
session, err := newSessionID()
|
||||
if err != nil {
|
||||
storage.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := storage.Put(ctx, announceName(session, time.Now()), nil); err != nil {
|
||||
storage.Close()
|
||||
return nil, errors.New("failed to announce session ", session).Base(err)
|
||||
}
|
||||
|
||||
errors.LogInfo(ctx, "opened session ", session)
|
||||
|
||||
return newConn(context.Background(), storage,
|
||||
uplinkPrefix(session), downlinkPrefix(session), paramsFromConfig(config), func() {
|
||||
storage.Close()
|
||||
}), nil
|
||||
}
|
||||
|
||||
type Listener struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
storage Storage
|
||||
addConn internet.ConnHandler
|
||||
params
|
||||
|
||||
mu sync.Mutex
|
||||
active map[string]bool
|
||||
handled map[string]time.Time
|
||||
idleSince map[string]time.Time
|
||||
}
|
||||
|
||||
func Serve(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) {
|
||||
config, err := streamConfig(streamSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
storage, err := newStorage(streamSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listenerCtx, cancel := context.WithCancel(context.Background())
|
||||
listener := &Listener{
|
||||
ctx: listenerCtx,
|
||||
cancel: cancel,
|
||||
storage: storage,
|
||||
addConn: addConn,
|
||||
params: paramsFromConfig(config),
|
||||
active: make(map[string]bool),
|
||||
handled: make(map[string]time.Time),
|
||||
idleSince: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
go listener.acceptLoop(ctx)
|
||||
go listener.collectLoop(ctx)
|
||||
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
func (l *Listener) acceptLoop(logCtx context.Context) {
|
||||
delay := l.minPollInterval
|
||||
active := time.Now()
|
||||
for {
|
||||
accepted, err := l.acceptPending(logCtx)
|
||||
if err != nil {
|
||||
errors.LogWarningInner(logCtx, err, "failed to list sessions")
|
||||
}
|
||||
|
||||
switch {
|
||||
case accepted:
|
||||
active = time.Now()
|
||||
delay = l.minPollInterval
|
||||
case time.Since(active) < l.eagerWindow:
|
||||
delay = l.minPollInterval
|
||||
default:
|
||||
delay *= 2
|
||||
if delay > l.maxPollInterval {
|
||||
delay = l.maxPollInterval
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-l.ctx.Done():
|
||||
return
|
||||
case <-time.After(delay):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) acceptPending(logCtx context.Context) (bool, error) {
|
||||
sessions, err := l.storage.List(l.ctx, sessionsDir)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
accepted := false
|
||||
for _, listed := range sessions {
|
||||
entry := listed.Name
|
||||
full := sessionsDir + "/" + entry
|
||||
|
||||
session, at, ok := parseAnnounce(entry)
|
||||
if !ok {
|
||||
go l.drop(full)
|
||||
continue
|
||||
}
|
||||
if time.Since(at) > l.sessionTTL {
|
||||
errors.LogInfo(logCtx, "dropping the stale announcement of session ", session)
|
||||
go l.drop(full)
|
||||
go l.drop(sessionPrefix(session))
|
||||
continue
|
||||
}
|
||||
if !l.claim(session) {
|
||||
continue
|
||||
}
|
||||
go l.drop(full)
|
||||
errors.LogInfo(logCtx, "accepted session ", session)
|
||||
accepted = true
|
||||
l.addConn(l.newSessionConn(session))
|
||||
}
|
||||
return accepted, nil
|
||||
}
|
||||
|
||||
func (l *Listener) drop(name string) {
|
||||
l.storage.Delete(l.ctx, name)
|
||||
}
|
||||
|
||||
func (l *Listener) claim(session string) bool {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.active[session] || !l.handled[session].IsZero() {
|
||||
return false
|
||||
}
|
||||
l.active[session] = true
|
||||
l.handled[session] = time.Now()
|
||||
return true
|
||||
}
|
||||
|
||||
func (l *Listener) newSessionConn(session string) *Conn {
|
||||
return newConn(l.ctx, l.storage,
|
||||
downlinkPrefix(session), uplinkPrefix(session), l.params, func() {
|
||||
l.mu.Lock()
|
||||
delete(l.active, session)
|
||||
l.mu.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
func (l *Listener) collectLoop(logCtx context.Context) {
|
||||
interval := l.sessionTTL / 2
|
||||
for {
|
||||
select {
|
||||
case <-l.ctx.Done():
|
||||
return
|
||||
case <-time.After(interval):
|
||||
}
|
||||
if err := l.collect(); err != nil {
|
||||
errors.LogWarningInner(logCtx, err, "failed to collect abandoned sessions")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Listener) collect() error {
|
||||
sessions, err := l.storage.List(l.ctx, streamsDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var expired []string
|
||||
|
||||
l.mu.Lock()
|
||||
present := make(map[string]bool, len(sessions))
|
||||
for _, listed := range sessions {
|
||||
session := listed.Name
|
||||
present[session] = true
|
||||
if l.active[session] {
|
||||
delete(l.idleSince, session)
|
||||
continue
|
||||
}
|
||||
since, seen := l.idleSince[session]
|
||||
if !seen {
|
||||
l.idleSince[session] = now
|
||||
continue
|
||||
}
|
||||
if now.Sub(since) >= l.sessionTTL {
|
||||
expired = append(expired, session)
|
||||
delete(l.idleSince, session)
|
||||
}
|
||||
}
|
||||
for session := range l.idleSince {
|
||||
if !present[session] {
|
||||
delete(l.idleSince, session)
|
||||
}
|
||||
}
|
||||
for session, at := range l.handled {
|
||||
if !l.active[session] && now.Sub(at) >= l.sessionTTL {
|
||||
delete(l.handled, session)
|
||||
}
|
||||
}
|
||||
l.mu.Unlock()
|
||||
|
||||
for _, session := range expired {
|
||||
if err := l.storage.Delete(l.ctx, sessionPrefix(session)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) Addr() net.Addr {
|
||||
return placeholderAddr
|
||||
}
|
||||
|
||||
func (l *Listener) Close() error {
|
||||
l.cancel()
|
||||
return l.storage.Close()
|
||||
}
|
||||
@@ -0,0 +1,583 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
)
|
||||
|
||||
const testPatience = 30 * time.Second
|
||||
|
||||
func settings(folder string) *internet.MemoryStreamConfig {
|
||||
return &internet.MemoryStreamConfig{
|
||||
ProtocolName: protocolName,
|
||||
ProtocolSettings: &Config{
|
||||
RemoteFolder: folder,
|
||||
Service: "local",
|
||||
FlushIntervalMs: 5,
|
||||
PollIntervalMs: 5,
|
||||
MaxPollIntervalMs: 20,
|
||||
SessionTtlSeconds: 5,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func pair(t *testing.T) (client, server stat.Connection, cleanup func()) {
|
||||
t.Helper()
|
||||
return pairWith(t, settings(t.TempDir()))
|
||||
}
|
||||
|
||||
func pairWith(t *testing.T, streamSettings *internet.MemoryStreamConfig) (client, server stat.Connection, cleanup func()) {
|
||||
t.Helper()
|
||||
|
||||
accepted := make(chan stat.Connection, 1)
|
||||
|
||||
listener, err := Serve(context.Background(), net.LocalHostIP, net.Port(0), streamSettings, func(conn stat.Connection) {
|
||||
accepted <- conn
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Serve: %v", err)
|
||||
}
|
||||
|
||||
client, err = Dial(context.Background(), net.Destination{}, streamSettings)
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
t.Fatalf("Dial: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case server = <-accepted:
|
||||
case <-time.After(testPatience):
|
||||
client.Close()
|
||||
listener.Close()
|
||||
t.Fatal("listener did not accept the session")
|
||||
}
|
||||
|
||||
return client, server, func() {
|
||||
client.Close()
|
||||
server.Close()
|
||||
listener.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func expectRead(t *testing.T, conn stat.Connection, want string) {
|
||||
t.Helper()
|
||||
|
||||
if err := conn.SetReadDeadline(time.Now().Add(testPatience)); err != nil {
|
||||
t.Fatalf("SetReadDeadline: %v", err)
|
||||
}
|
||||
buf := make([]byte, len(want))
|
||||
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||
t.Fatalf("ReadFull: %v", err)
|
||||
}
|
||||
if string(buf) != want {
|
||||
t.Fatalf("read %q, want %q", buf, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
client, server, cleanup := pair(t)
|
||||
defer cleanup()
|
||||
|
||||
if _, err := client.Write([]byte("ping")); err != nil {
|
||||
t.Fatalf("client write: %v", err)
|
||||
}
|
||||
expectRead(t, server, "ping")
|
||||
|
||||
if _, err := server.Write([]byte("pong")); err != nil {
|
||||
t.Fatalf("server write: %v", err)
|
||||
}
|
||||
expectRead(t, client, "pong")
|
||||
}
|
||||
|
||||
func TestInterleaved(t *testing.T) {
|
||||
client, server, cleanup := pair(t)
|
||||
defer cleanup()
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
if _, err := client.Write([]byte("up")); err != nil {
|
||||
t.Fatalf("client write %d: %v", i, err)
|
||||
}
|
||||
expectRead(t, server, "up")
|
||||
|
||||
if _, err := server.Write([]byte("down")); err != nil {
|
||||
t.Fatalf("server write %d: %v", i, err)
|
||||
}
|
||||
expectRead(t, client, "down")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiSegmentTransfer(t *testing.T) {
|
||||
client, server, cleanup := pair(t)
|
||||
defer cleanup()
|
||||
|
||||
payload := make([]byte, 3*defaultSegmentBytes+1234)
|
||||
if _, err := rand.Read(payload); err != nil {
|
||||
t.Fatalf("rand: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
client.Write(payload)
|
||||
}()
|
||||
|
||||
if err := server.SetReadDeadline(time.Now().Add(30 * time.Second)); err != nil {
|
||||
t.Fatalf("SetReadDeadline: %v", err)
|
||||
}
|
||||
got := make([]byte, len(payload))
|
||||
if _, err := io.ReadFull(server, got); err != nil {
|
||||
t.Fatalf("ReadFull: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatal("payload mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloseEOF(t *testing.T) {
|
||||
client, server, cleanup := pair(t)
|
||||
defer cleanup()
|
||||
|
||||
if _, err := client.Write([]byte("bye")); err != nil {
|
||||
t.Fatalf("client write: %v", err)
|
||||
}
|
||||
if err := client.Close(); err != nil {
|
||||
t.Fatalf("client close: %v", err)
|
||||
}
|
||||
|
||||
if err := server.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
||||
t.Fatalf("SetReadDeadline: %v", err)
|
||||
}
|
||||
got, err := io.ReadAll(server)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadAll: %v", err)
|
||||
}
|
||||
if string(got) != "bye" {
|
||||
t.Fatalf("read %q, want %q", got, "bye")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadDeadline(t *testing.T) {
|
||||
client, _, cleanup := pair(t)
|
||||
defer cleanup()
|
||||
|
||||
if err := client.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
||||
t.Fatalf("SetReadDeadline: %v", err)
|
||||
}
|
||||
buf := make([]byte, 4)
|
||||
if _, err := client.Read(buf); !os.IsTimeout(err) {
|
||||
t.Fatalf("Read returned %v, want a timeout", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalNameEscape(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
storage, err := newLocalStorage(root)
|
||||
if err != nil {
|
||||
t.Fatalf("newLocalStorage: %v", err)
|
||||
}
|
||||
|
||||
if err := storage.Put(context.Background(), "../escaped", []byte("x")); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(root + "/escaped"); err != nil {
|
||||
t.Fatalf("name was not clamped inside the root: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalMissingObject(t *testing.T) {
|
||||
storage, err := newLocalStorage(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("newLocalStorage: %v", err)
|
||||
}
|
||||
|
||||
if _, err := storage.Get(context.Background(), "nothing/here"); err != errNotFound {
|
||||
t.Fatalf("Get returned %v, want errNotFound", err)
|
||||
}
|
||||
names, err := storage.List(context.Background(), "nothing")
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(names) != 0 {
|
||||
t.Fatalf("List returned %v, want none", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResumeAfterIdle(t *testing.T) {
|
||||
client, server, cleanup := pair(t)
|
||||
defer cleanup()
|
||||
|
||||
if _, err := client.Write([]byte("first")); err != nil {
|
||||
t.Fatalf("client write: %v", err)
|
||||
}
|
||||
expectRead(t, server, "first")
|
||||
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
if _, err := client.Write([]byte("second")); err != nil {
|
||||
t.Fatalf("client write: %v", err)
|
||||
}
|
||||
expectRead(t, server, "second")
|
||||
}
|
||||
|
||||
func TestParseEntry(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
seq int64
|
||||
ok bool
|
||||
}{
|
||||
{"000000000.seg", 0, true},
|
||||
{"000000042.seg", 42, true},
|
||||
{"000000007.end", 7, true},
|
||||
{"000000001.tmp", 0, false},
|
||||
{"notanumber.seg", 0, false},
|
||||
{"000000001", 0, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
seq, ok := parseEntry(c.name)
|
||||
if ok != c.ok || (ok && seq != c.seq) {
|
||||
t.Fatalf("parseEntry(%q) = %d, %v; want %d, %v", c.name, seq, ok, c.seq, c.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParamDefaults(t *testing.T) {
|
||||
p := paramsFromConfig(&Config{})
|
||||
if p.segmentBytes != defaultSegmentBytes || p.flushInterval != defaultFlushInterval {
|
||||
t.Fatalf("defaults not applied: %+v", p)
|
||||
}
|
||||
|
||||
p = paramsFromConfig(&Config{SegmentBytes: 1 << 30, PollIntervalMs: 400, MaxPollIntervalMs: 100})
|
||||
if p.segmentBytes != maxSegmentBytes {
|
||||
t.Fatalf("segmentBytes is %d, want %d", p.segmentBytes, maxSegmentBytes)
|
||||
}
|
||||
if p.maxPollInterval < p.minPollInterval {
|
||||
t.Fatalf("maxPollInterval %v below minPollInterval %v", p.maxPollInterval, p.minPollInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func waitFor(t *testing.T, what string, done func() bool) {
|
||||
t.Helper()
|
||||
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if done() {
|
||||
return
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("timed out waiting for %s", what)
|
||||
}
|
||||
|
||||
func newTestListener(t *testing.T, folder string) *Listener {
|
||||
t.Helper()
|
||||
|
||||
storage, err := newLocalStorage(folder)
|
||||
if err != nil {
|
||||
t.Fatalf("newLocalStorage: %v", err)
|
||||
}
|
||||
return &Listener{
|
||||
ctx: context.Background(),
|
||||
storage: storage,
|
||||
params: paramsFromConfig(&Config{SessionTtlSeconds: 1}),
|
||||
active: make(map[string]bool),
|
||||
handled: make(map[string]time.Time),
|
||||
idleSince: make(map[string]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectAbandoned(t *testing.T) {
|
||||
folder := t.TempDir()
|
||||
listener := newTestListener(t, folder)
|
||||
|
||||
if err := listener.storage.Put(context.Background(), uplinkPrefix("dead")+"/000000000.seg", []byte("x")); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
|
||||
if err := listener.collect(); err != nil {
|
||||
t.Fatalf("collect: %v", err)
|
||||
}
|
||||
names, _ := listener.storage.List(context.Background(), streamsDir)
|
||||
if len(names) != 1 {
|
||||
t.Fatalf("first pass removed the session, got %v", names)
|
||||
}
|
||||
|
||||
listener.idleSince["dead"] = time.Now().Add(-2 * time.Second)
|
||||
if err := listener.collect(); err != nil {
|
||||
t.Fatalf("collect: %v", err)
|
||||
}
|
||||
names, _ = listener.storage.List(context.Background(), streamsDir)
|
||||
if len(names) != 0 {
|
||||
t.Fatalf("abandoned session still there, got %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectKeepsActive(t *testing.T) {
|
||||
folder := t.TempDir()
|
||||
listener := newTestListener(t, folder)
|
||||
listener.active["live"] = true
|
||||
|
||||
if err := listener.storage.Put(context.Background(), uplinkPrefix("live")+"/000000000.seg", []byte("x")); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
|
||||
listener.idleSince["live"] = time.Now().Add(-2 * time.Second)
|
||||
if err := listener.collect(); err != nil {
|
||||
t.Fatalf("collect: %v", err)
|
||||
}
|
||||
names, _ := listener.storage.List(context.Background(), streamsDir)
|
||||
if len(names) != 1 {
|
||||
t.Fatalf("collected an active session, got %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAnnounce(t *testing.T) {
|
||||
session, at, ok := parseAnnounce("1757000000123456789-abc123")
|
||||
if !ok || session != "abc123" || at.UnixNano() != 1757000000123456789 {
|
||||
t.Fatalf("parseAnnounce returned %q, %v, %v", session, at.UnixNano(), ok)
|
||||
}
|
||||
for _, bad := range []string{"abc123", "-abc123", "1757000000-", "notanumber-abc"} {
|
||||
if _, _, ok := parseAnnounce(bad); ok {
|
||||
t.Fatalf("parseAnnounce accepted %q", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaleAnnounce(t *testing.T) {
|
||||
folder := t.TempDir()
|
||||
listener := newTestListener(t, folder)
|
||||
|
||||
ctx := context.Background()
|
||||
stale := announceName("ghost", time.Now().Add(-time.Hour))
|
||||
if err := listener.storage.Put(ctx, stale, nil); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
if err := listener.storage.Put(ctx, uplinkPrefix("ghost")+"/000000000.seg", []byte("x")); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
|
||||
accepted, err := listener.acceptPending(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("acceptPending: %v", err)
|
||||
}
|
||||
if accepted {
|
||||
t.Fatal("accepted a stale announcement")
|
||||
}
|
||||
|
||||
waitFor(t, "the stale announcement to be removed", func() bool {
|
||||
names, _ := listener.storage.List(ctx, sessionsDir)
|
||||
return len(names) == 0
|
||||
})
|
||||
waitFor(t, "the stale session data to be removed", func() bool {
|
||||
names, _ := listener.storage.List(ctx, streamsDir)
|
||||
return len(names) == 0
|
||||
})
|
||||
}
|
||||
|
||||
func TestFreshAnnounce(t *testing.T) {
|
||||
folder := t.TempDir()
|
||||
listener := newTestListener(t, folder)
|
||||
listener.addConn = func(conn stat.Connection) { conn.Close() }
|
||||
|
||||
ctx := context.Background()
|
||||
if err := listener.storage.Put(ctx, announceName("fresh", time.Now()), nil); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
|
||||
accepted, err := listener.acceptPending(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("acceptPending: %v", err)
|
||||
}
|
||||
if !accepted {
|
||||
t.Fatal("did not accept a fresh announcement")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnouncePrecision(t *testing.T) {
|
||||
at := time.Unix(1757000000, int64(900*time.Millisecond))
|
||||
entry := strings.TrimPrefix(announceName("abc123", at), sessionsDir+"/")
|
||||
|
||||
session, parsed, ok := parseAnnounce(entry)
|
||||
if !ok || session != "abc123" {
|
||||
t.Fatalf("parseAnnounce(%q) returned %q, %v", entry, session, ok)
|
||||
}
|
||||
if !parsed.Equal(at) {
|
||||
t.Fatalf("timestamp came back as %v, want %v", parsed, at)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecentAnnounceTTL(t *testing.T) {
|
||||
folder := t.TempDir()
|
||||
listener := newTestListener(t, folder)
|
||||
listener.addConn = func(conn stat.Connection) { conn.Close() }
|
||||
|
||||
ctx := context.Background()
|
||||
recent := time.Now().Add(-900 * time.Millisecond)
|
||||
if err := listener.storage.Put(ctx, announceName("recent", recent), nil); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
|
||||
accepted, err := listener.acceptPending(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("acceptPending: %v", err)
|
||||
}
|
||||
if !accepted {
|
||||
t.Fatal("dropped an announcement younger than the TTL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingSegment(t *testing.T) {
|
||||
storage, err := newLocalStorage(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("newLocalStorage: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
p := paramsFromConfig(&Config{PollIntervalMs: 5, MaxPollIntervalMs: 20, HoleTimeoutMs: 200})
|
||||
if err := storage.Put(ctx, objectName("hole", 1, segSuffix), []byte("second")); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
|
||||
reader := newWALReader(ctx, storage, "hole", p)
|
||||
select {
|
||||
case _, ok := <-reader.ch:
|
||||
if ok {
|
||||
t.Fatal("delivered data past a missing segment")
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("reader did not give up on a missing segment")
|
||||
}
|
||||
|
||||
err = reader.Err()
|
||||
if err == nil || err == io.EOF {
|
||||
t.Fatalf("Err returned %v, want a failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdleStreamWaits(t *testing.T) {
|
||||
storage, err := newLocalStorage(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("newLocalStorage: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
p := paramsFromConfig(&Config{PollIntervalMs: 5, MaxPollIntervalMs: 20, HoleTimeoutMs: 100})
|
||||
reader := newWALReader(ctx, storage, "idle", p)
|
||||
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
if err := storage.Put(ctx, objectName("idle", 0, segSuffix), []byte("late")); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case data, ok := <-reader.ch:
|
||||
if !ok {
|
||||
t.Fatalf("the reader gave up on an idle stream: %v", reader.Err())
|
||||
}
|
||||
if string(data) != "late" {
|
||||
t.Fatalf("read %q, want %q", data, "late")
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("reader missed a late segment")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailureMarker(t *testing.T) {
|
||||
storage, err := newLocalStorage(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("newLocalStorage: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
p := paramsFromConfig(&Config{PollIntervalMs: 5, MaxPollIntervalMs: 20})
|
||||
if err := storage.Put(ctx, objectName("broken", 0, segSuffix), []byte("first")); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
if err := storage.Put(ctx, objectName("broken", 1, errSuffix), nil); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
|
||||
reader := newWALReader(ctx, storage, "broken", p)
|
||||
|
||||
select {
|
||||
case data, ok := <-reader.ch:
|
||||
if !ok || string(data) != "first" {
|
||||
t.Fatalf("want the segment before the marker, got %q %v", data, ok)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("reader did not deliver the first segment")
|
||||
}
|
||||
|
||||
select {
|
||||
case _, ok := <-reader.ch:
|
||||
if ok {
|
||||
t.Fatal("delivered data past the failure marker")
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("reader did not stop on the failure marker")
|
||||
}
|
||||
|
||||
if err := reader.Err(); err == nil || err == io.EOF {
|
||||
t.Fatalf("Err returned %v, want a failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
type inlineOnlyStorage struct {
|
||||
Storage
|
||||
gets int64
|
||||
}
|
||||
|
||||
func (s *inlineOnlyStorage) List(ctx context.Context, prefix string) ([]Entry, error) {
|
||||
return []Entry{{Name: "000000000" + segSuffix, Inline: []byte("carried by the listing")}}, nil
|
||||
}
|
||||
|
||||
func (s *inlineOnlyStorage) Get(ctx context.Context, name string) ([]byte, error) {
|
||||
atomic.AddInt64(&s.gets, 1)
|
||||
return nil, errNotFound
|
||||
}
|
||||
|
||||
func (s *inlineOnlyStorage) Delete(ctx context.Context, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestInlinePayload(t *testing.T) {
|
||||
base, err := newLocalStorage(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("newLocalStorage: %v", err)
|
||||
}
|
||||
storage := &inlineOnlyStorage{Storage: base}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
reader := newWALReader(ctx, storage, "inline", paramsFromConfig(&Config{PollIntervalMs: 5}))
|
||||
select {
|
||||
case data, ok := <-reader.ch:
|
||||
if !ok {
|
||||
t.Fatalf("reader stopped: %v", reader.Err())
|
||||
}
|
||||
if string(data) != "carried by the listing" {
|
||||
t.Fatalf("read %q, want the inline payload", data)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("reader did not deliver the inline payload")
|
||||
}
|
||||
|
||||
if got := atomic.LoadInt64(&storage.gets); got != 0 {
|
||||
t.Fatalf("called Get %d times for an inline payload", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
const yandexBase = "https://webdav.yandex.ru"
|
||||
|
||||
func envUint(name string, def uint32) uint32 {
|
||||
if v := os.Getenv(name); v != "" {
|
||||
var n uint32
|
||||
fmt.Sscanf(v, "%d", &n)
|
||||
if n > 0 {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func liveYandexSettings(t *testing.T) (*internet.MemoryStreamConfig, string, func()) {
|
||||
t.Helper()
|
||||
|
||||
user := os.Getenv("XDRIVE_YANDEX_USER")
|
||||
pass := os.Getenv("XDRIVE_YANDEX_PASS")
|
||||
if user == "" || pass == "" {
|
||||
t.Skip("set XDRIVE_YANDEX_USER and XDRIVE_YANDEX_PASS to run this test")
|
||||
}
|
||||
|
||||
folder := fmt.Sprintf("xdrive-live-%d", time.Now().UnixNano())
|
||||
dav := func(method, path string) int {
|
||||
req, _ := http.NewRequest(method, yandexBase+path, nil)
|
||||
req.SetBasicAuth(user, pass)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("%s %s: %v", method, path, err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
return resp.StatusCode
|
||||
}
|
||||
if code := dav("MKCOL", "/"+folder); code != 201 && code != 405 {
|
||||
t.Fatalf("MKCOL answered %d", code)
|
||||
}
|
||||
|
||||
tmpl := map[string]interface{}{
|
||||
"flatten": true,
|
||||
"auth": map[string]interface{}{"type": "basic", "username": "{secret0}", "password": "{secret1}"},
|
||||
"put": map[string]interface{}{"method": "PUT", "url": yandexBase + "/{folder}/{name}"},
|
||||
"get": map[string]interface{}{"method": "GET", "url": yandexBase + "/{folder}/{name}"},
|
||||
"delete": map[string]interface{}{"method": "DELETE", "url": yandexBase + "/{folder}/{name}"},
|
||||
"list": map[string]interface{}{
|
||||
"method": "PROPFIND", "url": yandexBase + "/{folder}/",
|
||||
"headers": map[string]interface{}{"Depth": "1"}, "namesRegex": `<d:href>[^<]*/([^/<]+)</d:href>`,
|
||||
},
|
||||
"retry": map[string]interface{}{"status": []int{429, 500, 502, 503}},
|
||||
}
|
||||
raw, _ := json.Marshal(tmpl)
|
||||
settings := &internet.MemoryStreamConfig{
|
||||
ProtocolName: protocolName,
|
||||
ProtocolSettings: &Config{
|
||||
RemoteFolder: folder,
|
||||
Service: "template",
|
||||
Secrets: []string{user, pass},
|
||||
Template: string(raw),
|
||||
SegmentBytes: 262144,
|
||||
FlushIntervalMs: 100,
|
||||
PollIntervalMs: 300,
|
||||
MaxPollIntervalMs: 1500,
|
||||
SessionTtlSeconds: 120,
|
||||
Concurrency: envUint("XDRIVE_LIVE_CONCURRENCY", 8),
|
||||
},
|
||||
}
|
||||
cleanup := func() { dav("DELETE", "/"+folder) }
|
||||
return settings, folder, cleanup
|
||||
}
|
||||
|
||||
func TestLiveYandexStorage(t *testing.T) {
|
||||
settings, _, cleanup := liveYandexSettings(t)
|
||||
defer cleanup()
|
||||
|
||||
storage, err := newTemplateStorage(settings, settings.ProtocolSettings.(*Config))
|
||||
if err != nil {
|
||||
t.Fatalf("newTemplateStorage: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
name := "streams/live/c2s/000000000.seg"
|
||||
payload := []byte("xdrive over real yandex webdav")
|
||||
|
||||
start := time.Now()
|
||||
if err := storage.Put(ctx, name, payload); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
t.Logf("Put took %v", time.Since(start))
|
||||
|
||||
start = time.Now()
|
||||
names, err := storage.List(ctx, "streams/live/c2s")
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
t.Logf("List took %v", time.Since(start))
|
||||
if len(names) != 1 || names[0].Name != "000000000.seg" {
|
||||
t.Fatalf("List returned %v, want one segment", names)
|
||||
}
|
||||
|
||||
start = time.Now()
|
||||
got, err := storage.Get(ctx, name)
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
t.Logf("Get took %v", time.Since(start))
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatalf("Get returned %q", got)
|
||||
}
|
||||
|
||||
if _, err := storage.Get(ctx, "streams/live/c2s/000000009.seg"); err != errNotFound {
|
||||
t.Fatalf("Get of a missing object returned %v, want errNotFound", err)
|
||||
}
|
||||
|
||||
if err := storage.Delete(ctx, name); err != nil {
|
||||
t.Fatalf("Delete: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveYandexTransport(t *testing.T) {
|
||||
settings, _, cleanup := liveYandexSettings(t)
|
||||
defer cleanup()
|
||||
|
||||
client, server, done := pairWith(t, settings)
|
||||
defer done()
|
||||
|
||||
start := time.Now()
|
||||
if _, err := client.Write([]byte("ping")); err != nil {
|
||||
t.Fatalf("client write: %v", err)
|
||||
}
|
||||
expectRead(t, server, "ping")
|
||||
t.Logf("client to server round took %v", time.Since(start))
|
||||
|
||||
size := 1000000
|
||||
if raw := os.Getenv("XDRIVE_LIVE_BYTES"); raw != "" {
|
||||
fmt.Sscanf(raw, "%d", &size)
|
||||
}
|
||||
payload := make([]byte, size)
|
||||
rand.Read(payload)
|
||||
|
||||
start = time.Now()
|
||||
go func() { client.Write(payload) }()
|
||||
if err := server.SetReadDeadline(time.Now().Add(5 * time.Minute)); err != nil {
|
||||
t.Fatalf("SetReadDeadline: %v", err)
|
||||
}
|
||||
got := make([]byte, len(payload))
|
||||
if _, err := io.ReadFull(server, got); err != nil {
|
||||
t.Fatalf("ReadFull: %v", err)
|
||||
}
|
||||
elapsed := time.Since(start)
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatal("payload mismatch")
|
||||
}
|
||||
t.Logf("%d bytes in %v -> %.1f KiB/s", len(payload), elapsed,
|
||||
float64(len(payload))/1024/elapsed.Seconds())
|
||||
}
|
||||
@@ -63,8 +63,8 @@ func New(opts ...Option) (*Reader, *Writer) {
|
||||
}
|
||||
|
||||
return &Reader{
|
||||
pipe: p,
|
||||
}, &Writer{
|
||||
pipe: p,
|
||||
}
|
||||
pipe: p,
|
||||
}, &Writer{
|
||||
pipe: p,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user