feat(xray): support the XDRIVE s3 storage service

xray-core 26.10.31 (#7231) added an "s3" XDRIVE service that carries the
stream through S3-compatible storage: secrets are AccessKeyID,
SecretAccessKey and an optional SessionToken, an "s3" object names the
endpoint, region, bucket and pathStyle, and remoteFolder becomes the
object prefix.

The schema and the shared XDRIVE form offer the service with a field for
every S3 key, so an outbound or inbound keeps its storage on save. The
save path refused "s3" as an unknown service; it now runs the vendored
loader, whose ValidateS3Config would otherwise only fire when the
listener starts and stop every inbound. A node refuses a pushed S3
storage like a pushed template one, since its endpoint is any URL the
node would dial, including its own panel. Labels in all 14 locales;
docs updated in en/fa/ru/zh.
This commit is contained in:
MHSanaei
2026-11-01 14:56:26 +01:00
parent 87749c1ee4
commit 62d27d54d5
27 changed files with 309 additions and 24 deletions
+16 -9
View File
@@ -1028,18 +1028,21 @@ const (
// listening (stopping every inbound) or an outbound dials.
func validateXDriveStorage(streamSettings string) error {
var stream struct {
Network string `json:"network"`
XDriveSettings struct {
Service string `json:"service"`
RemoteFolder string `json:"remoteFolder"`
Secrets []string `json:"secrets"`
Template json.RawMessage `json:"template"`
} `json:"xdriveSettings"`
Network string `json:"network"`
XDriveSettings json.RawMessage `json:"xdriveSettings"`
}
if json.Unmarshal([]byte(streamSettings), &stream) != nil || stream.Network != "xdrive" {
return nil
}
xd := stream.XDriveSettings
var xd struct {
Service string `json:"service"`
RemoteFolder string `json:"remoteFolder"`
Secrets []string `json:"secrets"`
Template json.RawMessage `json:"template"`
}
if len(stream.XDriveSettings) > 0 && json.Unmarshal(stream.XDriveSettings, &xd) != nil {
return common.NewError("XDRIVE settings are not a valid object.")
}
hasFolder := strings.TrimSpace(xd.RemoteFolder) != ""
switch xd.Service {
case "local":
@@ -1058,8 +1061,12 @@ func validateXDriveStorage(streamSettings string) error {
return common.NewError(`XDRIVE service "template" needs a "template" object describing the storage API.`)
}
return validateXDriveTemplate(xd.Template)
case "s3":
if err := xray.ValidateXDriveS3(stream.XDriveSettings); err != nil {
return common.NewError("XDRIVE ", err)
}
default:
return common.NewError(fmt.Sprintf("unsupported XDRIVE service %q: use local, google-drive or template.", xd.Service))
return common.NewError(fmt.Sprintf("unsupported XDRIVE service %q: use local, google-drive, template or s3.", xd.Service))
}
return nil
}
+1 -1
View File
@@ -150,7 +150,7 @@ func hostReaches(inbound *model.Inbound) ([]pushReach, error) {
object("hysteria masquerade", "hysteriasettings", "masquerade")
case leaf.endsWith("masterkeylog") && lower != "" && lower != "none":
add("masterKeyLog", leaf)
case leaf.endsWith("xdrivesettings", "service") && (lower == "local" || lower == "template"):
case leaf.endsWith("xdrivesettings", "service") && (lower == "local" || lower == "template" || lower == "s3"):
object("xdrive service", "xdrivesettings")
case leaf.endsWith("echconfiglist") && strings.Contains(lower, "://"):
add("echConfigList", leaf)
+4
View File
@@ -252,6 +252,10 @@ func TestAddInboundRefusesPushedFieldsThatReachThisHost(t *testing.T) {
t.Errorf("%s: refused: %v", name, err)
}
}
s3 := xdriveInbound(44404, `{"service":"s3","remoteFolder":"x","secrets":["AKID","SECRET"],"s3":{"endpoint":"http://127.0.0.1:2053","region":"r","bucket":"b"}}`)
if _, _, err := push.AddInbound(s3); err == nil || !strings.Contains(err.Error(), "may not set xdrive service") {
t.Fatalf("pushed S3 storage on this host's own port = %v, want refused by the push guard", err)
}
if _, _, err := (&InboundService{}).AddInbound(&model.Inbound{Enable: true, Listen: "env:XUI_DB_DSN", Port: 44403, Protocol: model.VLESS, Settings: `{"clients":[]}`}); err == nil || !strings.Contains(err.Error(), "env:") {
t.Fatalf("env: listen = %v, want refused for every caller", err)
}
@@ -140,6 +140,10 @@ func TestAddInboundRefusesXDriveStorageTheCoreCannotOpen(t *testing.T) {
{"template without a delete op", `{"service":"template","template":{"put":{"url":"u"},"get":{"url":"u"},"list":{"url":"u","namesRegex":"(.+)"}}}`, "XDRIVE template needs put, get, list and delete operations"},
{"template without namesRegex", `{"service":"template","template":{"put":{"url":"u"},"get":{"url":"u"},"list":{"url":"u"},"delete":{"url":"u"}}}`, "XDRIVE template list needs a namesRegex"},
{"template namesRegex without a group", `{"service":"template","template":{"put":{"url":"u"},"get":{"url":"u"},"list":{"url":"u","namesRegex":"name"},"delete":{"url":"u"}}}`, "XDRIVE template namesRegex needs one capture group"},
{"s3 without an s3 object", `{"service":"s3","remoteFolder":"f","secrets":["key","secret"]}`, `service "s3" needs an "s3" object`},
{"s3 endpoint that is not http", `{"service":"s3","remoteFolder":"f","secrets":["key","secret"],"s3":{"endpoint":"ftp://s3.example","region":"r","bucket":"b"}}`, "S3 endpoint must be an http(s) URL"},
{"s3 with one secret", `{"service":"s3","remoteFolder":"f","secrets":["key"],"s3":{"endpoint":"https://s3.example","region":"r","bucket":"b"}}`, "S3 needs AccessKeyID, SecretAccessKey"},
{"s3 virtual-hosted on an IP endpoint", `{"service":"s3","remoteFolder":"f","secrets":["key","secret"],"s3":{"endpoint":"https://10.0.0.1","region":"r","bucket":"b","pathStyle":false}}`, "requires a DNS endpoint"},
{"unknown service", `{"service":"Dropbox","remoteFolder":"f"}`, `unsupported XDRIVE service "Dropbox"`},
}
for i, tc := range tests {
@@ -161,6 +165,14 @@ func TestAddInboundAcceptsACompleteXDriveTemplate(t *testing.T) {
}
}
func TestAddInboundAcceptsACompleteXDriveS3Storage(t *testing.T) {
setupBulkDB(t)
s3 := `{"service":"s3","remoteFolder":"xdrive","secrets":["AKID","SECRET"],"s3":{"endpoint":"https://storage.yandexcloud.net","region":"ru-central1","bucket":"example-bucket"}}`
if _, _, err := (&InboundService{}).AddInbound(xdriveInbound(18481, s3)); err != nil {
t.Fatalf("AddInbound of an S3 storage xray-core can open: %v", err)
}
}
func TestSavePathsRefuseXDriveWithoutStorage(t *testing.T) {
const want = `XDRIVE needs a "remoteFolder"`
const empty = `{"service":"local","remoteFolder":""}`