mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-05-26 07:49:07 +00:00
Config: Support abstract unix sockets in remote loader (#6111)
Replaces https://github.com/XTLS/Xray-core/pull/5200
This commit is contained in:
+3
-47
@@ -7,60 +7,16 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
"github.com/xtls/xray-core/features/routing"
|
||||
routing_session "github.com/xtls/xray-core/features/routing/session"
|
||||
)
|
||||
|
||||
// parseURL splits a webhook URL into an HTTP URL and an optional Unix socket
|
||||
// path. For regular http/https URLs the input is returned unchanged with an
|
||||
// empty socketPath. For Unix sockets the format is:
|
||||
//
|
||||
// /path/to/socket.sock:/http/path
|
||||
// @abstract:/http/path
|
||||
// @@padded:/http/path
|
||||
//
|
||||
// The :/ separator after the socket path delimits the HTTP request path.
|
||||
// If omitted, "/" is used.
|
||||
func parseURL(raw string) (httpURL, socketPath string) {
|
||||
if len(raw) == 0 || (!filepath.IsAbs(raw) && raw[0] != '@') {
|
||||
return raw, ""
|
||||
}
|
||||
if idx := strings.Index(raw, ":/"); idx >= 0 {
|
||||
return "http://localhost" + raw[idx+1:], raw[:idx]
|
||||
}
|
||||
return "http://localhost/", raw
|
||||
}
|
||||
|
||||
// resolveSocketPath applies platform-specific transformations to a Unix
|
||||
// socket path, matching the behaviour of the listen side in
|
||||
// transport/internet/system_listener.go.
|
||||
//
|
||||
// For abstract sockets (prefix @) on Linux/Android:
|
||||
// - single @ — used as-is (lock-free abstract socket)
|
||||
// - double @@ — stripped to single @ and padded to
|
||||
// syscall.RawSockaddrUnix{}.Path length (HAProxy compat)
|
||||
func resolveSocketPath(path string) string {
|
||||
if len(path) == 0 || path[0] != '@' {
|
||||
return path
|
||||
}
|
||||
if runtime.GOOS != "linux" && runtime.GOOS != "android" {
|
||||
return path
|
||||
}
|
||||
if len(path) > 1 && path[1] == '@' {
|
||||
fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path))
|
||||
copy(fullAddr, path[1:])
|
||||
return string(fullAddr)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func ptr[T any](v T) *T { return &v }
|
||||
|
||||
@@ -96,7 +52,7 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
httpURL, socketPath := parseURL(cfg.Url)
|
||||
httpURL, socketPath := utils.SplitHTTPUnixURL(cfg.Url)
|
||||
h := &WebhookNotifier{
|
||||
url: httpURL,
|
||||
deduplication: cfg.Deduplication,
|
||||
@@ -107,7 +63,7 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
|
||||
}
|
||||
|
||||
if socketPath != "" {
|
||||
dialAddr := resolveSocketPath(socketPath)
|
||||
dialAddr := utils.ResolveSocketPath(socketPath)
|
||||
h.client.Transport = &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// ResolveSocketPath applies platform-specific transformations to a Unix
|
||||
// socket path, matching the listen-side behaviour in
|
||||
// transport/internet/system_listener.go.
|
||||
//
|
||||
// For abstract sockets (prefix @) on Linux/Android:
|
||||
// - single @ — used as-is (lock-free abstract socket)
|
||||
// - double @@ — stripped to single @ and padded to
|
||||
// syscall.RawSockaddrUnix{}.Path length (HAProxy compat)
|
||||
//
|
||||
// Filesystem paths and abstract sockets on other platforms are returned
|
||||
// unchanged.
|
||||
func ResolveSocketPath(path string) string {
|
||||
if len(path) == 0 || path[0] != '@' {
|
||||
return path
|
||||
}
|
||||
if runtime.GOOS != "linux" && runtime.GOOS != "android" {
|
||||
return path
|
||||
}
|
||||
if len(path) > 1 && path[1] == '@' {
|
||||
fullAddr := make([]byte, len(syscall.RawSockaddrUnix{}.Path))
|
||||
copy(fullAddr, path[1:])
|
||||
return string(fullAddr)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// SplitHTTPUnixURL splits a target into an HTTP URL and an optional Unix
|
||||
// socket path. For regular http(s) URLs the input is returned unchanged
|
||||
// with an empty socketPath. For Unix sockets the format is:
|
||||
//
|
||||
// /path/to/socket.sock[:/http/path]
|
||||
// @abstract[:/http/path]
|
||||
// @@padded[:/http/path]
|
||||
//
|
||||
// The :/ separator delimits the socket path from the HTTP request path.
|
||||
// If omitted, "/" is used.
|
||||
func SplitHTTPUnixURL(raw string) (httpURL, socketPath string) {
|
||||
if len(raw) == 0 || (!filepath.IsAbs(raw) && raw[0] != '@') {
|
||||
return raw, ""
|
||||
}
|
||||
if idx := strings.Index(raw, ":/"); idx >= 0 {
|
||||
return "http://localhost" + raw[idx+1:], raw[:idx]
|
||||
}
|
||||
return "http://localhost/", raw
|
||||
}
|
||||
|
||||
Vendored
+63
-54
@@ -3,8 +3,8 @@ package external
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/common/utils"
|
||||
"github.com/xtls/xray-core/main/confloader"
|
||||
)
|
||||
|
||||
@@ -20,9 +21,10 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
|
||||
var data []byte
|
||||
switch {
|
||||
case strings.HasPrefix(arg, "http+unix://"):
|
||||
data, err = FetchUnixSocketHTTPContent(arg)
|
||||
errors.PrintDeprecatedFeatureWarning(`"http+unix://" prefix`, `direct Unix socket path (e.g. /path/socket.sock:/api or @abstract:/api)`)
|
||||
data, err = FetchHTTPContent(httpUnixToCanonical(arg))
|
||||
|
||||
case strings.HasPrefix(arg, "http://"), strings.HasPrefix(arg, "https://"):
|
||||
case isRemoteSource(arg):
|
||||
data, err = FetchHTTPContent(arg)
|
||||
|
||||
case arg == "stdin:":
|
||||
@@ -39,19 +41,38 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// FetchHTTPContent issues an HTTP GET against either a regular HTTP(S) URL
|
||||
// or a Unix socket HTTP endpoint.
|
||||
//
|
||||
// http(s)://host/api regular HTTP(S)
|
||||
// /path/to/socket.sock[:/api] filesystem socket
|
||||
// @abstract[:/api] abstract socket (Linux/Android)
|
||||
// @@padded[:/api] padded abstract socket (HAProxy compat)
|
||||
//
|
||||
// When the ":/" separator is omitted on a socket target, the request is
|
||||
// made to "/".
|
||||
func FetchHTTPContent(target string) ([]byte, error) {
|
||||
parsedTarget, err := url.Parse(target)
|
||||
httpURL, socketPath := utils.SplitHTTPUnixURL(target)
|
||||
|
||||
parsedTarget, err := url.Parse(httpURL)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid URL: ", target).Base(err)
|
||||
}
|
||||
|
||||
if s := strings.ToLower(parsedTarget.Scheme); s != "http" && s != "https" {
|
||||
return nil, errors.New("invalid scheme: ", parsedTarget.Scheme)
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
if socketPath != "" {
|
||||
dialAddr := utils.ResolveSocketPath(socketPath)
|
||||
client.Transport = &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "unix", dialAddr)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := client.Do(&http.Request{
|
||||
Method: "GET",
|
||||
URL: parsedTarget,
|
||||
@@ -74,58 +95,46 @@ func FetchHTTPContent(target string) ([]byte, error) {
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// Format: http+unix:///path/to/socket.sock/api/endpoint
|
||||
func FetchUnixSocketHTTPContent(target string) ([]byte, error) {
|
||||
path := strings.TrimPrefix(target, "http+unix://")
|
||||
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
return nil, errors.New("unix socket path must be absolute")
|
||||
|
||||
// isRemoteSource reports whether arg should be fetched via HTTP (regular
|
||||
// network or Unix socket) rather than read from the local filesystem.
|
||||
// Recognized forms:
|
||||
//
|
||||
// - http(s)://... regular HTTP(S)
|
||||
// - @abstract[:/api] abstract socket (Linux/Android)
|
||||
// - /abs/path:/api filesystem socket, explicit HTTP path
|
||||
// - /abs/path filesystem socket detected via os.ModeSocket
|
||||
func isRemoteSource(arg string) bool {
|
||||
if arg == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
var socketPath, httpPath string
|
||||
|
||||
sockIdx := strings.Index(path, ".sock")
|
||||
if sockIdx != -1 {
|
||||
socketPath = path[:sockIdx+5]
|
||||
httpPath = path[sockIdx+5:]
|
||||
if httpPath == "" {
|
||||
httpPath = "/"
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("cannot determine socket path, socket file should have .sock extension")
|
||||
if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, err := os.Stat(socketPath); err != nil {
|
||||
return nil, errors.New("socket file not found: ", socketPath).Base(err)
|
||||
if arg[0] == '@' {
|
||||
return true
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "unix", socketPath)
|
||||
},
|
||||
},
|
||||
if arg[0] != '/' {
|
||||
return false
|
||||
}
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
resp, err := client.Get("http://localhost" + httpPath)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to fetch from unix socket: ", socketPath).Base(err)
|
||||
if strings.Contains(arg, ":/") {
|
||||
return true
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, errors.New("unexpected HTTP status code: ", resp.StatusCode)
|
||||
info, err := os.Stat(arg)
|
||||
return err == nil && info.Mode()&os.ModeSocket != 0
|
||||
}
|
||||
|
||||
|
||||
// httpUnixToCanonical converts the deprecated http+unix:///path/to/socket.sock/api
|
||||
// URL into the canonical /path/to/socket.sock:/api form by inserting ":"
|
||||
// between the ".sock" extension and the HTTP path. Inputs without a path
|
||||
// after ".sock" are returned with just the "http+unix://" prefix stripped.
|
||||
func httpUnixToCanonical(target string) string {
|
||||
raw := strings.TrimPrefix(target, "http+unix://")
|
||||
if i := strings.Index(raw, ".sock/"); i >= 0 {
|
||||
raw = raw[:i+5] + ":" + raw[i+5:]
|
||||
}
|
||||
|
||||
content, err := buf.ReadAllToBytes(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to read response").Base(err)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
return raw
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
Reference in New Issue
Block a user