feat(vless): support multi-region URI schemes and node metadata
This commit is contained in:
parent
c7cbf43665
commit
5ff9d0ade0
@ -4,32 +4,45 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"account/internal/auth"
|
||||
"account/internal/store"
|
||||
"account/internal/xrayconfig"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultXHTTPPath = "/split"
|
||||
defaultXHTTPMode = "auto"
|
||||
defaultXHTTPPort = 443
|
||||
defaultTCPPort = 1443
|
||||
defaultTLSFP = "chrome"
|
||||
defaultTCPFlow = "xtls-rprx-vision"
|
||||
)
|
||||
|
||||
type vlessNode struct {
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Users []string `json:"users,omitempty"`
|
||||
Transport string `json:"transport,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Security string `json:"security,omitempty"`
|
||||
Flow string `json:"flow,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port,omitempty"`
|
||||
Users []string `json:"users,omitempty"`
|
||||
Transport string `json:"transport,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Security string `json:"security,omitempty"`
|
||||
Flow string `json:"flow,omitempty"`
|
||||
ServerName string `json:"server_name,omitempty"`
|
||||
XHTTPPort int `json:"xhttp_port,omitempty"`
|
||||
TCPPort int `json:"tcp_port,omitempty"`
|
||||
URISchemeXHTTP string `json:"uri_scheme_xhttp,omitempty"`
|
||||
URISchemeTCP string `json:"uri_scheme_tcp,omitempty"`
|
||||
}
|
||||
|
||||
func (h *handler) listAgentNodes(c *gin.Context) {
|
||||
// For now, valid nodes are derived from the server's public URL.
|
||||
// We currently assume the server itself exposes a VLESS/XHTTP endpoint.
|
||||
// In the future, we might retrieve this from the agent registry if agents report their public IPs.
|
||||
|
||||
// Get current user ID to use as VLESS UUID
|
||||
userID := auth.GetUserID(c)
|
||||
if userID == "" {
|
||||
@ -55,7 +68,7 @@ func (h *handler) listAgentNodes(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
proxyUUID := user.ProxyUUID
|
||||
proxyUUID := strings.TrimSpace(user.ProxyUUID)
|
||||
if proxyUUID == "" {
|
||||
proxyUUID = user.ID
|
||||
}
|
||||
@ -68,47 +81,165 @@ func (h *handler) listAgentNodes(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
hosts := parseProxyNodeHosts(h.publicURL)
|
||||
if len(hosts) == 0 {
|
||||
c.JSON(http.StatusOK, []vlessNode{})
|
||||
return
|
||||
}
|
||||
|
||||
xhttpPath := envOrDefault("XRAY_XHTTP_PATH", defaultXHTTPPath)
|
||||
xhttpMode := envOrDefault("XRAY_XHTTP_MODE", defaultXHTTPMode)
|
||||
xhttpPort := envIntOrDefault("XRAY_XHTTP_PORT", defaultXHTTPPort)
|
||||
tcpPort := envIntOrDefault("XRAY_TCP_PORT", defaultTCPPort)
|
||||
|
||||
xhttpScheme := xrayconfig.VLESSXHTTPScheme()
|
||||
tcpScheme := xrayconfig.VLESSTCPScheme()
|
||||
|
||||
users := []string{proxyUUID}
|
||||
nodes := make([]vlessNode, 0)
|
||||
|
||||
if h.publicURL != "" {
|
||||
u, err := url.Parse(h.publicURL)
|
||||
if err == nil {
|
||||
hostname := u.Hostname()
|
||||
portStr := u.Port()
|
||||
port := 443
|
||||
if portStr != "" {
|
||||
if p, err := strconv.Atoi(portStr); err == nil {
|
||||
port = p
|
||||
}
|
||||
} else if u.Scheme == "http" {
|
||||
port = 80
|
||||
}
|
||||
|
||||
// Add "Global Acceleration (XHTTP)" node
|
||||
nodes = append(nodes, vlessNode{
|
||||
Name: "Global Acceleration (XHTTP)",
|
||||
Address: hostname,
|
||||
Port: port, // Default port, client will adjust based on transport if needed
|
||||
Users: users,
|
||||
Transport: "xhttp",
|
||||
Path: "/split",
|
||||
Security: "tls",
|
||||
Mode: "auto",
|
||||
})
|
||||
|
||||
// Add "Global Acceleration (TCP)" node
|
||||
nodes = append(nodes, vlessNode{
|
||||
Name: "Global Acceleration (TCP)",
|
||||
Address: hostname,
|
||||
Port: 1443, // Fixed TCP port from template_tcp.json
|
||||
Users: users,
|
||||
Transport: "tcp",
|
||||
Security: "tls",
|
||||
Flow: "xtls-rprx-vision",
|
||||
})
|
||||
}
|
||||
nodes := make([]vlessNode, 0, len(hosts))
|
||||
for _, host := range hosts {
|
||||
nodeName := nodeNameForHost(host)
|
||||
nodes = append(nodes, vlessNode{
|
||||
Name: nodeName,
|
||||
Address: host,
|
||||
Port: xhttpPort,
|
||||
Users: users,
|
||||
Transport: "xhttp",
|
||||
Path: xhttpPath,
|
||||
Mode: xhttpMode,
|
||||
Security: "tls",
|
||||
Flow: defaultTCPFlow,
|
||||
ServerName: host,
|
||||
XHTTPPort: xhttpPort,
|
||||
TCPPort: tcpPort,
|
||||
URISchemeXHTTP: renderVLESSURIScheme(xhttpScheme, map[string]string{
|
||||
"UUID": proxyUUID,
|
||||
"DOMAIN": host,
|
||||
"NODE": host,
|
||||
"PATH": url.QueryEscape(xhttpPath),
|
||||
"MODE": url.QueryEscape(xhttpMode),
|
||||
"SNI": host,
|
||||
"FP": defaultTLSFP,
|
||||
"TAG": url.QueryEscape(nodeName),
|
||||
}),
|
||||
URISchemeTCP: renderVLESSURIScheme(tcpScheme, map[string]string{
|
||||
"UUID": proxyUUID,
|
||||
"DOMAIN": host,
|
||||
"NODE": host,
|
||||
"SNI": host,
|
||||
"FP": defaultTLSFP,
|
||||
"FLOW": defaultTCPFlow,
|
||||
"TAG": url.QueryEscape(nodeName),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, nodes)
|
||||
}
|
||||
|
||||
func parseProxyNodeHosts(publicURL string) []string {
|
||||
seen := make(map[string]struct{})
|
||||
hosts := make([]string, 0)
|
||||
|
||||
appendHost := func(raw string) {
|
||||
host := normalizeHost(raw)
|
||||
if host == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[host]; ok {
|
||||
return
|
||||
}
|
||||
seen[host] = struct{}{}
|
||||
hosts = append(hosts, host)
|
||||
}
|
||||
|
||||
if raw := strings.TrimSpace(os.Getenv("XRAY_PROXY_NODES")); raw != "" {
|
||||
fields := strings.FieldsFunc(raw, func(r rune) bool {
|
||||
return r == ',' || r == ';' || r == '\n' || r == '\t' || r == ' '
|
||||
})
|
||||
for _, field := range fields {
|
||||
appendHost(field)
|
||||
}
|
||||
}
|
||||
|
||||
if len(hosts) == 0 {
|
||||
appendHost(publicURL)
|
||||
}
|
||||
|
||||
return hosts
|
||||
}
|
||||
|
||||
func normalizeHost(raw string) string {
|
||||
value := strings.TrimSpace(raw)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if strings.Contains(value, "://") {
|
||||
u, err := url.Parse(value)
|
||||
if err == nil {
|
||||
value = u.Hostname()
|
||||
}
|
||||
}
|
||||
|
||||
value = strings.TrimPrefix(value, "https://")
|
||||
value = strings.TrimPrefix(value, "http://")
|
||||
if strings.Contains(value, "/") {
|
||||
parts := strings.SplitN(value, "/", 2)
|
||||
value = parts[0]
|
||||
}
|
||||
if strings.Contains(value, ":") {
|
||||
host, _, found := strings.Cut(value, ":")
|
||||
if found {
|
||||
value = host
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func envOrDefault(key, fallback string) string {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func envIntOrDefault(key string, fallback int) int {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil || parsed <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func nodeNameForHost(host string) string {
|
||||
prefix := host
|
||||
if idx := strings.Index(prefix, "."); idx > 0 {
|
||||
prefix = prefix[:idx]
|
||||
}
|
||||
prefix = strings.TrimSpace(prefix)
|
||||
if prefix == "" {
|
||||
prefix = host
|
||||
}
|
||||
prefix = strings.ReplaceAll(prefix, "_", "-")
|
||||
prefix = strings.ToUpper(prefix)
|
||||
return prefix + "-NODE"
|
||||
}
|
||||
|
||||
func renderVLESSURIScheme(tpl string, values map[string]string) string {
|
||||
rendered := strings.TrimSpace(tpl)
|
||||
if rendered == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
for key, value := range values {
|
||||
rendered = strings.ReplaceAll(rendered, "${"+key+"}", value)
|
||||
}
|
||||
return rendered
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
vless://${UUID}@${NODE}.svc.plus:443?encryption=none&type=xhttp&security=tls&host=${NODE}.svc.plus&path=${PATH}&mode=auto&sni=${NODE}.svc.plus&alpn=h2%2Chttp%2F1.1%2Ch3#${TAG}
|
||||
vless://${UUID}@${DOMAIN}:1443?encryption=none&type=tcp&security=tls&sni=${SNI}&fp=${FP}&flow=${FLOW}#${TAG}
|
||||
|
||||
@ -1 +1 @@
|
||||
vless://${UUID}@${DOMAIN}:443?encryption=none&type=xhttp&security=tls&host=${DOMAIN}&path=${PATH}&mode=auto&sni=${DOMAIN}&alpn=h2%2Chttp%2F1.1%2Ch3#${TAG}
|
||||
vless://${UUID}@${DOMAIN}:443?encryption=none&type=xhttp&security=tls&host=${DOMAIN}&path=${PATH}&mode=${MODE}&sni=${SNI}&fp=${FP}&alpn=h2%2Chttp%2F1.1%2Ch3#${TAG}
|
||||
|
||||
79
internal/xrayconfig/template_tcp.json
Normal file
79
internal/xrayconfig/template_tcp.json
Normal file
@ -0,0 +1,79 @@
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "warning"
|
||||
},
|
||||
"routing": {
|
||||
"domainStrategy": "IPIfNonMatch",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"ip": [
|
||||
"geoip:cn"
|
||||
],
|
||||
"outboundTag": "block"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"listen": "0.0.0.0",
|
||||
"port": 1443,
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"clients": [],
|
||||
"decryption": "none",
|
||||
"fallbacks": [
|
||||
{
|
||||
"dest": "8001",
|
||||
"xver": 1
|
||||
},
|
||||
{
|
||||
"alpn": "h2",
|
||||
"dest": "8002",
|
||||
"xver": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp",
|
||||
"security": "tls",
|
||||
"tlsSettings": {
|
||||
"rejectUnknownSni": true,
|
||||
"minVersion": "1.2",
|
||||
"certificates": [
|
||||
{
|
||||
"ocspStapling": 3600,
|
||||
"certificateFile": "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/{{.Domain}}/{{.Domain}}.crt",
|
||||
"keyFile": "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/{{.Domain}}/{{.Domain}}.key"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"sniffing": {
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"tag": "direct"
|
||||
},
|
||||
{
|
||||
"protocol": "blackhole",
|
||||
"tag": "block"
|
||||
}
|
||||
],
|
||||
"policy": {
|
||||
"levels": {
|
||||
"0": {
|
||||
"handshake": 2,
|
||||
"connIdle": 120
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
internal/xrayconfig/template_xhttp.json
Normal file
46
internal/xrayconfig/template_xhttp.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "debug"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"listen": "/dev/shm/xray.sock,0666",
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"clients": [],
|
||||
"decryption": "none"
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "xhttp",
|
||||
"xhttpSettings": {
|
||||
"mode": "auto",
|
||||
"path": "/split"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "direct",
|
||||
"protocol": "freedom",
|
||||
"settings": {}
|
||||
},
|
||||
{
|
||||
"tag": "blocked",
|
||||
"protocol": "blackhole",
|
||||
"settings": {}
|
||||
}
|
||||
],
|
||||
"routing": {
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"ip": [
|
||||
"geoip:private"
|
||||
],
|
||||
"outboundTag": "blocked"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,12 @@ var (
|
||||
|
||||
//go:embed template_xhttp.json
|
||||
xhttpTemplateJSON []byte
|
||||
|
||||
//go:embed VLESS-TCP-URI.Scheme
|
||||
vlessTCPScheme []byte
|
||||
|
||||
//go:embed VLESS-XHTTP-URI.Scheme
|
||||
vlessXHTTPScheme []byte
|
||||
)
|
||||
|
||||
// DefaultDefinition returns the built-in Xray configuration definition used when
|
||||
@ -30,3 +36,13 @@ func TCPDefinition() Definition {
|
||||
func XHTTPDefinition() Definition {
|
||||
return JSONDefinition{Raw: append([]byte(nil), xhttpTemplateJSON...)}
|
||||
}
|
||||
|
||||
// VLESSTCPScheme returns the embedded VLESS URI template for TCP transport.
|
||||
func VLESSTCPScheme() string {
|
||||
return string(append([]byte(nil), vlessTCPScheme...))
|
||||
}
|
||||
|
||||
// VLESSXHTTPScheme returns the embedded VLESS URI template for XHTTP transport.
|
||||
func VLESSXHTTPScheme() string {
|
||||
return string(append([]byte(nil), vlessXHTTPScheme...))
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user