mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-10-20 08:14:26 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab507b7a0d | ||
|
|
969e5b6c11 |
@@ -109,7 +109,7 @@ func (h *HealthPingRTTS) getStatistics() *HealthPingStats {
|
||||
stats.Min = 0
|
||||
return stats
|
||||
}
|
||||
stats.Average = sum / time.Duration(cnt)
|
||||
stats.Average = time.Duration(int(sum) / cnt)
|
||||
var std float64
|
||||
if cnt < 2 {
|
||||
// no enough data for standard deviation, we assume it's half of the average rtt
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import (
|
||||
var (
|
||||
Version_x byte = 26
|
||||
Version_y byte = 10
|
||||
Version_z byte = 17
|
||||
Version_z byte = 10
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -15,13 +15,11 @@ import (
|
||||
|
||||
googleuuid "github.com/google/uuid"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
xlua "github.com/xtls/xray-core/common/lua"
|
||||
"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/ipfilter"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/aes128gcm"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/header"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/mkcp/original"
|
||||
@@ -76,7 +74,6 @@ var (
|
||||
customVarNamePattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||
|
||||
tcpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||
"ip-filter": func() interface{} { return new(IPFilter) },
|
||||
"header-custom": func() interface{} { return new(HeaderCustomTCP) },
|
||||
"fragment": func() interface{} { return new(FragmentMask) },
|
||||
"sudoku": func() interface{} { return new(Sudoku) },
|
||||
@@ -84,7 +81,6 @@ var (
|
||||
}, "type", "settings")
|
||||
|
||||
udpmaskLoader = NewJSONConfigLoader(ConfigCreatorCache{
|
||||
"ip-filter": func() interface{} { return new(IPFilter) },
|
||||
"header-custom": func() interface{} { return new(HeaderCustomUDP) },
|
||||
"mkcp-legacy": func() interface{} { return new(MkcpLegacy) },
|
||||
"noise": func() interface{} { return new(NoiseMask) },
|
||||
@@ -97,18 +93,6 @@ var (
|
||||
}, "type", "settings")
|
||||
)
|
||||
|
||||
type IPFilter struct {
|
||||
BlockedIPs []string `json:"blockedIPs"`
|
||||
}
|
||||
|
||||
func (c *IPFilter) Build() (proto.Message, error) {
|
||||
rules, err := geodata.ParseIPRules(c.BlockedIPs)
|
||||
if err != nil {
|
||||
return nil, errors.New(`invalid "blockedIPs"`).Base(err)
|
||||
}
|
||||
return &ipfilter.Config{BlockedIps: rules}, nil
|
||||
}
|
||||
|
||||
type TCPItem struct {
|
||||
Delay Int32Range `json:"delay"`
|
||||
Rand int32 `json:"rand"`
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package conf_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
. "github.com/xtls/xray-core/infra/conf"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask/ipfilter"
|
||||
)
|
||||
|
||||
func TestFinalMaskIPFilter(t *testing.T) {
|
||||
for _, network := range []string{"tcp", "udp"} {
|
||||
t.Run(network, func(t *testing.T) {
|
||||
input := fmt.Sprintf(`{"finalmask":{"%s":[{"type":"ip-filter","settings":{"blockedIPs":["192.0.2.0/24"]}}]}}`, network)
|
||||
var config StreamConfig
|
||||
if err := json.Unmarshal([]byte(input), &config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
built, err := config.Build()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
masks := built.Tcpmasks
|
||||
if network == "udp" {
|
||||
masks = built.Udpmasks
|
||||
}
|
||||
if len(masks) != 1 {
|
||||
t.Fatalf("expected one mask, got %d", len(masks))
|
||||
}
|
||||
instance, err := masks[0].GetInstance()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
filter, ok := instance.(*ipfilter.Config)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected mask type: %T", instance)
|
||||
}
|
||||
if len(filter.BlockedIps) != 1 {
|
||||
t.Fatalf("expected one blocked IP rule, got %d", len(filter.BlockedIps))
|
||||
}
|
||||
cidr := filter.BlockedIps[0].GetCustom().GetCidr()
|
||||
if cidr.GetPrefix() != 24 || !net.IP(cidr.GetIp()).Equal(net.IPv4(192, 0, 2, 0)) {
|
||||
t.Fatalf("unexpected blocked IP rule: %v", cidr)
|
||||
}
|
||||
if _, err := internet.ToMemoryStreamConfig(built); err != nil {
|
||||
t.Fatalf("failed to initialize runtime finalmask: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
if _, err := (&IPFilter{BlockedIPs: []string{"192.0.2.0/33"}}).Build(); err == nil || !strings.Contains(err.Error(), "blockedIPs") {
|
||||
t.Errorf("expected invalid blockedIPs, got %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
utls "github.com/refraction-networking/utls"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/serial"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
)
|
||||
|
||||
type TransportProtocol string
|
||||
@@ -104,14 +100,6 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
|
||||
tm := serial.ToTypedMessage(ts)
|
||||
config.SecuritySettings = append(config.SecuritySettings, tm)
|
||||
config.SecurityType = tm.Type
|
||||
if config.ProtocolName == "xdrive" && tlsSettings.ALPN != nil && slices.Equal(*tlsSettings.ALPN, StringList{"http/1.1"}) && tlsSettings.ECHConfigList == "" {
|
||||
// a fingerprint puts its own ALPN into the hello, except Go TLS and the
|
||||
// randomized ones, which take it from the config or send none
|
||||
if fp := tls.GetFingerprint(strings.ToLower(tlsSettings.Fingerprint)); fp != nil && fp.Client != utls.HelloGolang.Client &&
|
||||
!strings.HasPrefix(fp.Client, utls.HelloRandomized.Client) {
|
||||
errors.LogWarning(context.Background(), `XDRIVE: "alpn" ["http/1.1"] reaches the wire only inside ECH, without "echConfigList" the hello carries the ALPN of the fingerprint and the server may pick h2`)
|
||||
}
|
||||
}
|
||||
case "reality":
|
||||
if config.ProtocolName != "tcp" && config.ProtocolName != "splithttp" && config.ProtocolName != "grpc" {
|
||||
return nil, errors.New("REALITY only supports RAW, XHTTP and gRPC for now.")
|
||||
|
||||
@@ -1004,10 +1004,6 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
|
||||
switch c.Service {
|
||||
case "local":
|
||||
case "Google Drive":
|
||||
errors.PrintDeprecatedFeatureWarning(`"service": "Google Drive"`, `"service": "google-drive"`)
|
||||
c.Service = "google-drive"
|
||||
fallthrough
|
||||
case "google-drive":
|
||||
if len(c.Secrets) != 3 {
|
||||
return nil, errors.New("Google Drive needs 3 secrets in order of ClientID, ClientSecret, RefreshToken")
|
||||
}
|
||||
@@ -1016,7 +1012,7 @@ func (c *XDriveConfig) Build() (proto.Message, error) {
|
||||
return nil, errors.New(`service "template" needs a "template" object`)
|
||||
}
|
||||
default:
|
||||
return nil, errors.New(`unknown "service": "`, c.Service, `", use "google-drive", "template" or "local"`)
|
||||
return nil, errors.New("unsupported service")
|
||||
}
|
||||
config := &xdrive.Config{
|
||||
RemoteFolder: c.RemoteFolder,
|
||||
|
||||
@@ -5,11 +5,9 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/xtls/xray-core/common/log"
|
||||
. "github.com/xtls/xray-core/infra/conf"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
finalmaskcustom "github.com/xtls/xray-core/transport/internet/finalmask/header/custom"
|
||||
"github.com/xtls/xray-core/transport/internet/xdrive"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
@@ -323,25 +321,8 @@ func TestXDriveRejectsUnknownService(t *testing.T) {
|
||||
if err := json.Unmarshal([]byte(`{"remoteFolder": "/tmp/xdrive", "service": "Dropbox"}`), config); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if _, err := config.Build(); err == nil || !strings.Contains(err.Error(), `use "google-drive", "template" or "local"`) {
|
||||
t.Fatalf("expected an unknown service error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDriveGoogleDriveName(t *testing.T) {
|
||||
for _, service := range []string{"google-drive", "Google Drive"} {
|
||||
config := &XDriveConfig{RemoteFolder: "folder-id", Service: service, Secrets: []string{"a", "b", "c"}}
|
||||
built, err := config.Build()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", service, err)
|
||||
}
|
||||
if got := built.(*xdrive.Config).Service; got != "google-drive" {
|
||||
t.Errorf("%s: service is %q, want google-drive", service, got)
|
||||
}
|
||||
config = &XDriveConfig{RemoteFolder: "folder-id", Service: service, Secrets: []string{"a", "b"}}
|
||||
if _, err := config.Build(); err == nil {
|
||||
t.Errorf("%s: accepted two secrets", service)
|
||||
}
|
||||
if _, err := config.Build(); err == nil {
|
||||
t.Fatal("Build accepted an unsupported service")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,56 +355,6 @@ func TestXDriveTemplateStreamConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type logLines []string
|
||||
|
||||
func (l *logLines) Handle(msg log.Message) {
|
||||
*l = append(*l, msg.String())
|
||||
}
|
||||
|
||||
// "alpn" ["http/1.1"] is warned about where the hello does not carry it, which
|
||||
// is outside ECH with a fingerprint that has an ALPN of its own
|
||||
func TestXDriveHTTP1ALPNWarning(t *testing.T) {
|
||||
var lines logLines
|
||||
log.RegisterHandler(&lines)
|
||||
|
||||
for _, c := range []struct {
|
||||
method, tlsSettings string
|
||||
warn bool
|
||||
}{
|
||||
{"xdrive", `{}`, false},
|
||||
{"xdrive", `{"alpn": ["http/1.1"]}`, true},
|
||||
{"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "Firefox"}`, true},
|
||||
{"raw", `{"alpn": ["http/1.1"]}`, false},
|
||||
{"xdrive", `{"alpn": ["h2", "http/1.1"]}`, false},
|
||||
{"xdrive", `{"alpn": ["http/1.1"], "echConfigList": "udp://1.1.1.1"}`, false},
|
||||
{"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "unsafe"}`, false},
|
||||
{"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "hellogolang"}`, false},
|
||||
{"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "randomized"}`, false},
|
||||
{"xdrive", `{"alpn": ["http/1.1"], "fingerprint": "hellorandomizednoalpn"}`, false},
|
||||
} {
|
||||
lines = nil
|
||||
config := new(StreamConfig)
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"method": "`+c.method+`",
|
||||
"security": "tls",
|
||||
"tlsSettings": `+c.tlsSettings+`,
|
||||
"xdriveSettings": {"remoteFolder": "/tmp/xdrive", "service": "local"}
|
||||
}`), config); err != nil {
|
||||
t.Fatalf("Unmarshal: %v", err)
|
||||
}
|
||||
if _, err := config.Build(); err != nil {
|
||||
t.Fatalf("%s %s: %v", c.method, c.tlsSettings, err)
|
||||
}
|
||||
warned := false
|
||||
for _, line := range lines {
|
||||
warned = warned || strings.HasPrefix(line, "[Warning]") && strings.Contains(line, `"alpn"`)
|
||||
}
|
||||
if warned != c.warn {
|
||||
t.Errorf("%s %s: warned %v, want %v", c.method, c.tlsSettings, warned, c.warn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDriveTemplateNeedsTemplate(t *testing.T) {
|
||||
config := new(XDriveConfig)
|
||||
if err := json.Unmarshal([]byte(`{"remoteFolder": "f", "service": "template"}`), config); err != nil {
|
||||
@@ -433,83 +364,3 @@ func TestXDriveTemplateNeedsTemplate(t *testing.T) {
|
||||
t.Fatal("Build accepted a template service without a template")
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
xdriveStream = `"streamSettings": {"network": "xdrive", "xdriveSettings": {"remoteFolder": "f", "service": "local"}}`
|
||||
xdriveID = `"id": "27848739-7e62-4138-9fd3-098a63964b6b"`
|
||||
xdriveServer = `"address": "127.0.0.1", "port": 443, ` + xdriveID
|
||||
xdriveEnc = `"encryption": "mlkem768x25519plus.native.0rtt.XKbaq1BuzJKKKPxZbwgRpn-gYQbnhrYketgezASCL1I"`
|
||||
xdriveDec = `"decryption": "mlkem768x25519plus.native.600s.UKfUEMDdjlEnycTMBP0a08acky8vRRKSrM1MRnaYk3s"`
|
||||
)
|
||||
|
||||
func TestXDriveOutboundConfig(t *testing.T) {
|
||||
build := func(s string) error {
|
||||
c := new(OutboundDetourConfig)
|
||||
if err := json.Unmarshal([]byte(s), c); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.Build()
|
||||
return err
|
||||
}
|
||||
|
||||
for _, input := range []string{
|
||||
`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true}}`,
|
||||
`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": 0, "xudpConcurrency": 8, "xudpProxyUDP443": "allow"}}`,
|
||||
`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `, "reverse": {"tag": "r"}}, ` + xdriveStream + `}`,
|
||||
`{"protocol": "vless", "settings": {"vnext": [{"address": "127.0.0.1", "port": 443, "users": [{` + xdriveID + `, ` + xdriveEnc + `}]}]}, ` + xdriveStream + `, "mux": {"enabled": true}}`,
|
||||
} {
|
||||
if err := build(input); err != nil {
|
||||
t.Errorf("%s: %v", input, err)
|
||||
}
|
||||
}
|
||||
for _, c := range []struct{ input, err string }{
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, "encryption": "none"}, ` + xdriveStream + `, "mux": {"enabled": true}}`, `"encryption"`},
|
||||
{`{"protocol": "vless", "settings": {"address": "example.com", "port": 443, ` + xdriveID + `, "encryption": "none"}, ` + xdriveStream + `, "mux": {"enabled": true}}`, `"encryption"`},
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `}`, `"mux"`},
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": false}}`, `"mux"`},
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": -1}}`, `"mux"`},
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "xudpConcurrency": -1}}`, `"mux"`},
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `}, ` + xdriveStream + `, "mux": {"enabled": true, "xudpProxyUDP443": "skip"}}`, `"mux"`},
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `, "flow": "xtls-rprx-vision"}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": -1}}`, `"flow"`},
|
||||
{`{"protocol": "vless", "settings": {"vnext": [{"address": "127.0.0.1", "port": 443, "users": [{` + xdriveID + `, ` + xdriveEnc + `, "flow": "xtls-rprx-vision"}]}]}, ` + xdriveStream + `, "mux": {"enabled": true, "concurrency": -1}}`, `"flow"`},
|
||||
{`{"protocol": "vless", "settings": {` + xdriveServer + `, ` + xdriveEnc + `, "testpre": 1}, ` + xdriveStream + `, "mux": {"enabled": true}}`, `"testpre"`},
|
||||
{`{"protocol": "vmess", "settings": {` + xdriveServer + `}, ` + xdriveStream + `, "mux": {"enabled": true}}`, "VLESS"},
|
||||
{`{"protocol": "trojan", "settings": {"address": "example.com", "port": 443, "password": "p"}, ` + xdriveStream + `, "mux": {"enabled": true}}`, "VLESS"},
|
||||
{`{"protocol": "freedom", ` + xdriveStream + `}`, "VLESS"},
|
||||
} {
|
||||
if err := build(c.input); err == nil || !strings.Contains(err.Error(), "xdrive transport") || !strings.Contains(err.Error(), c.err) {
|
||||
t.Errorf("expected an xdrive error about %s for %s, got %v", c.err, c.input, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestXDriveInboundConfig(t *testing.T) {
|
||||
build := func(s string) error {
|
||||
c := new(InboundDetourConfig)
|
||||
if err := json.Unmarshal([]byte(s), c); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.Build()
|
||||
return err
|
||||
}
|
||||
|
||||
for _, input := range []string{
|
||||
`{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `}, ` + xdriveStream + `}`,
|
||||
`{"protocol": "vless", "listen": "@xdrive", "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `}, ` + xdriveStream + `}`,
|
||||
} {
|
||||
if err := build(input); err != nil {
|
||||
t.Errorf("%s: %v", input, err)
|
||||
}
|
||||
}
|
||||
for _, c := range []struct{ input, err string }{
|
||||
{`{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `}], "decryption": "none"}, ` + xdriveStream + `}`, `"decryption"`},
|
||||
{`{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `, "flow": "xtls-rprx-vision"}, ` + xdriveStream + `}`, `"flow"`},
|
||||
{`{"protocol": "vless", "port": 443, "settings": {"clients": [{` + xdriveID + `, "flow": "xtls-rprx-vision"}], ` + xdriveDec + `}, ` + xdriveStream + `}`, `"flow"`},
|
||||
{`{"protocol": "vmess", "port": 443, "settings": {"clients": [{` + xdriveID + `}]}, ` + xdriveStream + `}`, "VLESS"},
|
||||
{`{"protocol": "vless", "port": "10800-10803", "settings": {"clients": [{` + xdriveID + `}], ` + xdriveDec + `}, ` + xdriveStream + `}`, "one port"},
|
||||
} {
|
||||
if err := build(c.input); err == nil || !strings.Contains(err.Error(), "xdrive transport") || !strings.Contains(err.Error(), c.err) {
|
||||
t.Errorf("expected an xdrive error about %s for %s, got %v", c.err, c.input, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,7 @@ import (
|
||||
core "github.com/xtls/xray-core/core"
|
||||
"github.com/xtls/xray-core/proxy/freedom"
|
||||
"github.com/xtls/xray-core/proxy/masque"
|
||||
"github.com/xtls/xray-core/proxy/vless"
|
||||
"github.com/xtls/xray-core/proxy/vless/inbound"
|
||||
"github.com/xtls/xray-core/proxy/vless/outbound"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -213,9 +209,6 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
|
||||
if _, ok := ts.(*masque.ServerConfig); !ok && receiverSettings.StreamSettings != nil && receiverSettings.StreamSettings.ProtocolName == "masque" {
|
||||
return nil, errors.New("the masque transport can only be used by the masque inbound")
|
||||
}
|
||||
if err := validateXDrive(ts, receiverSettings.StreamSettings, nil, receiverSettings.PortList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &core.InboundHandlerConfig{
|
||||
Tag: c.Tag,
|
||||
@@ -269,54 +262,6 @@ func validateOutboundTransportSecurity(rawConfig interface{}, senderSettings *pr
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateXDrive allows only VLESS with encryption on xdrive, and on the client Mux.Cool for every connection
|
||||
func validateXDrive(ts proto.Message, ss *internet.StreamConfig, ms *proxyman.MultiplexingConfig, pl *net.PortList) error {
|
||||
if ss == nil || ss.ProtocolName != "xdrive" {
|
||||
return nil
|
||||
}
|
||||
switch c := ts.(type) {
|
||||
case *outbound.Config:
|
||||
a, err := c.Vnext.User.Account.GetInstance()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account := a.(*vless.Account)
|
||||
if account.Flow != "" {
|
||||
return errors.New(`the xdrive transport doesn't support VLESS "flow"`)
|
||||
}
|
||||
if account.Encryption == "none" {
|
||||
return errors.New(`the xdrive transport needs VLESS "encryption", run "xray vlessenc" to get one`)
|
||||
}
|
||||
// reverse opens its own mux
|
||||
if account.Reverse == nil && (ms == nil || !ms.Enabled || ms.Concurrency < 0 || ms.XudpConcurrency < 0 || ms.XudpProxyUDP443 == "skip") {
|
||||
return errors.New(`the xdrive transport needs "mux" enabled, with no negative "concurrency" or "xudpConcurrency" and no "xudpProxyUDP443":"skip"`)
|
||||
}
|
||||
if account.Testpre != 0 {
|
||||
return errors.New(`the xdrive transport doesn't support VLESS "testpre"`)
|
||||
}
|
||||
case *inbound.Config:
|
||||
if c.Decryption == "none" {
|
||||
return errors.New(`the xdrive transport needs VLESS "decryption", run "xray vlessenc" to get one`)
|
||||
}
|
||||
for _, user := range c.Users {
|
||||
a, err := user.Account.GetInstance()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if a.(*vless.Account).Flow != "" {
|
||||
return errors.New(`the xdrive transport doesn't support VLESS "flow"`)
|
||||
}
|
||||
}
|
||||
// every port gets its own listener on the same folder, and each of them accepts the same session
|
||||
if pl != nil && len(pl.Ports()) != 1 {
|
||||
return errors.New("the xdrive transport can only listen on one port")
|
||||
}
|
||||
default:
|
||||
return errors.New("the xdrive transport can only be used by VLESS")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build implements Buildable.
|
||||
func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||
if c.ProxySettings != nil {
|
||||
@@ -395,9 +340,6 @@ func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to build outbound handler for protocol ", c.Protocol).Base(err)
|
||||
}
|
||||
if err := validateXDrive(ts, senderSettings.StreamSettings, senderSettings.MultiplexSettings, nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateOutboundTransportSecurity(rawConfig, senderSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ func parsePlainUDPPacket(sessionID, packetID uint64, bodyPlain []byte) (DecodedU
|
||||
return DecodedUDPPacket{}, ErrBadHeaderType
|
||||
}
|
||||
epoch := binary.BigEndian.Uint64(bodyPlain[1:9])
|
||||
diff := math.Abs(float64(time.Now().Unix() - int64(epoch)))
|
||||
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch))))
|
||||
if diff > 30 {
|
||||
return DecodedUDPPacket{}, ErrBadTimestamp
|
||||
}
|
||||
|
||||
@@ -49,12 +49,12 @@ func (f *SlidingWindow) Add(counter uint64) {
|
||||
|
||||
if counter > f.last {
|
||||
lastBlockIndex := f.last >> swBlockBitLog
|
||||
diff := blockIndex - lastBlockIndex
|
||||
diff := int(blockIndex - lastBlockIndex)
|
||||
if diff > swRingBlocks {
|
||||
diff = swRingBlocks
|
||||
}
|
||||
|
||||
for i := 0; i < int(diff); i++ {
|
||||
for i := 0; i < diff; i++ {
|
||||
lastBlockIndex = (lastBlockIndex + 1) & swBlockMask
|
||||
f.ring[lastBlockIndex] = 0
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ func ReadClientRequestHeaderWithFixed(reader *StreamReader, fixedChunk []byte) (
|
||||
}
|
||||
|
||||
epoch := binary.BigEndian.Uint64(plainFixed[1:9])
|
||||
diff := math.Abs(float64(time.Now().Unix() - int64(epoch)))
|
||||
diff := int(math.Abs(float64(time.Now().Unix() - int64(epoch))))
|
||||
if diff > 30 {
|
||||
return nil, ErrBadTimestamp
|
||||
}
|
||||
@@ -431,7 +431,7 @@ func ReadTCPResponse(r io.Reader, method *CipherMethod, psk []byte, clientSalt [
|
||||
}
|
||||
|
||||
serverEpoch := binary.BigEndian.Uint64(decryptedFixed[1:9])
|
||||
diff := math.Abs(float64(time.Now().Unix() - int64(serverEpoch)))
|
||||
diff := int(math.Abs(float64(time.Now().Unix() - int64(serverEpoch))))
|
||||
if diff > 30 {
|
||||
return nil, ErrBadTimestamp
|
||||
}
|
||||
|
||||
@@ -144,8 +144,7 @@ func (t *Handler) Start() error {
|
||||
errors.LogInfo(context.Background(), "[tun] falied to set interface > iface == nil")
|
||||
return errors.New("iface not found")
|
||||
}
|
||||
var controlErr, sockErr error
|
||||
controlErr = c.Control(func(fd uintptr) {
|
||||
return c.Control(func(fd uintptr) {
|
||||
addrPort, _ := netip.ParseAddrPort(address)
|
||||
// skip loopback
|
||||
if addrPort.Addr().IsLoopback() || strings.HasPrefix(strings.ToLower(address), "localhost:") {
|
||||
@@ -153,13 +152,9 @@ func (t *Handler) Start() error {
|
||||
}
|
||||
err := setinterface(network, address, fd, iface)
|
||||
if err != nil {
|
||||
sockErr = errors.New("[tun] falied to set interface").Base(err)
|
||||
errors.LogInfoInner(context.Background(), err, "[tun] falied to set interface")
|
||||
}
|
||||
})
|
||||
if sockErr != nil {
|
||||
return sockErr
|
||||
}
|
||||
return controlErr
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+14
-11
@@ -16,9 +16,8 @@ import (
|
||||
)
|
||||
|
||||
type bind struct {
|
||||
ctx context.Context
|
||||
resolveFunc func(host string) (net.IP, error)
|
||||
listenFunc func(ctx context.Context) (net.PacketConn, error)
|
||||
listenFunc func() (net.PacketConn, error)
|
||||
downFunc func() error
|
||||
reserved []byte
|
||||
|
||||
@@ -27,12 +26,6 @@ type bind struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (b *bind) SetCtx(ctx context.Context) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.ctx = ctx
|
||||
}
|
||||
|
||||
func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
@@ -41,7 +34,7 @@ func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err
|
||||
return nil, 0, conn.ErrBindAlreadyOpen
|
||||
}
|
||||
|
||||
c, err := b.listenFunc(b.ctx)
|
||||
c, err := b.listenFunc()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -59,9 +52,12 @@ func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err
|
||||
case <-ch:
|
||||
default:
|
||||
errors.LogErrorInner(context.Background(), err, "unexpected closed")
|
||||
if b.downFunc != nil {
|
||||
b.mu.Lock()
|
||||
downFunc := b.downFunc
|
||||
b.mu.Unlock()
|
||||
if downFunc != nil {
|
||||
go func() {
|
||||
common.Must(b.downFunc())
|
||||
common.Must(downFunc())
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -83,6 +79,13 @@ func (b *bind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint16, err
|
||||
}, uint16(c.LocalAddr().(*net.UDPAddr).Port), nil
|
||||
}
|
||||
|
||||
// setDownFunc sets downFunc after the device is created, since the device may already be using the bind.
|
||||
func (b *bind) setDownFunc(f func() error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.downFunc = f
|
||||
}
|
||||
|
||||
func (b *bind) Close() error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
+149
-165
@@ -7,7 +7,6 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
@@ -32,14 +31,18 @@ import (
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
p policy.Manager
|
||||
conf *DeviceConfig
|
||||
policyManager policy.Manager
|
||||
dns dns.Client
|
||||
|
||||
canup *atomic.Bool
|
||||
tnet *Net
|
||||
bind *bind
|
||||
dev *device.Device
|
||||
ipc string
|
||||
mu sync.Mutex
|
||||
streamSettings *internet.MemoryStreamConfig
|
||||
uplinkCounter stats.Counter
|
||||
downlinkCounter stats.Counter
|
||||
|
||||
tun tun.Device
|
||||
tnet *Net
|
||||
dev *device.Device
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
@@ -121,86 +124,17 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blockup := newblockup(tun)
|
||||
bind := &bind{}
|
||||
logger := &device.Logger{
|
||||
Verbosef: func(format string, args ...any) {
|
||||
log.Record(&log.GeneralMessage{
|
||||
Severity: log.Severity_Debug,
|
||||
Content: fmt.Sprintf(format, args...),
|
||||
})
|
||||
},
|
||||
Errorf: func(format string, args ...any) {
|
||||
log.Record(&log.GeneralMessage{
|
||||
Severity: log.Severity_Error,
|
||||
Content: fmt.Sprintf(format, args...),
|
||||
})
|
||||
},
|
||||
}
|
||||
dev := device.NewDevice(blockup, bind, logger)
|
||||
bind.ctx = context.Background()
|
||||
bind.resolveFunc = MakeResolveLocal(d, streamSettings)
|
||||
bind.listenFunc = func(ctx context.Context) (net.PacketConn, error) {
|
||||
dest, err := net.ParseDestination("udp:" + conf.Peers[0].Endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pktConn net.PacketConn
|
||||
if streamSettings.FinalMask != nil {
|
||||
conn, err := streamSettings.FinalMask.DialUDP(ctx, dest)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to dial to dest").Base(err)
|
||||
}
|
||||
pktConn = conn.(*net.PacketConnWrapper).PacketConn
|
||||
} else {
|
||||
conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to dial to dest").Base(err)
|
||||
}
|
||||
switch c := conn.(type) {
|
||||
case *net.PacketConnWrapper:
|
||||
pktConn = c.PacketConn
|
||||
case *cnc.Connection:
|
||||
pktConn = &internet.FakePacketConn{Conn: c}
|
||||
default:
|
||||
panic(reflect.TypeOf(c))
|
||||
}
|
||||
}
|
||||
if uplinkCounter != nil || downlinkCounter != nil {
|
||||
pktConn = &PacketCounterConnection{
|
||||
PacketConn: pktConn,
|
||||
ReadCounter: downlinkCounter,
|
||||
WriteCounter: uplinkCounter,
|
||||
}
|
||||
}
|
||||
return pktConn, nil
|
||||
}
|
||||
bind.downFunc = dev.Down
|
||||
bind.reserved = conf.Reserved
|
||||
var cfg strings.Builder
|
||||
cfg.WriteString("private_key=" + conf.SecretKey + "\n")
|
||||
for _, peer := range conf.Peers {
|
||||
cfg.WriteString("public_key=" + peer.PublicKey + "\n")
|
||||
if peer.PreSharedKey != "" {
|
||||
cfg.WriteString("preshared_key=" + peer.PreSharedKey + "\n")
|
||||
}
|
||||
cfg.WriteString("endpoint=" + peer.Endpoint + "\n")
|
||||
for _, ip := range peer.AllowedIps {
|
||||
cfg.WriteString("allowed_ip=" + ip + "\n")
|
||||
}
|
||||
if peer.KeepAlive != "" {
|
||||
cfg.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
return &Handler{
|
||||
p: p,
|
||||
conf: conf,
|
||||
policyManager: p,
|
||||
dns: d,
|
||||
|
||||
canup: blockup.canup,
|
||||
tnet: tnet,
|
||||
bind: bind,
|
||||
dev: dev,
|
||||
ipc: cfg.String(),
|
||||
streamSettings: streamSettings,
|
||||
uplinkCounter: uplinkCounter,
|
||||
downlinkCounter: downlinkCounter,
|
||||
|
||||
tun: tun,
|
||||
tnet: tnet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -215,7 +149,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
ob.CanSpliceCopy = 3
|
||||
dialer.SetOutboundGateway(ctx, ob)
|
||||
|
||||
if err := h.up(ctx); err != nil {
|
||||
if err := h.init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -225,7 +159,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
|
||||
newCtx, newCancel = context.WithCancel(context.Background())
|
||||
}
|
||||
|
||||
sessionPolicy := h.p.ForLevel(0)
|
||||
sessionPolicy := h.policyManager.ForLevel(0)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
timer := signal.CancelAfterInactivity(ctx, func() {
|
||||
cancel()
|
||||
@@ -300,72 +234,150 @@ func (h *Handler) Close() (err error) {
|
||||
if h.dev != nil {
|
||||
h.dev.Close()
|
||||
h.dev = nil
|
||||
h.tun = nil
|
||||
} else if h.tun != nil {
|
||||
h.tun.Close()
|
||||
h.tun = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) up(ctx context.Context) error {
|
||||
func (h *Handler) init(ctx context.Context) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
if h.dev == nil {
|
||||
if h.tun == nil {
|
||||
return errors.New("closed")
|
||||
}
|
||||
h.bind.SetCtx(ctx)
|
||||
if !h.canup.Load() {
|
||||
h.canup.Store(true)
|
||||
if h.dev != nil {
|
||||
return h.dev.Up()
|
||||
}
|
||||
if len(h.ipc) > 0 {
|
||||
if err := h.dev.IpcSet(h.ipc); err != nil {
|
||||
return err
|
||||
}
|
||||
h.ipc = ""
|
||||
}
|
||||
return h.dev.Up()
|
||||
}
|
||||
|
||||
func MakeResolveLocal(d dns.Client, streamSettings *internet.MemoryStreamConfig) func(host string) (net.IP, error) {
|
||||
return func(host string) (net.IP, error) {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
ips, _, err := d.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||
resolveFunc := h.resolveLocal
|
||||
listenFunc := func() (net.PacketConn, error) {
|
||||
dest, err := net.ParseDestination("udp:" + h.conf.Peers[0].Endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
got := ips
|
||||
if streamSettings.SocketSettings != nil {
|
||||
var got4, got6 []net.IP
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
got4 = append(got4, ip)
|
||||
} else {
|
||||
got6 = append(got6, ip)
|
||||
}
|
||||
var pktConn net.PacketConn
|
||||
if h.streamSettings.FinalMask != nil {
|
||||
conn, err := h.streamSettings.FinalMask.DialUDP(ctx, dest)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to dial to dest").Base(err)
|
||||
}
|
||||
switch streamSettings.SocketSettings.DomainStrategy {
|
||||
case internet.DomainStrategy_AS_IS, internet.DomainStrategy_USE_IP, internet.DomainStrategy_FORCE_IP:
|
||||
got = ips
|
||||
case internet.DomainStrategy_USE_IP4, internet.DomainStrategy_FORCE_IP4:
|
||||
got = got4
|
||||
case internet.DomainStrategy_USE_IP6, internet.DomainStrategy_FORCE_IP6:
|
||||
got = got6
|
||||
case internet.DomainStrategy_USE_IP46, internet.DomainStrategy_FORCE_IP46:
|
||||
got = got4
|
||||
if len(got) == 0 {
|
||||
got = got6
|
||||
}
|
||||
case internet.DomainStrategy_USE_IP64, internet.DomainStrategy_FORCE_IP64:
|
||||
got = got6
|
||||
if len(got) == 0 {
|
||||
got = got4
|
||||
}
|
||||
pktConn = conn.(*net.PacketConnWrapper).PacketConn
|
||||
} else {
|
||||
conn, err := internet.DialSystem(ctx, dest, h.streamSettings.SocketSettings)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to dial to dest").Base(err)
|
||||
}
|
||||
if len(got) == 0 {
|
||||
return nil, dns.ErrEmptyResponse
|
||||
switch c := conn.(type) {
|
||||
case *net.PacketConnWrapper:
|
||||
pktConn = c.PacketConn
|
||||
case *cnc.Connection:
|
||||
pktConn = &internet.FakePacketConn{Conn: c}
|
||||
default:
|
||||
panic(reflect.TypeOf(c))
|
||||
}
|
||||
}
|
||||
return got[dice.Roll(len(got))], nil
|
||||
if h.uplinkCounter != nil || h.downlinkCounter != nil {
|
||||
pktConn = &PacketCounterConnection{
|
||||
PacketConn: pktConn,
|
||||
ReadCounter: h.downlinkCounter,
|
||||
WriteCounter: h.uplinkCounter,
|
||||
}
|
||||
}
|
||||
return pktConn, nil
|
||||
}
|
||||
// device.NewDevice may use the bind right away (Up -> BindUpdate -> Open),
|
||||
// so everything it reads must be set before creating the device.
|
||||
bind := &bind{
|
||||
resolveFunc: resolveFunc,
|
||||
listenFunc: listenFunc,
|
||||
reserved: h.conf.Reserved,
|
||||
}
|
||||
logger := &device.Logger{
|
||||
Verbosef: func(format string, args ...any) {
|
||||
log.Record(&log.GeneralMessage{
|
||||
Severity: log.Severity_Debug,
|
||||
Content: fmt.Sprintf(format, args...),
|
||||
})
|
||||
},
|
||||
Errorf: func(format string, args ...any) {
|
||||
log.Record(&log.GeneralMessage{
|
||||
Severity: log.Severity_Error,
|
||||
Content: fmt.Sprintf(format, args...),
|
||||
})
|
||||
},
|
||||
}
|
||||
dev := device.NewDevice(h.tun, bind, logger)
|
||||
bind.setDownFunc(dev.Down)
|
||||
var cfg strings.Builder
|
||||
cfg.WriteString("private_key=" + h.conf.SecretKey + "\n")
|
||||
for _, peer := range h.conf.Peers {
|
||||
cfg.WriteString("public_key=" + peer.PublicKey + "\n")
|
||||
if peer.PreSharedKey != "" {
|
||||
cfg.WriteString("preshared_key=" + peer.PreSharedKey + "\n")
|
||||
}
|
||||
cfg.WriteString("endpoint=" + peer.Endpoint + "\n")
|
||||
for _, ip := range peer.AllowedIps {
|
||||
cfg.WriteString("allowed_ip=" + ip + "\n")
|
||||
}
|
||||
if peer.KeepAlive != "" {
|
||||
cfg.WriteString("persistent_keepalive_interval=" + peer.KeepAlive + "\n")
|
||||
}
|
||||
}
|
||||
err := dev.IpcSet(cfg.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = dev.Up()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.dev = dev
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) resolveLocal(host string) (net.IP, error) {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return ip, nil
|
||||
}
|
||||
ips, _, err := h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
got := ips
|
||||
if h.streamSettings.SocketSettings != nil {
|
||||
var got4, got6 []net.IP
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
got4 = append(got4, ip)
|
||||
} else {
|
||||
got6 = append(got6, ip)
|
||||
}
|
||||
}
|
||||
switch h.streamSettings.SocketSettings.DomainStrategy {
|
||||
case internet.DomainStrategy_AS_IS, internet.DomainStrategy_USE_IP, internet.DomainStrategy_FORCE_IP:
|
||||
got = ips
|
||||
case internet.DomainStrategy_USE_IP4, internet.DomainStrategy_FORCE_IP4:
|
||||
got = got4
|
||||
case internet.DomainStrategy_USE_IP6, internet.DomainStrategy_FORCE_IP6:
|
||||
got = got6
|
||||
case internet.DomainStrategy_USE_IP46, internet.DomainStrategy_FORCE_IP46:
|
||||
got = got4
|
||||
if len(got) == 0 {
|
||||
got = got6
|
||||
}
|
||||
case internet.DomainStrategy_USE_IP64, internet.DomainStrategy_FORCE_IP64:
|
||||
got = got6
|
||||
if len(got) == 0 {
|
||||
got = got4
|
||||
}
|
||||
}
|
||||
if len(got) == 0 {
|
||||
return nil, dns.ErrEmptyResponse
|
||||
}
|
||||
}
|
||||
return got[dice.Roll(len(got))], nil
|
||||
}
|
||||
|
||||
type UDPConnClient struct {
|
||||
@@ -498,31 +510,3 @@ func (c *cache) Cache(host string, saddr []string, ttl uint32) {
|
||||
deadline: time.Now().Add(time.Second * time.Duration(ttl)),
|
||||
}
|
||||
}
|
||||
|
||||
type blockup struct {
|
||||
tun.Device
|
||||
ch chan tun.Event
|
||||
canup *atomic.Bool
|
||||
}
|
||||
|
||||
func newblockup(t tun.Device) *blockup {
|
||||
ch := make(chan tun.Event, cap(t.Events()))
|
||||
canup := &atomic.Bool{}
|
||||
go func() {
|
||||
for e := range t.Events() {
|
||||
if e&tun.EventUp != 0 && !canup.Load() {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case ch <- e:
|
||||
default:
|
||||
}
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return &blockup{Device: t, ch: ch, canup: canup}
|
||||
}
|
||||
|
||||
func (b *blockup) Events() <-chan tun.Event {
|
||||
return b.ch
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ func (s *Server) Start() error {
|
||||
if s.src.Address.Family().IsDomain() {
|
||||
return errors.New("address is domain")
|
||||
}
|
||||
listenFunc := func(context.Context) (net.PacketConn, error) {
|
||||
listenFunc := func() (net.PacketConn, error) {
|
||||
var pktConn net.PacketConn
|
||||
var err error
|
||||
if s.streamSettings.FinalMask != nil {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package ipfilter
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
)
|
||||
|
||||
func (c *Config) matcher() (geodata.IPMatcher, error) {
|
||||
if len(c.BlockedIps) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return geodata.IPReg.BuildIPMatcher(c.BlockedIps)
|
||||
}
|
||||
|
||||
func (c *Config) Listen(inner net.Listener) (net.Listener, error) {
|
||||
matcher, err := c.matcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matcher == nil {
|
||||
return inner, nil
|
||||
}
|
||||
return &listener{Listener: inner, matcher: matcher}, nil
|
||||
}
|
||||
|
||||
func (c *Config) WrapConnClient(conn net.Conn, dest *net.Destination, dialer *finalmask.Dialer) (net.Conn, error) {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Config) WrapConnServer(conn net.Conn) (net.Conn, error) {
|
||||
// TCP filtering is installed by Listen, before per-connection masks.
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnClient(conn net.PacketConn, dest *net.Destination, dialer *finalmask.Dialer) (net.PacketConn, error) {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *Config) WrapPacketConnServer(conn net.PacketConn, addr net.Addr, lc *finalmask.ListenConfig) (net.PacketConn, error) {
|
||||
matcher, err := c.matcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matcher == nil {
|
||||
return conn, nil
|
||||
}
|
||||
return &packetConn{PacketConn: conn, matcher: matcher}, nil
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.5
|
||||
// source: transport/internet/finalmask/ipfilter/config.proto
|
||||
|
||||
package ipfilter
|
||||
|
||||
import (
|
||||
geodata "github.com/xtls/xray-core/common/geodata"
|
||||
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"`
|
||||
BlockedIps []*geodata.IPRule `protobuf:"bytes,1,rep,name=blocked_ips,json=blockedIps,proto3" json:"blocked_ips,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Config) Reset() {
|
||||
*x = Config{}
|
||||
mi := &file_transport_internet_finalmask_ipfilter_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_ipfilter_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_ipfilter_config_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Config) GetBlockedIps() []*geodata.IPRule {
|
||||
if x != nil {
|
||||
return x.BlockedIps
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_transport_internet_finalmask_ipfilter_config_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_transport_internet_finalmask_ipfilter_config_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"2transport/internet/finalmask/ipfilter/config.proto\x12*xray.transport.internet.finalmask.ipfilter\x1a\x1bcommon/geodata/geodat.proto\"F\n" +
|
||||
"\x06Config\x12<\n" +
|
||||
"\vblocked_ips\x18\x01 \x03(\v2\x1b.xray.common.geodata.IPRuleR\n" +
|
||||
"blockedIpsB\xa0\x01\n" +
|
||||
".com.xray.transport.internet.finalmask.ipfilterP\x01Z?github.com/xtls/xray-core/transport/internet/finalmask/ipfilter\xaa\x02*Xray.Transport.Internet.Finalmask.Ipfilterb\x06proto3"
|
||||
|
||||
var (
|
||||
file_transport_internet_finalmask_ipfilter_config_proto_rawDescOnce sync.Once
|
||||
file_transport_internet_finalmask_ipfilter_config_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_transport_internet_finalmask_ipfilter_config_proto_rawDescGZIP() []byte {
|
||||
file_transport_internet_finalmask_ipfilter_config_proto_rawDescOnce.Do(func() {
|
||||
file_transport_internet_finalmask_ipfilter_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_finalmask_ipfilter_config_proto_rawDesc), len(file_transport_internet_finalmask_ipfilter_config_proto_rawDesc)))
|
||||
})
|
||||
return file_transport_internet_finalmask_ipfilter_config_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_transport_internet_finalmask_ipfilter_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||
var file_transport_internet_finalmask_ipfilter_config_proto_goTypes = []any{
|
||||
(*Config)(nil), // 0: xray.transport.internet.finalmask.ipfilter.Config
|
||||
(*geodata.IPRule)(nil), // 1: xray.common.geodata.IPRule
|
||||
}
|
||||
var file_transport_internet_finalmask_ipfilter_config_proto_depIdxs = []int32{
|
||||
1, // 0: xray.transport.internet.finalmask.ipfilter.Config.blocked_ips:type_name -> xray.common.geodata.IPRule
|
||||
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_ipfilter_config_proto_init() }
|
||||
func file_transport_internet_finalmask_ipfilter_config_proto_init() {
|
||||
if File_transport_internet_finalmask_ipfilter_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_ipfilter_config_proto_rawDesc), len(file_transport_internet_finalmask_ipfilter_config_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 1,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_transport_internet_finalmask_ipfilter_config_proto_goTypes,
|
||||
DependencyIndexes: file_transport_internet_finalmask_ipfilter_config_proto_depIdxs,
|
||||
MessageInfos: file_transport_internet_finalmask_ipfilter_config_proto_msgTypes,
|
||||
}.Build()
|
||||
File_transport_internet_finalmask_ipfilter_config_proto = out.File
|
||||
file_transport_internet_finalmask_ipfilter_config_proto_goTypes = nil
|
||||
file_transport_internet_finalmask_ipfilter_config_proto_depIdxs = nil
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package xray.transport.internet.finalmask.ipfilter;
|
||||
option csharp_namespace = "Xray.Transport.Internet.Finalmask.Ipfilter";
|
||||
option go_package = "github.com/xtls/xray-core/transport/internet/finalmask/ipfilter";
|
||||
option java_package = "com.xray.transport.internet.finalmask.ipfilter";
|
||||
option java_multiple_files = true;
|
||||
|
||||
import "common/geodata/geodat.proto";
|
||||
|
||||
message Config {
|
||||
repeated xray.common.geodata.IPRule blocked_ips = 1;
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package ipfilter
|
||||
|
||||
import (
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
)
|
||||
|
||||
type listener struct {
|
||||
net.Listener
|
||||
matcher geodata.IPMatcher
|
||||
}
|
||||
|
||||
// Accept filters connections after the TCP handshake and before higher-level processing.
|
||||
func (l *listener) Accept() (net.Conn, error) {
|
||||
for {
|
||||
conn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matchAddress(l.matcher, conn.RemoteAddr()) {
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
}
|
||||
|
||||
type packetConn struct {
|
||||
net.PacketConn
|
||||
matcher geodata.IPMatcher
|
||||
}
|
||||
|
||||
func (c *packetConn) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
for {
|
||||
n, addr, err := c.PacketConn.ReadFrom(p)
|
||||
// Reads may return both data and an error; filter the data first.
|
||||
if matchAddress(c.matcher, addr) {
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
return n, addr, err
|
||||
}
|
||||
}
|
||||
|
||||
func matchAddress(matcher geodata.IPMatcher, addr net.Addr) bool {
|
||||
switch addr := addr.(type) {
|
||||
case *net.TCPAddr:
|
||||
return matcher.Match(addr.IP)
|
||||
case *net.UDPAddr:
|
||||
return matcher.Match(addr.IP)
|
||||
case *net.IPAddr:
|
||||
return matcher.Match(addr.IP)
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
package ipfilter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pires/go-proxyproto"
|
||||
"github.com/xtls/xray-core/common/geodata"
|
||||
xnet "github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet/finalmask"
|
||||
)
|
||||
|
||||
func TestTCPFilterBeforeOtherMasks(t *testing.T) {
|
||||
blocked4 := sourceConn("192.0.2.1")
|
||||
blocked6 := sourceConn("2001:db8::1")
|
||||
unknown := &testConn{addr: &net.UnixAddr{Name: "ipfilter.sock", Net: "unix"}}
|
||||
missing := new(testConn)
|
||||
allowed := sourceConn("198.51.100.1")
|
||||
raw := &testListener{conns: []net.Conn{blocked4, blocked6, unknown, missing, allowed}}
|
||||
spy := new(recordMask)
|
||||
fm := finalmask.NewFinalMask([]finalmask.TCPMask{spy, testConfig(t, "192.0.2.0/24", "2001:db8::/32")}, nil, nil,
|
||||
func(context.Context, net.Addr) (net.Listener, error) { return raw, nil }, nil, nil)
|
||||
l, err := fm.Listen(context.Background(), &net.TCPAddr{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
got, err := l.Accept()
|
||||
if err != nil || got != allowed {
|
||||
t.Fatalf("Accept() = (%v, %v), want unblocked connection", got, err)
|
||||
}
|
||||
// Fake sockets panic on Read or Write. Rejection must not require handshake data.
|
||||
if !blocked4.closed || !blocked6.closed || !unknown.closed || !missing.closed || allowed.closed || raw.closed {
|
||||
t.Fatal("filter did not close only the blocked connections")
|
||||
}
|
||||
if len(spy.seen) != 1 || spy.seen[0] != allowed {
|
||||
t.Fatal("blocked connections reached the next mask")
|
||||
}
|
||||
if conn, err := l.Accept(); conn != nil || !errors.Is(err, io.EOF) {
|
||||
t.Fatalf("listener error was not propagated: (%v, %v)", conn, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTCPFilterUsesProxyHeader(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name, peer, reported string
|
||||
blocked bool
|
||||
}{
|
||||
{"blocked_header", "198.51.100.1", "192.0.2.1", true},
|
||||
{"allowed_header", "192.0.2.1", "198.51.100.1", false},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
if err := server.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
peer := sourceConn(test.peer)
|
||||
peer.Conn = server
|
||||
conn := proxyproto.NewConn(peer)
|
||||
next := sourceConn("198.51.100.2")
|
||||
l, err := testConfig(t, "192.0.2.0/24").Listen(&testListener{conns: []net.Conn{conn, next}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
written := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := client.Write([]byte("PROXY TCP4 " + test.reported + " 203.0.113.1 1234 443\r\n"))
|
||||
written <- err
|
||||
}()
|
||||
got, err := l.Accept()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := <-written; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var want net.Conn = conn
|
||||
if test.blocked {
|
||||
want = next
|
||||
}
|
||||
if got != want || peer.closed != test.blocked {
|
||||
t.Fatal("filter did not follow the address supplied by PROXY protocol")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUDPFilter(t *testing.T) {
|
||||
allowed := &net.UDPAddr{IP: net.ParseIP("198.51.100.1"), Port: 1234}
|
||||
raw := &testPacketConn{reads: []datagram{
|
||||
{data: []byte("blocked"), addr: &net.UDPAddr{IP: net.ParseIP("192.0.2.1"), Port: 1234}},
|
||||
{addr: &net.UDPAddr{IP: net.ParseIP("2001:db8::1"), Port: 1234}},
|
||||
{data: []byte("unknown"), addr: &net.UnixAddr{Name: "ipfilter.sock", Net: "unixgram"}},
|
||||
{data: []byte("missing address")},
|
||||
{addr: allowed},
|
||||
{data: []byte("allowed"), addr: allowed},
|
||||
{data: []byte("blocked again"), addr: &net.UDPAddr{IP: net.ParseIP("192.0.2.2"), Port: 1234}},
|
||||
}}
|
||||
fm := finalmask.NewFinalMask(nil, []finalmask.UDPMask{testConfig(t, "192.0.2.0/24", "2001:db8::/32")}, nil, nil, nil,
|
||||
func(context.Context, net.Addr) (net.PacketConn, error) { return raw, nil })
|
||||
conn, err := fm.ListenPacket(context.Background(), &net.UDPAddr{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
buf := make([]byte, 64)
|
||||
if n, addr, err := conn.ReadFrom(buf); n != 0 || addr != allowed || err != nil {
|
||||
t.Fatalf("zero-length allowed datagram was lost: (%d, %v, %v)", n, addr, err)
|
||||
}
|
||||
n, addr, err := conn.ReadFrom(buf)
|
||||
if err != nil || addr != allowed || string(buf[:n]) != "allowed" {
|
||||
t.Fatalf("ReadFrom() = (%q, %v, %v)", buf[:n], addr, err)
|
||||
}
|
||||
if _, _, err := conn.ReadFrom(buf); !errors.Is(err, io.EOF) {
|
||||
t.Fatalf("underlying read error was not propagated: %v", err)
|
||||
}
|
||||
if raw.closed {
|
||||
t.Fatal("dropping a packet closed the shared UDP socket")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyFilter(t *testing.T) {
|
||||
rawListener := new(testListener)
|
||||
rawPacket := new(testPacketConn)
|
||||
empty := new(Config)
|
||||
if l, err := empty.Listen(rawListener); err != nil || l != rawListener {
|
||||
t.Fatal("an empty TCP filter should preserve the listener")
|
||||
}
|
||||
if conn, err := empty.WrapPacketConnServer(rawPacket, nil, nil); err != nil || conn != rawPacket {
|
||||
t.Fatal("an empty UDP filter should preserve the socket")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUDPFilterReadError(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
source net.Addr
|
||||
blocked bool
|
||||
}{
|
||||
{"blocked", &net.UDPAddr{IP: net.ParseIP("192.0.2.1")}, true},
|
||||
{"allowed", &net.UDPAddr{IP: net.ParseIP("198.51.100.1")}, false},
|
||||
{"missing address", nil, true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
source := test.source
|
||||
raw := &testPacketConn{reads: []datagram{{data: []byte("partial"), addr: source, err: io.ErrShortBuffer}}}
|
||||
conn, err := testConfig(t, "192.0.2.0/24").WrapPacketConnServer(raw, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buf := make([]byte, 32)
|
||||
n, addr, err := conn.ReadFrom(buf)
|
||||
if !errors.Is(err, io.ErrShortBuffer) {
|
||||
t.Fatalf("read error was not preserved: %v", err)
|
||||
}
|
||||
if test.blocked {
|
||||
if n != 0 || addr != nil {
|
||||
t.Fatal("partial data from a blocked source was returned with an error")
|
||||
}
|
||||
} else if n != 7 || addr != source || string(buf[:n]) != "partial" {
|
||||
t.Fatal("partial data from an unblocked source was changed")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testConfig(t *testing.T, entries ...string) *Config {
|
||||
t.Helper()
|
||||
rules, err := geodata.ParseIPRules(entries)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &Config{BlockedIps: rules}
|
||||
}
|
||||
|
||||
type testConn struct {
|
||||
net.Conn
|
||||
addr net.Addr
|
||||
closed bool
|
||||
}
|
||||
|
||||
func sourceConn(ip string) *testConn {
|
||||
return &testConn{addr: &net.TCPAddr{IP: net.ParseIP(ip), Port: 1234}}
|
||||
}
|
||||
|
||||
func (c *testConn) RemoteAddr() net.Addr { return c.addr }
|
||||
func (c *testConn) Close() error {
|
||||
c.closed = true
|
||||
if c.Conn != nil {
|
||||
return c.Conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type testListener struct {
|
||||
net.Listener
|
||||
conns []net.Conn
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (l *testListener) Accept() (net.Conn, error) {
|
||||
if len(l.conns) == 0 {
|
||||
return nil, io.EOF
|
||||
}
|
||||
conn := l.conns[0]
|
||||
l.conns = l.conns[1:]
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (l *testListener) Close() error { l.closed = true; return nil }
|
||||
|
||||
type datagram struct {
|
||||
data []byte
|
||||
addr net.Addr
|
||||
err error
|
||||
}
|
||||
|
||||
type testPacketConn struct {
|
||||
net.PacketConn
|
||||
reads []datagram
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (c *testPacketConn) ReadFrom(p []byte) (int, net.Addr, error) {
|
||||
if len(c.reads) == 0 {
|
||||
return 0, nil, io.EOF
|
||||
}
|
||||
packet := c.reads[0]
|
||||
c.reads = c.reads[1:]
|
||||
return copy(p, packet.data), packet.addr, packet.err
|
||||
}
|
||||
|
||||
func (c *testPacketConn) Close() error { c.closed = true; return nil }
|
||||
|
||||
type recordMask struct{ seen []net.Conn }
|
||||
|
||||
func (m *recordMask) WrapConnClient(c net.Conn, _ *xnet.Destination, _ *finalmask.Dialer) (net.Conn, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (m *recordMask) WrapConnServer(c net.Conn) (net.Conn, error) {
|
||||
m.seen = append(m.seen, c)
|
||||
return c, nil
|
||||
}
|
||||
@@ -22,12 +22,6 @@ type interConn struct {
|
||||
|
||||
client bool
|
||||
user *protocol.MemoryUser
|
||||
lock chan struct{}
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (c *interConn) unlock() {
|
||||
<-c.lock
|
||||
}
|
||||
|
||||
func (c *interConn) User() *protocol.MemoryUser {
|
||||
@@ -39,9 +33,6 @@ func (c *interConn) Read(b []byte) (int, error) {
|
||||
}
|
||||
|
||||
func (c *interConn) Write(b []byte) (int, error) {
|
||||
c.lock <- struct{}{}
|
||||
defer c.unlock()
|
||||
|
||||
if c.client {
|
||||
c.client = false
|
||||
if _, err := c.stream.Write(append(quicvarint.Append(nil, FrameTypeTCPRequest), b...)); err != nil {
|
||||
@@ -49,20 +40,11 @@ func (c *interConn) Write(b []byte) (int, error) {
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
return c.stream.Write(b)
|
||||
}
|
||||
|
||||
func (c *interConn) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
select {
|
||||
case c.lock <- struct{}{}:
|
||||
defer c.unlock()
|
||||
default:
|
||||
c.stream.CancelRead(0)
|
||||
c.stream.CancelWrite(0)
|
||||
return nil
|
||||
}
|
||||
c.stream.CancelRead(0)
|
||||
return c.stream.Close()
|
||||
}
|
||||
|
||||
@@ -257,7 +257,6 @@ func (c *client) tcp(ctx context.Context) (stat.Connection, error) {
|
||||
remote: c.conn.RemoteAddr(),
|
||||
|
||||
client: true,
|
||||
lock: make(chan struct{}, 1),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,6 @@ func (h *httpHandler) StreamDispatcher(ft http3.FrameType, stream *quic.Stream,
|
||||
remote: h.conn.RemoteAddr(),
|
||||
|
||||
user: h.user,
|
||||
lock: make(chan struct{}, 1),
|
||||
})
|
||||
return true, nil
|
||||
default:
|
||||
|
||||
@@ -28,22 +28,3 @@ func (v *SocketConfig) ParseTFOValue() int {
|
||||
}
|
||||
return tfo
|
||||
}
|
||||
|
||||
// Dial should fail on bind errors, return this to indicate such errors.
|
||||
type BindError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *BindError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func (e *BindError) Error() string {
|
||||
return "failed to bind to interface: " + e.err.Error()
|
||||
}
|
||||
|
||||
func NewBindError(err error) *BindError {
|
||||
return &BindError{
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,15 +136,15 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
|
||||
if config.Interface != "" {
|
||||
iface, err := net.InterfaceByName(config.Interface)
|
||||
if err != nil {
|
||||
return NewBindError(errors.New("failed to get interface ", config.Interface).Base(err))
|
||||
return errors.New("failed to get interface ", config.Interface).Base(err)
|
||||
}
|
||||
if network == "tcp6" || network == "udp6" {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index); err != nil {
|
||||
return NewBindError(errors.New("failed to set IPV6_BOUND_IF").Base(err))
|
||||
return errors.New("failed to set IPV6_BOUND_IF").Base(err)
|
||||
}
|
||||
} else {
|
||||
if err := unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index); err != nil {
|
||||
return NewBindError(errors.New("failed to set IP_BOUND_IF").Base(err))
|
||||
return errors.New("failed to set IP_BOUND_IF").Base(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
|
||||
|
||||
if config.Interface != "" {
|
||||
if err := syscall.BindToDevice(int(fd), config.Interface); err != nil {
|
||||
return NewBindError(errors.New("failed to set Interface").Base(err))
|
||||
return errors.New("failed to set Interface").Base(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
|
||||
if config.Interface != "" {
|
||||
inf, err := net.InterfaceByName(config.Interface)
|
||||
if err != nil {
|
||||
return NewBindError(errors.New("failed to find the interface").Base(err))
|
||||
return errors.New("failed to find the interface").Base(err)
|
||||
}
|
||||
// easy way to check if the address is ipv4
|
||||
isV4 := strings.Contains(address, ".")
|
||||
@@ -47,20 +47,20 @@ func applyOutboundSocketOptions(network string, address string, fd uintptr, conf
|
||||
binary.BigEndian.PutUint32(bytes[:], uint32(inf.Index))
|
||||
idx := *(*uint32)(unsafe.Pointer(&bytes[0]))
|
||||
if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IP, IP_UNICAST_IF, int(idx)); err != nil {
|
||||
return NewBindError(errors.New("failed to set IP_UNICAST_IF").Base(err))
|
||||
return errors.New("failed to set IP_UNICAST_IF").Base(err)
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil && ip.IsMulticast() && isUDPSocket(network) {
|
||||
if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IP, syscall.IP_MULTICAST_IF, int(idx)); err != nil {
|
||||
return NewBindError(errors.New("failed to set IP_MULTICAST_IF").Base(err))
|
||||
return errors.New("failed to set IP_MULTICAST_IF").Base(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IPV6, IPV6_UNICAST_IF, inf.Index); err != nil {
|
||||
return NewBindError(errors.New("failed to set IPV6_UNICAST_IF").Base(err))
|
||||
return errors.New("failed to set IPV6_UNICAST_IF").Base(err)
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil && ip.IsMulticast() && isUDPSocket(network) {
|
||||
if err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.IPPROTO_IPV6, syscall.IPV6_MULTICAST_IF, inf.Index); err != nil {
|
||||
return NewBindError(errors.New("failed to set IPV6_MULTICAST_IF").Base(err))
|
||||
return errors.New("failed to set IPV6_MULTICAST_IF").Base(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,27 +68,19 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
|
||||
}
|
||||
}
|
||||
var lc net.ListenConfig
|
||||
if sockopt != nil || len(Controllers) > 0 {
|
||||
lc.Control = func(network, address string, c syscall.RawConn) error {
|
||||
for _, ctl := range Controllers {
|
||||
if err := ctl(network, address, c); err != nil {
|
||||
return errors.New("failed to apply external controller").Base(err)
|
||||
}
|
||||
lc.Control = func(network, address string, c syscall.RawConn) error {
|
||||
for _, ctl := range Controllers {
|
||||
if err := ctl(network, address, c); err != nil {
|
||||
errors.LogInfoInner(ctx, err, "failed to apply external controller")
|
||||
}
|
||||
if sockopt != nil {
|
||||
var sockErr, controlErr error
|
||||
controlErr = c.Control(func(fd uintptr) {
|
||||
if sockErr = applyOutboundSocketOptions(network, destAddr.String(), fd, sockopt); sockErr != nil {
|
||||
errors.LogInfoInner(ctx, sockErr, "failed to apply socket options")
|
||||
}
|
||||
})
|
||||
if _, ok := sockErr.(*BindError); ok {
|
||||
return sockErr
|
||||
}
|
||||
return controlErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return c.Control(func(fd uintptr) {
|
||||
if sockopt != nil {
|
||||
if err := applyOutboundSocketOptions(network, destAddr.String(), fd, sockopt); err != nil {
|
||||
errors.LogInfo(ctx, err, "failed to apply socket options")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
packetConn, err := lc.ListenPacket(ctx, srcAddr.Network(), srcAddr.String())
|
||||
if err != nil {
|
||||
@@ -136,22 +128,16 @@ func (d *DefaultSystemDialer) Dial(ctx context.Context, src net.Address, dest ne
|
||||
dialer.Control = func(network, address string, c syscall.RawConn) error {
|
||||
for _, ctl := range Controllers {
|
||||
if err := ctl(network, address, c); err != nil {
|
||||
return errors.New("failed to apply external controller").Base(err)
|
||||
errors.LogInfoInner(ctx, err, "failed to apply external controller")
|
||||
}
|
||||
}
|
||||
if sockopt != nil {
|
||||
var sockErr, controlErr error
|
||||
controlErr = c.Control(func(fd uintptr) {
|
||||
if sockErr = applyOutboundSocketOptions(network, address, fd, sockopt); sockErr != nil {
|
||||
errors.LogInfoInner(ctx, sockErr, "failed to apply socket options")
|
||||
return c.Control(func(fd uintptr) {
|
||||
if sockopt != nil {
|
||||
if err := applyOutboundSocketOptions(network, address, fd, sockopt); err != nil {
|
||||
errors.LogInfoInner(ctx, err, "failed to apply socket options")
|
||||
}
|
||||
})
|
||||
if _, ok := sockErr.(*BindError); ok {
|
||||
return sockErr
|
||||
}
|
||||
return controlErr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,124 +2,44 @@ package xdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
gotls "crypto/tls"
|
||||
"net/http"
|
||||
"sync"
|
||||
"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"
|
||||
)
|
||||
|
||||
var errStalled = errors.New("the request made no progress")
|
||||
|
||||
type serviceTransport struct {
|
||||
plain http.RoundTripper
|
||||
secure http.RoundTripper
|
||||
stall time.Duration
|
||||
}
|
||||
|
||||
// RoundTrip ends a request that stands still for t.stall, with no answer that
|
||||
// long after its body went out or no byte of either body, but not one that just
|
||||
// runs long: a large object on a slow link takes minutes and is fine as long as
|
||||
// its bytes move
|
||||
func (t *serviceTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
next := t.plain
|
||||
if r.URL.Scheme == "https" {
|
||||
next = t.secure
|
||||
return t.secure.RoundTrip(r)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancelCause(r.Context())
|
||||
timer := time.AfterFunc(t.stall, func() { cancel(errStalled) })
|
||||
alive := func() { timer.Reset(t.stall) }
|
||||
end := func() {
|
||||
timer.Stop()
|
||||
cancel(nil)
|
||||
}
|
||||
r = r.WithContext(ctx)
|
||||
if r.Body != nil && r.Body != http.NoBody {
|
||||
r.Body = &progressBody{ReadCloser: r.Body, alive: alive}
|
||||
}
|
||||
// a body sent again by the transport itself, after a GOAWAY or on a reused
|
||||
// connection that failed, moves just the same
|
||||
if getBody := r.GetBody; getBody != nil {
|
||||
r.GetBody = func() (io.ReadCloser, error) {
|
||||
body, err := getBody()
|
||||
if err != nil || body == http.NoBody {
|
||||
return body, err
|
||||
}
|
||||
return &progressBody{ReadCloser: body, alive: alive}, nil
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := next.RoundTrip(r)
|
||||
if err != nil {
|
||||
err = stalled(ctx, err)
|
||||
end()
|
||||
return nil, err
|
||||
}
|
||||
alive()
|
||||
resp.Body = &progressBody{ReadCloser: resp.Body, alive: alive, end: end, ctx: ctx}
|
||||
return resp, nil
|
||||
return t.plain.RoundTrip(r)
|
||||
}
|
||||
|
||||
// stalled returns errStalled for an error that the stall gave, which http2 reports
|
||||
// as a plain cancel
|
||||
func stalled(ctx context.Context, err error) error {
|
||||
if context.Cause(ctx) == errStalled {
|
||||
return errStalled
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type progressBody struct {
|
||||
io.ReadCloser
|
||||
alive func()
|
||||
end func()
|
||||
// ctx is that of the request, for the body of its response
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (b *progressBody) Read(p []byte) (int, error) {
|
||||
// http2 reads a request body up to 512 KiB at a time and sends it all before
|
||||
// the next read, which on a slow link could look like no progress
|
||||
n, err := b.ReadCloser.Read(p[:min(len(p), 16<<10)])
|
||||
if n > 0 {
|
||||
b.alive()
|
||||
}
|
||||
if err != nil && err != io.EOF && b.ctx != nil {
|
||||
err = stalled(b.ctx, err)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (b *progressBody) Close() error {
|
||||
err := b.ReadCloser.Close()
|
||||
if b.end != nil {
|
||||
b.end()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// newServiceClient returns a client whose requests end after stall without
|
||||
// progress, see serviceTransport.RoundTrip
|
||||
func newServiceClient(streamSettings *internet.MemoryStreamConfig, stall time.Duration, maxConns int) *http.Client {
|
||||
func newServiceClient(streamSettings *internet.MemoryStreamConfig, timeout time.Duration, maxConns int) *http.Client {
|
||||
var (
|
||||
tlsConfig *tls.Config
|
||||
sockopt *internet.SocketConfig
|
||||
fronting *net.Destination
|
||||
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
|
||||
}
|
||||
if tlsConfig == nil {
|
||||
tlsConfig = &tls.Config{}
|
||||
}
|
||||
fingerprint := tls.GetFingerprint(tlsConfig.Fingerprint)
|
||||
overHTTP2 := allowsHTTP2(tlsConfig, realityConfig)
|
||||
|
||||
dial := func(ctx context.Context, addr string) (net.Conn, net.Destination, error) {
|
||||
host, err := net.ParseDestination("tcp:" + addr)
|
||||
@@ -152,40 +72,58 @@ func newServiceClient(streamSettings *internet.MemoryStreamConfig, stall time.Du
|
||||
return conn, err
|
||||
}
|
||||
|
||||
// http.Transport picks h2 or h1 by the ALPN in the ConnectionState of the conn
|
||||
dialTLS := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
dialTLS := func(ctx context.Context, addr string) (net.Conn, error) {
|
||||
conn, host, err := dial(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gotlsConfig := tlsConfig.GetTLSConfig(tls.WithDestination(host))
|
||||
var tlsConn net.Conn
|
||||
if fingerprint == nil {
|
||||
tlsConn = tls.Client(conn, gotlsConfig)
|
||||
} else {
|
||||
tlsConn = tls.UClient(conn, gotlsConfig, fingerprint)
|
||||
if realityConfig != nil {
|
||||
return reality.UClient(conn, realityConfig, ctx, host)
|
||||
}
|
||||
// the dial outlives the request that started it, and a handshake that hangs
|
||||
// would keep the one connection of the host to itself
|
||||
ctx, cancel := context.WithTimeout(ctx, stall)
|
||||
defer cancel()
|
||||
if err := tlsConn.(tls.Interface).HandshakeContext(ctx); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
|
||||
gotlsConfig := &gotls.Config{ServerName: host.Address.String()}
|
||||
if tlsConfig != nil {
|
||||
gotlsConfig = tlsConfig.GetTLSConfig(tls.WithDestination(host))
|
||||
}
|
||||
return tlsConn, nil
|
||||
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
|
||||
}
|
||||
|
||||
secure := func(connsPerHost int) *http.Transport {
|
||||
return &http.Transport{
|
||||
DialTLSContext: dialTLS,
|
||||
ForceAttemptHTTP2: true,
|
||||
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: connsPerHost,
|
||||
HTTP2: &http.HTTP2Config{SendPingTimeout: net.ChromeH2KeepAlivePeriod},
|
||||
MaxConnsPerHost: maxConns,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,27 +136,18 @@ func newServiceClient(streamSettings *internet.MemoryStreamConfig, stall time.Du
|
||||
MaxIdleConnsPerHost: maxConns,
|
||||
MaxConnsPerHost: maxConns,
|
||||
},
|
||||
secure: &secureTransport{h2: secure(1), h1: secure(maxConns)},
|
||||
stall: stall,
|
||||
secure: secure,
|
||||
},
|
||||
Timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
// secureTransport keeps a host to one connection, all that h2 needs, as without
|
||||
// a limit parallel requests would each dial their own. A host that answers over
|
||||
// h1 moves to a transport with more, since h1 carries one request at a time
|
||||
type secureTransport struct {
|
||||
h2, h1 http.RoundTripper
|
||||
h1Hosts sync.Map
|
||||
}
|
||||
|
||||
func (t *secureTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
if _, ok := t.h1Hosts.Load(r.URL.Host); ok {
|
||||
return t.h1.RoundTrip(r)
|
||||
func allowsHTTP2(tlsConfig *tls.Config, realityConfig *reality.Config) bool {
|
||||
if realityConfig != nil {
|
||||
return true
|
||||
}
|
||||
resp, err := t.h2.RoundTrip(r)
|
||||
if err == nil && resp.ProtoMajor == 1 {
|
||||
t.h1Hosts.Store(r.URL.Host, true)
|
||||
if tlsConfig == nil {
|
||||
return false
|
||||
}
|
||||
return resp, err
|
||||
return len(tlsConfig.NextProtocol) == 1 && tlsConfig.NextProtocol[0] == "h2"
|
||||
}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
gotls "crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -106,403 +99,3 @@ func TestServiceSNIOverride(t *testing.T) {
|
||||
t.Fatalf("explicit serverName gave SNI %q, want www.google.com", got)
|
||||
}
|
||||
}
|
||||
|
||||
// slowReader hands out one byte every gap, the way a body crosses a slow link
|
||||
type slowReader struct {
|
||||
left int
|
||||
gap time.Duration
|
||||
}
|
||||
|
||||
func (r *slowReader) Read(p []byte) (int, error) {
|
||||
if r.left == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
time.Sleep(r.gap)
|
||||
r.left--
|
||||
p[0] = 'x'
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// a request that keeps moving outlives the stall limit many times over, one
|
||||
// that stands still does not get much past it
|
||||
func TestServiceClientEndsOnlyStalledRequests(t *testing.T) {
|
||||
const stall = 200 * time.Millisecond
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
io.Copy(io.Discard, r.Body)
|
||||
switch r.URL.Path {
|
||||
case "/slow":
|
||||
for i := 0; i < 20; i++ {
|
||||
w.Write([]byte("x"))
|
||||
w.(http.Flusher).Flush()
|
||||
time.Sleep(stall / 4)
|
||||
}
|
||||
case "/stuck":
|
||||
w.Write([]byte("x"))
|
||||
w.(http.Flusher).Flush()
|
||||
time.Sleep(5 * stall)
|
||||
case "/silent":
|
||||
time.Sleep(5 * stall)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
client := newServiceClient(&internet.MemoryStreamConfig{}, stall, 8)
|
||||
|
||||
fetch := func(path string, body io.Reader) (int, error) {
|
||||
req, err := http.NewRequest(http.MethodPost, server.URL+path, body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
got, err := io.ReadAll(resp.Body)
|
||||
return len(got), err
|
||||
}
|
||||
|
||||
if _, err := fetch("/", &slowReader{left: 20, gap: stall / 4}); err != nil {
|
||||
t.Errorf("a slow upload failed: %v", err)
|
||||
}
|
||||
if n, err := fetch("/slow", nil); err != nil || n != 20 {
|
||||
t.Errorf("a slow download gave %d bytes, %v", n, err)
|
||||
}
|
||||
for _, path := range []string{"/stuck", "/silent"} {
|
||||
start := time.Now()
|
||||
_, err := fetch(path, nil)
|
||||
if !errors.Is(err, errStalled) {
|
||||
t.Errorf("%s gave %v, want %v", path, err, errStalled)
|
||||
}
|
||||
if took := time.Since(start); took > 4*stall {
|
||||
t.Errorf("%s took %v to give up", path, took)
|
||||
}
|
||||
}
|
||||
|
||||
// a body that the transport sends again by itself keeps the request going too
|
||||
replay := &serviceTransport{stall: stall, plain: roundTrip(func(r *http.Request) (*http.Response, error) {
|
||||
body, err := r.GetBody()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for b := make([]byte, 1); ; time.Sleep(stall / 4) {
|
||||
if _, err := body.Read(b); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := context.Cause(r.Context()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil
|
||||
})}
|
||||
req, err := http.NewRequest(http.MethodPost, "http://drive/", bytes.NewReader(make([]byte, 20)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp, err := replay.RoundTrip(req); err != nil {
|
||||
t.Errorf("a body sent again failed: %v", err)
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// over TLS a request that stands still ends the same way, and says so, on h2
|
||||
// as on h1
|
||||
func TestServiceClientStallOverTLS(t *testing.T) {
|
||||
const stall = 200 * time.Millisecond
|
||||
for _, protos := range [][]string{chromeALPN, {"http/1.1"}} {
|
||||
server := newTLSServer(t, &gotls.Config{NextProtos: protos}, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/stuck" {
|
||||
w.Write([]byte("x"))
|
||||
w.(http.Flusher).Flush()
|
||||
}
|
||||
time.Sleep(5 * stall)
|
||||
})
|
||||
client := newServiceClient(server.settings(&tls.Config{}), stall, 8)
|
||||
|
||||
for _, path := range []string{"/stuck", "/silent"} {
|
||||
start := time.Now()
|
||||
resp, err := client.Get(server.URL + path)
|
||||
if err == nil {
|
||||
if resp.Proto != picked(protos) {
|
||||
t.Fatalf("%s went over %s", path, resp.Proto)
|
||||
}
|
||||
_, err = io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
}
|
||||
if !errors.Is(err, errStalled) {
|
||||
t.Errorf("server %v: %s gave %v, want %v", protos, path, err, errStalled)
|
||||
}
|
||||
if took := time.Since(start); took > 4*stall {
|
||||
t.Errorf("server %v: %s took %v to give up", protos, path, took)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tlsServer is a TLS server that keeps the last hello it got and counts its
|
||||
// connections
|
||||
type tlsServer struct {
|
||||
*httptest.Server
|
||||
mu sync.Mutex
|
||||
hello *gotls.ClientHelloInfo
|
||||
conns atomic.Int32
|
||||
}
|
||||
|
||||
func newTLSServer(t *testing.T, config *gotls.Config, handler http.HandlerFunc) *tlsServer {
|
||||
t.Helper()
|
||||
|
||||
s := &tlsServer{Server: httptest.NewUnstartedServer(handler)}
|
||||
config.GetConfigForClient = func(hello *gotls.ClientHelloInfo) (*gotls.Config, error) {
|
||||
s.mu.Lock()
|
||||
s.hello = hello
|
||||
s.mu.Unlock()
|
||||
return nil, nil
|
||||
}
|
||||
s.TLS = config
|
||||
s.Config.ConnState = func(_ net.Conn, state http.ConnState) {
|
||||
if state == http.StateNew {
|
||||
s.conns.Add(1)
|
||||
}
|
||||
}
|
||||
s.StartTLS()
|
||||
t.Cleanup(s.Close)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *tlsServer) lastHello() *gotls.ClientHelloInfo {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.hello
|
||||
}
|
||||
|
||||
// settings pins the certificate of the server in config
|
||||
func (s *tlsServer) settings(config *tls.Config) *internet.MemoryStreamConfig {
|
||||
config.PinnedPeerCertSha256 = [][]byte{tls.GenerateCertHash(s.Certificate())}
|
||||
return &internet.MemoryStreamConfig{ProtocolName: protocolName, SecuritySettings: config}
|
||||
}
|
||||
|
||||
// picked is the protocol a server with ALPN protos answers a Chrome hello with
|
||||
func picked(protos []string) string {
|
||||
if protos[0] == "h2" {
|
||||
return "HTTP/2.0"
|
||||
}
|
||||
return "HTTP/1.1"
|
||||
}
|
||||
|
||||
var chromeALPN = []string{"h2", "http/1.1"}
|
||||
|
||||
// the hello offers the ALPN of the fingerprint whatever "alpn" says, or with Go
|
||||
// TLS that of the config, h2 and http/1.1 by default, and the client speaks what
|
||||
// the server picks from it
|
||||
func TestServiceClientSpeaksWhatServerPicks(t *testing.T) {
|
||||
for _, protos := range [][]string{chromeALPN, {"http/1.1"}} {
|
||||
server := newTLSServer(t, &gotls.Config{NextProtos: protos}, func(w http.ResponseWriter, r *http.Request) {})
|
||||
for _, config := range []*tls.Config{{}, {NextProtocol: []string{"h2"}}, {NextProtocol: []string{"http/1.1"}}, {Fingerprint: "unsafe"}} {
|
||||
client := newServiceClient(server.settings(config), 5*time.Second, 8)
|
||||
resp, err := client.Get(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("server %v, alpn %v, fingerprint %q: %v", protos, config.NextProtocol, config.Fingerprint, err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.Proto != picked(protos) {
|
||||
t.Errorf("server %v, alpn %v, fingerprint %q: went over %s", protos, config.NextProtocol, config.Fingerprint, resp.Proto)
|
||||
}
|
||||
if offered := server.lastHello().SupportedProtos; !slices.Equal(offered, chromeALPN) {
|
||||
t.Errorf("server %v, alpn %v, fingerprint %q: the hello offered %v, want %v", protos, config.NextProtocol, config.Fingerprint, offered, chromeALPN)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// with no tlsSettings the hello is that of Chrome too, which unlike Go TLS
|
||||
// offers certificate compression
|
||||
func TestServiceClientDefaultsToChrome(t *testing.T) {
|
||||
server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {})
|
||||
client := newServiceClient(&internet.MemoryStreamConfig{}, 5*time.Second, 8)
|
||||
// the test certificate is not trusted, the hello is all that is needed
|
||||
if resp, err := client.Get(server.URL); err == nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
hello := server.lastHello()
|
||||
if hello == nil {
|
||||
t.Fatal("no hello")
|
||||
}
|
||||
if !slices.Equal(hello.SupportedProtos, chromeALPN) || !slices.Contains(hello.Extensions, 27) {
|
||||
t.Errorf("the hello offered %v with extensions %v, not like Chrome", hello.SupportedProtos, hello.Extensions)
|
||||
}
|
||||
}
|
||||
|
||||
// parallel requests share one h2 connection instead of dialing one each, the
|
||||
// first ones as the later ones, and next to a host that answers over h1
|
||||
func TestServiceClientOneConnection(t *testing.T) {
|
||||
server := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
})
|
||||
h1 := newTLSServer(t, &gotls.Config{NextProtos: []string{"http/1.1"}}, func(w http.ResponseWriter, r *http.Request) {})
|
||||
client := newServiceClient(server.settings(&tls.Config{}), 5*time.Second, driveMaxInflight)
|
||||
get := func(url string) {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
for i := range 2 {
|
||||
if i == 1 {
|
||||
get(h1.URL)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for range driveMaxInflight {
|
||||
wg.Go(func() { get(server.URL) })
|
||||
}
|
||||
wg.Wait()
|
||||
if n := server.conns.Load(); n != 1 {
|
||||
t.Errorf("round %d: %d parallel requests took %d connections", i, driveMaxInflight, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// in the format of xray tls ech, for public name public.example
|
||||
const (
|
||||
testECHConfigList = "AEH+DQA9AAAgACBziE85FLxJySxZAPRHs8CDCbuz1h76I9pLhctC/kgScAAEAAEAAQAOcHVibGljLmV4YW1wbGUAAA=="
|
||||
testECHServerKeys = "ACD2AjC5yuBQ3430geNDLdIRndloCry/DBb9ZeYp0TmeowBB/g0APQAAIAAgc4hPORS8ScksWQD0R7PAgwm7s9Ye+iPaS4XLQv5IEnAABAABAAEADnB1YmxpYy5leGFtcGxlAAA="
|
||||
)
|
||||
|
||||
// a server that speaks only h1, or gets ALPN http/1.1 inside ECH or from Go TLS,
|
||||
// answers over h1, and after that answer parallel requests get more than one
|
||||
// connection
|
||||
func TestServiceClientHTTP1(t *testing.T) {
|
||||
keys, err := base64.StdEncoding.DecodeString(testECHServerKeys)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
echKeys, err := tls.ConvertToGoECHKeys(keys)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ech := &tls.Config{ServerName: "example.com", NextProtocol: []string{"http/1.1"}, EchConfigList: testECHConfigList}
|
||||
|
||||
for _, c := range []struct {
|
||||
protos, seen []string
|
||||
config *tls.Config
|
||||
}{
|
||||
{[]string{"http/1.1"}, chromeALPN, &tls.Config{}},
|
||||
{chromeALPN, []string{"http/1.1"}, ech},
|
||||
{chromeALPN, []string{"http/1.1"}, &tls.Config{NextProtocol: []string{"http/1.1"}, Fingerprint: "unsafe"}},
|
||||
} {
|
||||
server := newTLSServer(t, &gotls.Config{NextProtos: c.protos, EncryptedClientHelloKeys: echKeys}, func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
})
|
||||
client := newServiceClient(server.settings(c.config), 5*time.Second, 8)
|
||||
get := func() {
|
||||
resp, err := client.Get(server.URL)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.Proto != "HTTP/1.1" || resp.TLS.ECHAccepted != (c.config.EchConfigList != "") {
|
||||
t.Errorf("server %v, ALPN %v: went over %s, ECH accepted %v", c.protos, c.seen, resp.Proto, resp.TLS.ECHAccepted)
|
||||
}
|
||||
}
|
||||
|
||||
get()
|
||||
first := server.conns.Load()
|
||||
var wg sync.WaitGroup
|
||||
for range 4 {
|
||||
wg.Go(get)
|
||||
}
|
||||
wg.Wait()
|
||||
if offered := server.lastHello().SupportedProtos; !slices.Equal(offered, c.seen) {
|
||||
t.Errorf("server %v: saw ALPN %v, want %v", c.protos, offered, c.seen)
|
||||
}
|
||||
if n := server.conns.Load() - first; n < 2 {
|
||||
t.Errorf("server %v, ALPN %v: 4 parallel requests over h1 took %d connections", c.protos, c.seen, n)
|
||||
}
|
||||
}
|
||||
|
||||
// without the keys the server gets the outer hello, which offers the ALPN of
|
||||
// Chrome
|
||||
outer := newTLSServer(t, &gotls.Config{NextProtos: chromeALPN}, func(w http.ResponseWriter, r *http.Request) {})
|
||||
if resp, err := newServiceClient(outer.settings(ech), 5*time.Second, 8).Get(outer.URL); err == nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
if hello := outer.lastHello(); hello == nil {
|
||||
t.Error("no outer hello")
|
||||
} else if !slices.Equal(hello.SupportedProtos, chromeALPN) {
|
||||
t.Errorf("the outer hello offered %v, want %v", hello.SupportedProtos, chromeALPN)
|
||||
}
|
||||
}
|
||||
|
||||
// muteFirst leaves the first connection it accepts unanswered
|
||||
type muteFirst struct {
|
||||
net.Listener
|
||||
muted net.Conn
|
||||
}
|
||||
|
||||
func (l *muteFirst) Accept() (net.Conn, error) {
|
||||
if l.muted == nil {
|
||||
conn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.muted = conn
|
||||
}
|
||||
return l.Listener.Accept()
|
||||
}
|
||||
|
||||
// a handshake that hangs ends after stall, and does not keep the host from
|
||||
// getting another connection
|
||||
func TestServiceClientHungHandshake(t *testing.T) {
|
||||
const stall = 200 * time.Millisecond
|
||||
for _, fingerprint := range []string{"", "unsafe"} {
|
||||
server := &tlsServer{Server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))}
|
||||
server.Listener = &muteFirst{Listener: server.Listener}
|
||||
server.StartTLS()
|
||||
t.Cleanup(server.Close)
|
||||
client := newServiceClient(server.settings(&tls.Config{Fingerprint: fingerprint}), stall, 8)
|
||||
|
||||
if _, err := client.Get(server.URL); err == nil {
|
||||
t.Fatalf("fingerprint %q: a request got through a handshake that hung", fingerprint)
|
||||
}
|
||||
for i := range 2 {
|
||||
resp, err := client.Get(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("fingerprint %q: request %d after a handshake that hung: %v", fingerprint, i, err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// a connection whose handshake failed is closed, not left to the garbage
|
||||
// collector
|
||||
func TestServiceClientClosesFailedHandshake(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
for _, fingerprint := range []string{"", "unsafe"} {
|
||||
settings := &internet.MemoryStreamConfig{ProtocolName: protocolName, SecuritySettings: &tls.Config{Fingerprint: fingerprint}}
|
||||
go newServiceClient(settings, 5*time.Second, 8).Get("https://" + ln.Addr().String())
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// not TLS, so the handshake fails at once
|
||||
conn.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
|
||||
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
||||
if _, err := io.Copy(io.Discard, conn); err != nil {
|
||||
t.Errorf("fingerprint %q: the client left the connection open: %v", fingerprint, err)
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type roundTrip func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (f roundTrip) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
|
||||
|
||||
@@ -26,8 +26,7 @@ const (
|
||||
driveMaxAttempts = 8
|
||||
driveMaxInflight = 32
|
||||
driveInlineLimit = 12000
|
||||
// driveTimeout ends a request that makes no progress for that long
|
||||
driveTimeout = 60 * time.Second
|
||||
driveTimeout = 60 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -154,7 +153,7 @@ func jitter(backoff time.Duration) time.Duration {
|
||||
if half <= 0 {
|
||||
return backoff
|
||||
}
|
||||
return half + time.Duration(dice.RollInt63n(int64(half)))
|
||||
return half + time.Duration(dice.Roll(int(half)))
|
||||
}
|
||||
|
||||
func rateLimited(payload []byte) bool {
|
||||
|
||||
@@ -42,7 +42,7 @@ func liveDriveConfig(t *testing.T) *Config {
|
||||
|
||||
config := &Config{
|
||||
RemoteFolder: secrets.Folder,
|
||||
Service: "google-drive",
|
||||
Service: "Google Drive",
|
||||
Secrets: []string{secrets.ClientID, secrets.ClientSecret, secrets.RefreshToken},
|
||||
SegmentBytes: 256 * 1024,
|
||||
FlushIntervalMs: 100,
|
||||
|
||||
@@ -317,7 +317,7 @@ func driveSettings() *internet.MemoryStreamConfig {
|
||||
ProtocolName: protocolName,
|
||||
ProtocolSettings: &Config{
|
||||
RemoteFolder: "folder-id",
|
||||
Service: "google-drive",
|
||||
Service: "Google Drive",
|
||||
Secrets: []string{"client", "secret", "refresh"},
|
||||
FlushIntervalMs: 5,
|
||||
PollIntervalMs: 5,
|
||||
@@ -564,16 +564,6 @@ func TestDriveSharedClient(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveOldServiceName(t *testing.T) {
|
||||
newFakeDrive(t)
|
||||
|
||||
settings := driveSettings()
|
||||
settings.ProtocolSettings.(*Config).Service = "Google Drive"
|
||||
if _, err := newStorage(settings); err != nil {
|
||||
t.Fatalf("newStorage: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveInlineListing(t *testing.T) {
|
||||
drive := newFakeDrive(t)
|
||||
storage := newDriveBackend(t)
|
||||
|
||||
@@ -35,7 +35,7 @@ func newStorage(streamSettings *internet.MemoryStreamConfig) (Storage, error) {
|
||||
switch config.Service {
|
||||
case "local":
|
||||
return newLocalStorage(config.RemoteFolder)
|
||||
case "google-drive", "Google Drive":
|
||||
case "Google Drive":
|
||||
return sharedStorage(streamSettings, config, func() (Storage, error) {
|
||||
return newDriveStorage(streamSettings, config)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user