mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-07-02 17:58:46 +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"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
"github.com/xtls/xray-core/common/utils"
|
||||||
"github.com/xtls/xray-core/features/routing"
|
"github.com/xtls/xray-core/features/routing"
|
||||||
routing_session "github.com/xtls/xray-core/features/routing/session"
|
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 }
|
func ptr[T any](v T) *T { return &v }
|
||||||
|
|
||||||
@@ -96,7 +52,7 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
httpURL, socketPath := parseURL(cfg.Url)
|
httpURL, socketPath := utils.SplitHTTPUnixURL(cfg.Url)
|
||||||
h := &WebhookNotifier{
|
h := &WebhookNotifier{
|
||||||
url: httpURL,
|
url: httpURL,
|
||||||
deduplication: cfg.Deduplication,
|
deduplication: cfg.Deduplication,
|
||||||
@@ -107,7 +63,7 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if socketPath != "" {
|
if socketPath != "" {
|
||||||
dialAddr := resolveSocketPath(socketPath)
|
dialAddr := utils.ResolveSocketPath(socketPath)
|
||||||
h.client.Transport = &http.Transport{
|
h.client.Transport = &http.Transport{
|
||||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||||
var d net.Dialer
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"net"
|
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/xtls/xray-core/common/buf"
|
"github.com/xtls/xray-core/common/buf"
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
|
"github.com/xtls/xray-core/common/utils"
|
||||||
"github.com/xtls/xray-core/main/confloader"
|
"github.com/xtls/xray-core/main/confloader"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,9 +21,10 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
|
|||||||
var data []byte
|
var data []byte
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(arg, "http+unix://"):
|
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)
|
data, err = FetchHTTPContent(arg)
|
||||||
|
|
||||||
case arg == "stdin:":
|
case arg == "stdin:":
|
||||||
@@ -39,19 +41,38 @@ func ConfigLoader(arg string) (out io.Reader, err error) {
|
|||||||
return
|
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) {
|
func FetchHTTPContent(target string) ([]byte, error) {
|
||||||
parsedTarget, err := url.Parse(target)
|
httpURL, socketPath := utils.SplitHTTPUnixURL(target)
|
||||||
|
|
||||||
|
parsedTarget, err := url.Parse(httpURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("invalid URL: ", target).Base(err)
|
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{
|
client := &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
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{
|
resp, err := client.Do(&http.Request{
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
URL: parsedTarget,
|
URL: parsedTarget,
|
||||||
@@ -74,58 +95,46 @@ func FetchHTTPContent(target string) ([]byte, error) {
|
|||||||
return content, nil
|
return content, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format: http+unix:///path/to/socket.sock/api/endpoint
|
|
||||||
func FetchUnixSocketHTTPContent(target string) ([]byte, error) {
|
// isRemoteSource reports whether arg should be fetched via HTTP (regular
|
||||||
path := strings.TrimPrefix(target, "http+unix://")
|
// network or Unix socket) rather than read from the local filesystem.
|
||||||
|
// Recognized forms:
|
||||||
if !strings.HasPrefix(path, "/") {
|
//
|
||||||
return nil, errors.New("unix socket path must be absolute")
|
// - 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
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://") {
|
||||||
var socketPath, httpPath string
|
return true
|
||||||
|
|
||||||
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 arg[0] == '@' {
|
||||||
if _, err := os.Stat(socketPath); err != nil {
|
return true
|
||||||
return nil, errors.New("socket file not found: ", socketPath).Base(err)
|
|
||||||
}
|
}
|
||||||
|
if arg[0] != '/' {
|
||||||
client := &http.Client{
|
return false
|
||||||
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)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
defer client.CloseIdleConnections()
|
if strings.Contains(arg, ":/") {
|
||||||
|
return true
|
||||||
resp, err := client.Get("http://localhost" + httpPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("failed to fetch from unix socket: ", socketPath).Base(err)
|
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
info, err := os.Stat(arg)
|
||||||
|
return err == nil && info.Mode()&os.ModeSocket != 0
|
||||||
if resp.StatusCode != 200 {
|
}
|
||||||
return nil, errors.New("unexpected HTTP status code: ", resp.StatusCode)
|
|
||||||
|
|
||||||
|
// 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:]
|
||||||
}
|
}
|
||||||
|
return raw
|
||||||
content, err := buf.ReadAllToBytes(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("failed to read response").Base(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return content, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
Reference in New Issue
Block a user