From 95461cc14839d3a0b8e86c4d3f305235c6ba835d Mon Sep 17 00:00:00 2001 From: Artem Lytkin <146867384+4RH1T3CT0R7@users.noreply.github.com> Date: Sat, 17 Oct 2026 17:13:57 +0300 Subject: [PATCH] XDRIVE transport: Require VLESS Multiplex+Encryption; "Google Drive"->"google-drive" (#7182) https://github.com/XTLS/Xray-core/pull/6748#issuecomment-5752780799 --- infra/conf/transport_method.go | 6 +- infra/conf/transport_test.go | 102 ++++++++++++++++++- infra/conf/xray.go | 58 +++++++++++ transport/internet/xdrive/drive_live_test.go | 2 +- transport/internet/xdrive/drive_test.go | 12 ++- transport/internet/xdrive/storage.go | 2 +- 6 files changed, 176 insertions(+), 6 deletions(-) diff --git a/infra/conf/transport_method.go b/infra/conf/transport_method.go index d36d6e809..a7d79dcd7 100644 --- a/infra/conf/transport_method.go +++ b/infra/conf/transport_method.go @@ -1004,6 +1004,10 @@ 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") } @@ -1012,7 +1016,7 @@ func (c *XDriveConfig) Build() (proto.Message, error) { return nil, errors.New(`service "template" needs a "template" object`) } default: - return nil, errors.New("unsupported service") + return nil, errors.New(`unknown "service": "`, c.Service, `", use "google-drive", "template" or "local"`) } config := &xdrive.Config{ RemoteFolder: c.RemoteFolder, diff --git a/infra/conf/transport_test.go b/infra/conf/transport_test.go index 47d75970c..957186cc9 100644 --- a/infra/conf/transport_test.go +++ b/infra/conf/transport_test.go @@ -9,6 +9,7 @@ import ( . "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" ) @@ -322,8 +323,25 @@ 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 { - t.Fatal("Build accepted an unsupported service") + 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) + } } } @@ -415,3 +433,83 @@ 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) + } + } +} diff --git a/infra/conf/xray.go b/infra/conf/xray.go index d6617024b..6bf6ba444 100644 --- a/infra/conf/xray.go +++ b/infra/conf/xray.go @@ -17,7 +17,11 @@ 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 ( @@ -209,6 +213,9 @@ 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, @@ -262,6 +269,54 @@ 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 { @@ -340,6 +395,9 @@ 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 } diff --git a/transport/internet/xdrive/drive_live_test.go b/transport/internet/xdrive/drive_live_test.go index 6c7b3ce24..f652cd794 100644 --- a/transport/internet/xdrive/drive_live_test.go +++ b/transport/internet/xdrive/drive_live_test.go @@ -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, diff --git a/transport/internet/xdrive/drive_test.go b/transport/internet/xdrive/drive_test.go index db27962cf..b43235093 100644 --- a/transport/internet/xdrive/drive_test.go +++ b/transport/internet/xdrive/drive_test.go @@ -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,6 +564,16 @@ 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) diff --git a/transport/internet/xdrive/storage.go b/transport/internet/xdrive/storage.go index 0f3417fac..c83a8b5f1 100644 --- a/transport/internet/xdrive/storage.go +++ b/transport/internet/xdrive/storage.go @@ -35,7 +35,7 @@ func newStorage(streamSettings *internet.MemoryStreamConfig) (Storage, error) { switch config.Service { case "local": return newLocalStorage(config.RemoteFolder) - case "Google Drive": + case "google-drive", "Google Drive": return sharedStorage(streamSettings, config, func() (Storage, error) { return newDriveStorage(streamSettings, config) })