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"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"account/internal/auth"
|
"account/internal/auth"
|
||||||
"account/internal/store"
|
"account/internal/store"
|
||||||
|
"account/internal/xrayconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultXHTTPPath = "/split"
|
||||||
|
defaultXHTTPMode = "auto"
|
||||||
|
defaultXHTTPPort = 443
|
||||||
|
defaultTCPPort = 1443
|
||||||
|
defaultTLSFP = "chrome"
|
||||||
|
defaultTCPFlow = "xtls-rprx-vision"
|
||||||
)
|
)
|
||||||
|
|
||||||
type vlessNode struct {
|
type vlessNode struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
Port int `json:"port,omitempty"`
|
Port int `json:"port,omitempty"`
|
||||||
Users []string `json:"users,omitempty"`
|
Users []string `json:"users,omitempty"`
|
||||||
Transport string `json:"transport,omitempty"`
|
Transport string `json:"transport,omitempty"`
|
||||||
Path string `json:"path,omitempty"`
|
Path string `json:"path,omitempty"`
|
||||||
Mode string `json:"mode,omitempty"`
|
Mode string `json:"mode,omitempty"`
|
||||||
Security string `json:"security,omitempty"`
|
Security string `json:"security,omitempty"`
|
||||||
Flow string `json:"flow,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) {
|
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
|
// Get current user ID to use as VLESS UUID
|
||||||
userID := auth.GetUserID(c)
|
userID := auth.GetUserID(c)
|
||||||
if userID == "" {
|
if userID == "" {
|
||||||
@ -55,7 +68,7 @@ func (h *handler) listAgentNodes(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyUUID := user.ProxyUUID
|
proxyUUID := strings.TrimSpace(user.ProxyUUID)
|
||||||
if proxyUUID == "" {
|
if proxyUUID == "" {
|
||||||
proxyUUID = user.ID
|
proxyUUID = user.ID
|
||||||
}
|
}
|
||||||
@ -68,47 +81,165 @@ func (h *handler) listAgentNodes(c *gin.Context) {
|
|||||||
return
|
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}
|
users := []string{proxyUUID}
|
||||||
nodes := make([]vlessNode, 0)
|
nodes := make([]vlessNode, 0, len(hosts))
|
||||||
|
for _, host := range hosts {
|
||||||
if h.publicURL != "" {
|
nodeName := nodeNameForHost(host)
|
||||||
u, err := url.Parse(h.publicURL)
|
nodes = append(nodes, vlessNode{
|
||||||
if err == nil {
|
Name: nodeName,
|
||||||
hostname := u.Hostname()
|
Address: host,
|
||||||
portStr := u.Port()
|
Port: xhttpPort,
|
||||||
port := 443
|
Users: users,
|
||||||
if portStr != "" {
|
Transport: "xhttp",
|
||||||
if p, err := strconv.Atoi(portStr); err == nil {
|
Path: xhttpPath,
|
||||||
port = p
|
Mode: xhttpMode,
|
||||||
}
|
Security: "tls",
|
||||||
} else if u.Scheme == "http" {
|
Flow: defaultTCPFlow,
|
||||||
port = 80
|
ServerName: host,
|
||||||
}
|
XHTTPPort: xhttpPort,
|
||||||
|
TCPPort: tcpPort,
|
||||||
// Add "Global Acceleration (XHTTP)" node
|
URISchemeXHTTP: renderVLESSURIScheme(xhttpScheme, map[string]string{
|
||||||
nodes = append(nodes, vlessNode{
|
"UUID": proxyUUID,
|
||||||
Name: "Global Acceleration (XHTTP)",
|
"DOMAIN": host,
|
||||||
Address: hostname,
|
"NODE": host,
|
||||||
Port: port, // Default port, client will adjust based on transport if needed
|
"PATH": url.QueryEscape(xhttpPath),
|
||||||
Users: users,
|
"MODE": url.QueryEscape(xhttpMode),
|
||||||
Transport: "xhttp",
|
"SNI": host,
|
||||||
Path: "/split",
|
"FP": defaultTLSFP,
|
||||||
Security: "tls",
|
"TAG": url.QueryEscape(nodeName),
|
||||||
Mode: "auto",
|
}),
|
||||||
})
|
URISchemeTCP: renderVLESSURIScheme(tcpScheme, map[string]string{
|
||||||
|
"UUID": proxyUUID,
|
||||||
// Add "Global Acceleration (TCP)" node
|
"DOMAIN": host,
|
||||||
nodes = append(nodes, vlessNode{
|
"NODE": host,
|
||||||
Name: "Global Acceleration (TCP)",
|
"SNI": host,
|
||||||
Address: hostname,
|
"FP": defaultTLSFP,
|
||||||
Port: 1443, // Fixed TCP port from template_tcp.json
|
"FLOW": defaultTCPFlow,
|
||||||
Users: users,
|
"TAG": url.QueryEscape(nodeName),
|
||||||
Transport: "tcp",
|
}),
|
||||||
Security: "tls",
|
})
|
||||||
Flow: "xtls-rprx-vision",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, nodes)
|
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
|
//go:embed template_xhttp.json
|
||||||
xhttpTemplateJSON []byte
|
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
|
// DefaultDefinition returns the built-in Xray configuration definition used when
|
||||||
@ -30,3 +36,13 @@ func TCPDefinition() Definition {
|
|||||||
func XHTTPDefinition() Definition {
|
func XHTTPDefinition() Definition {
|
||||||
return JSONDefinition{Raw: append([]byte(nil), xhttpTemplateJSON...)}
|
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