Fix VLESS share links to Remnawave format and default inbound to VLESS.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-29 05:26:00 +03:00
co-authored by Cursor
parent 20a20168b6
commit b48b936c27
3 changed files with 171 additions and 49 deletions
+138 -39
View File
@@ -5,6 +5,7 @@ import (
"encoding/base64"
"fmt"
"net/url"
"strings"
"time"
"github.com/google/uuid"
@@ -272,66 +273,164 @@ ORDER BY i.sort_order ASC, i.tag ASC, n.name ASC NULLS LAST`, client.ID)
seen[key] = true
if h.Available && h.Address != "" {
h.Link = buildShareLink(uid, client.Username+"-"+h.Tag, &h)
h.Link = buildShareLink(uid, hostDisplayName(client.Username, &h), &h)
}
hosts = append(hosts, h)
}
return hosts, rows.Err()
}
func hostDisplayName(username string, h *models.SubHost) string {
if h.Remark != "" {
return h.Remark
}
if h.NodeName != "" {
return h.NodeName
}
if h.Tag != "" {
return h.Tag
}
return username
}
// buildShareLink builds Remnawave-compatible share URIs.
// Query keys are intentionally ordered (not alphabetically).
func buildShareLink(uid, name string, h *models.SubHost) string {
name = url.QueryEscape(name)
q := url.Values{}
q.Set("type", h.Network)
q.Set("security", h.Security)
if h.Path != "" {
q.Set("path", h.Path)
}
if h.HostHeader != "" {
q.Set("host", h.HostHeader)
}
if h.SNI != "" {
q.Set("sni", h.SNI)
}
if h.Fingerprint != "" {
q.Set("fp", h.Fingerprint)
}
if h.Flow != "" {
q.Set("flow", h.Flow)
}
if h.ALPN != "" {
q.Set("alpn", h.ALPN)
}
if h.PublicKey != "" {
q.Set("pbk", h.PublicKey)
}
if h.ShortID != "" {
q.Set("sid", h.ShortID)
}
switch h.ProtocolCode {
switch strings.ToLower(h.ProtocolCode) {
case "vless":
q.Set("encryption", "none")
return fmt.Sprintf("vless://%s@%s:%d?%s#%s", uid, h.Address, h.Port, q.Encode(), name)
return buildVLESSLink(uid, name, h)
case "vmess":
return fmt.Sprintf("vmess://%s@%s:%d?%s#%s", uid, h.Address, h.Port, q.Encode(), name)
return buildVMessLink(uid, name, h)
case "trojan":
pass := uid
if h.Password != "" {
pass = h.Password
}
return fmt.Sprintf("trojan://%s@%s:%d?%s#%s", pass, h.Address, h.Port, q.Encode(), name)
return buildTrojanLink(uid, name, h)
case "shadowsocks":
method := h.SSMethod
if method == "" {
method = "aes-128-gcm"
}
userInfo := base64.RawURLEncoding.EncodeToString([]byte(method + ":" + uid))
userInfo := base64.StdEncoding.EncodeToString([]byte(method + ":" + uid))
return fmt.Sprintf("ss://%s@%s:%d#%s", userInfo, h.Address, h.Port, name)
default:
return fmt.Sprintf("%s://%s@%s:%d?%s#%s", h.ProtocolCode, uid, h.Address, h.Port, q.Encode(), name)
// Non-Xray protocols are not shareable as vless-style URIs.
return ""
}
}
func buildVLESSLink(uid, name string, h *models.SubHost) string {
network := h.Network
if network == "" {
network = "tcp"
}
security := h.Security
if security == "" {
security = "none"
}
flow := h.Flow
if flow == "" && security == "reality" && network == "tcp" {
flow = "xtls-rprx-vision"
}
fp := h.Fingerprint
if fp == "" && (security == "reality" || security == "tls") {
fp = "chrome"
}
var parts []string
parts = append(parts, "encryption=none")
if flow != "" {
parts = append(parts, "flow="+url.QueryEscape(flow))
}
parts = append(parts, "type="+url.QueryEscape(network))
parts = append(parts, "security="+url.QueryEscape(security))
if h.Path != "" {
parts = append(parts, "path="+url.QueryEscape(h.Path))
}
if h.HostHeader != "" {
parts = append(parts, "host="+url.QueryEscape(h.HostHeader))
}
if h.SNI != "" {
parts = append(parts, "sni="+url.QueryEscape(h.SNI))
}
if fp != "" {
parts = append(parts, "fp="+url.QueryEscape(fp))
}
if h.ALPN != "" {
parts = append(parts, "alpn="+url.QueryEscape(h.ALPN))
}
if h.PublicKey != "" {
parts = append(parts, "pbk="+url.QueryEscape(h.PublicKey))
}
if h.ShortID != "" {
parts = append(parts, "sid="+url.QueryEscape(h.ShortID))
}
return fmt.Sprintf("vless://%s@%s:%d?%s#%s", uid, h.Address, h.Port, strings.Join(parts, "&"), name)
}
func buildVMessLink(uid, name string, h *models.SubHost) string {
network := orDefault(h.Network, "tcp")
security := orDefault(h.Security, "none")
var parts []string
parts = append(parts, "type="+url.QueryEscape(network))
parts = append(parts, "security="+url.QueryEscape(security))
if h.Path != "" {
parts = append(parts, "path="+url.QueryEscape(h.Path))
}
if h.HostHeader != "" {
parts = append(parts, "host="+url.QueryEscape(h.HostHeader))
}
if h.SNI != "" {
parts = append(parts, "sni="+url.QueryEscape(h.SNI))
}
if h.Fingerprint != "" {
parts = append(parts, "fp="+url.QueryEscape(h.Fingerprint))
}
if h.PublicKey != "" {
parts = append(parts, "pbk="+url.QueryEscape(h.PublicKey))
}
if h.ShortID != "" {
parts = append(parts, "sid="+url.QueryEscape(h.ShortID))
}
return fmt.Sprintf("vmess://%s@%s:%d?%s#%s", uid, h.Address, h.Port, strings.Join(parts, "&"), name)
}
func buildTrojanLink(uid, name string, h *models.SubHost) string {
pass := uid
if h.Password != "" {
pass = h.Password
}
network := orDefault(h.Network, "tcp")
security := orDefault(h.Security, "tls")
var parts []string
parts = append(parts, "type="+url.QueryEscape(network))
parts = append(parts, "security="+url.QueryEscape(security))
if h.Path != "" {
parts = append(parts, "path="+url.QueryEscape(h.Path))
}
if h.HostHeader != "" {
parts = append(parts, "host="+url.QueryEscape(h.HostHeader))
}
if h.SNI != "" {
parts = append(parts, "sni="+url.QueryEscape(h.SNI))
}
if h.Fingerprint != "" {
parts = append(parts, "fp="+url.QueryEscape(h.Fingerprint))
}
if h.PublicKey != "" {
parts = append(parts, "pbk="+url.QueryEscape(h.PublicKey))
}
if h.ShortID != "" {
parts = append(parts, "sid="+url.QueryEscape(h.ShortID))
}
return fmt.Sprintf("trojan://%s@%s:%d?%s#%s", pass, h.Address, h.Port, strings.Join(parts, "&"), name)
}
func orDefault(v, def string) string {
if v == "" {
return def
}
return v
}
// ClientSubscriptionLinks builds share links for available hosts only.
func ClientSubscriptionLinks(db *sql.DB, client *models.Client) ([]string, error) {
hosts, err := ClientSubscriptionHosts(db, client)