Apply inbounds on nodes via Xray with Reality settings and client sync.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+52
-9
@@ -2,6 +2,7 @@ package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
@@ -228,6 +229,8 @@ func ClientSubscriptionHosts(db *sql.DB, client *models.Client) ([]models.SubHos
|
||||
}
|
||||
rows, err := db.Query(`
|
||||
SELECT i.id, i.tag, i.remark, p.code, p.name, i.port, i.network, i.security,
|
||||
i.path, i.host, i.sni, i.fingerprint, i.flow, i.alpn,
|
||||
i.reality_public_key, i.reality_short_id, i.ss_method, i.password,
|
||||
COALESCE(n.host, ''), COALESCE(n.name, ''),
|
||||
COALESCE(n.status = 'online', FALSE),
|
||||
COALESCE(ni.enabled AND n.status = 'online', FALSE)
|
||||
@@ -250,7 +253,10 @@ ORDER BY i.sort_order ASC, i.tag ASC, n.name ASC NULLS LAST`, client.ID)
|
||||
var h models.SubHost
|
||||
if err := rows.Scan(
|
||||
&h.InboundID, &h.Tag, &h.Remark, &h.ProtocolCode, &h.ProtocolName,
|
||||
&h.Port, &h.Network, &h.Security, &h.Address, &h.NodeName,
|
||||
&h.Port, &h.Network, &h.Security,
|
||||
&h.Path, &h.HostHeader, &h.SNI, &h.Fingerprint, &h.Flow, &h.ALPN,
|
||||
&h.PublicKey, &h.ShortID, &h.SSMethod, &h.Password,
|
||||
&h.Address, &h.NodeName,
|
||||
&h.NodeOnline, &h.Available,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
@@ -266,26 +272,63 @@ 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(h.ProtocolCode, uid, h.Address, h.Port, h.Network, h.Security, client.Username+"-"+h.Tag)
|
||||
h.Link = buildShareLink(uid, client.Username+"-"+h.Tag, &h)
|
||||
}
|
||||
hosts = append(hosts, h)
|
||||
}
|
||||
return hosts, rows.Err()
|
||||
}
|
||||
|
||||
func buildShareLink(code, uid, host string, port int, network, security, name string) string {
|
||||
func buildShareLink(uid, name string, h *models.SubHost) string {
|
||||
name = url.QueryEscape(name)
|
||||
switch code {
|
||||
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 {
|
||||
case "vless":
|
||||
return fmt.Sprintf("vless://%s@%s:%d?encryption=none&type=%s&security=%s#%s", uid, host, port, network, security, name)
|
||||
q.Set("encryption", "none")
|
||||
return fmt.Sprintf("vless://%s@%s:%d?%s#%s", uid, h.Address, h.Port, q.Encode(), name)
|
||||
case "vmess":
|
||||
return fmt.Sprintf("vmess://%s@%s:%d?type=%s&security=%s#%s", uid, host, port, network, security, name)
|
||||
return fmt.Sprintf("vmess://%s@%s:%d?%s#%s", uid, h.Address, h.Port, q.Encode(), name)
|
||||
case "trojan":
|
||||
return fmt.Sprintf("trojan://%s@%s:%d?type=%s&security=%s#%s", uid, host, port, network, security, name)
|
||||
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)
|
||||
case "shadowsocks":
|
||||
return fmt.Sprintf("ss://%s@%s:%d#%s", uid, host, port, name)
|
||||
method := h.SSMethod
|
||||
if method == "" {
|
||||
method = "aes-128-gcm"
|
||||
}
|
||||
userInfo := base64.RawURLEncoding.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?type=%s&security=%s#%s", code, uid, host, port, network, security, name)
|
||||
return fmt.Sprintf("%s://%s@%s:%d?%s#%s", h.ProtocolCode, uid, h.Address, h.Port, q.Encode(), name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,19 @@ CREATE TABLE IF NOT EXISTS inbounds (
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
remark TEXT NOT NULL DEFAULT '',
|
||||
path TEXT NOT NULL DEFAULT '',
|
||||
host TEXT NOT NULL DEFAULT '',
|
||||
sni TEXT NOT NULL DEFAULT '',
|
||||
fingerprint TEXT NOT NULL DEFAULT 'chrome',
|
||||
flow TEXT NOT NULL DEFAULT '',
|
||||
alpn TEXT NOT NULL DEFAULT '',
|
||||
reality_dest TEXT NOT NULL DEFAULT '',
|
||||
reality_server_names TEXT NOT NULL DEFAULT '',
|
||||
reality_private_key TEXT NOT NULL DEFAULT '',
|
||||
reality_public_key TEXT NOT NULL DEFAULT '',
|
||||
reality_short_id TEXT NOT NULL DEFAULT '',
|
||||
ss_method TEXT NOT NULL DEFAULT 'aes-128-gcm',
|
||||
password TEXT NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (profile_id, tag)
|
||||
@@ -160,6 +173,25 @@ CREATE TABLE IF NOT EXISTS client_inbounds (
|
||||
// Additive migrations for existing deployments.
|
||||
_, _ = db.Exec(`ALTER TABLE nodes ADD COLUMN IF NOT EXISTS runtime_log TEXT NOT NULL DEFAULT ''`)
|
||||
_, _ = db.Exec(`ALTER TABLE nodes ADD COLUMN IF NOT EXISTS profile_id UUID REFERENCES config_profiles(id) ON DELETE SET NULL`)
|
||||
for _, q := range []string{
|
||||
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS path TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS host TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS sni TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS fingerprint TEXT NOT NULL DEFAULT 'chrome'`,
|
||||
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS flow TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS alpn TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS reality_dest TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS reality_server_names TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS reality_private_key TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS reality_public_key TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS reality_short_id TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS ss_method TEXT NOT NULL DEFAULT 'aes-128-gcm'`,
|
||||
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS password TEXT NOT NULL DEFAULT ''`,
|
||||
} {
|
||||
if _, err := db.Exec(q); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lib/pq"
|
||||
|
||||
"github.com/orohi/vpn-panel/internal/models"
|
||||
)
|
||||
@@ -310,15 +311,45 @@ ORDER BY p.sort_order`, nodeID)
|
||||
if !in.NodeEnabled || !in.Enabled {
|
||||
continue
|
||||
}
|
||||
clients, err := ListActiveClientsForInbound(db, in.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var clientList []map[string]any
|
||||
for _, c := range clients {
|
||||
clientList = append(clientList, map[string]any{
|
||||
"id": c.ID.String(),
|
||||
"uuid": c.UUID.String(),
|
||||
"email": c.Email,
|
||||
"username": c.Username,
|
||||
})
|
||||
}
|
||||
if clientList == nil {
|
||||
clientList = []map[string]any{}
|
||||
}
|
||||
inList = append(inList, map[string]any{
|
||||
"id": in.ID.String(),
|
||||
"tag": in.Tag,
|
||||
"protocol": in.ProtocolCode,
|
||||
"port": in.Port,
|
||||
"network": in.Network,
|
||||
"security": in.Security,
|
||||
"listen": in.Listen,
|
||||
"remark": in.Remark,
|
||||
"id": in.ID.String(),
|
||||
"tag": in.Tag,
|
||||
"protocol": in.ProtocolCode,
|
||||
"port": in.Port,
|
||||
"network": in.Network,
|
||||
"security": in.Security,
|
||||
"listen": in.Listen,
|
||||
"remark": in.Remark,
|
||||
"path": in.Path,
|
||||
"host": in.Host,
|
||||
"sni": in.SNI,
|
||||
"fingerprint": in.Fingerprint,
|
||||
"flow": in.Flow,
|
||||
"alpn": in.ALPN,
|
||||
"reality_dest": in.RealityDest,
|
||||
"reality_server_names": in.RealityServerNames,
|
||||
"reality_private_key": in.RealityPrivateKey,
|
||||
"reality_public_key": in.RealityPublicKey,
|
||||
"reality_short_id": in.RealityShortID,
|
||||
"ss_method": in.SSMethod,
|
||||
"password": in.Password,
|
||||
"clients": clientList,
|
||||
})
|
||||
}
|
||||
if inList == nil {
|
||||
@@ -329,3 +360,60 @@ ORDER BY p.sort_order`, nodeID)
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// ListActiveClientsForInbound returns active non-expired clients assigned to an inbound.
|
||||
func ListActiveClientsForInbound(db *sql.DB, inboundID uuid.UUID) ([]models.Client, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT c.id, c.username, c.email, c.uuid, c.status,
|
||||
c.traffic_limit_bytes, c.traffic_used_bytes, c.expire_at, c.sub_token, c.note,
|
||||
c.created_at, c.updated_at, 0
|
||||
FROM clients c
|
||||
JOIN client_inbounds ci ON ci.client_id = c.id AND ci.inbound_id = $1
|
||||
WHERE c.status = 'active'
|
||||
AND (c.expire_at IS NULL OR c.expire_at > NOW())
|
||||
AND (c.traffic_limit_bytes = 0 OR c.traffic_used_bytes < c.traffic_limit_bytes)
|
||||
ORDER BY c.username ASC`, inboundID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var list []models.Client
|
||||
for rows.Next() {
|
||||
c, err := scanClient(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, *c)
|
||||
}
|
||||
return list, rows.Err()
|
||||
}
|
||||
|
||||
// ListOnlineNodesByInboundIDs returns online nodes that have any of the inbounds enabled.
|
||||
func ListOnlineNodesByInboundIDs(db *sql.DB, inboundIDs []uuid.UUID) ([]models.Node, error) {
|
||||
if len(inboundIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
ids := make([]string, len(inboundIDs))
|
||||
for i, id := range inboundIDs {
|
||||
ids[i] = id.String()
|
||||
}
|
||||
rows, err := db.Query(`
|
||||
SELECT DISTINCT `+nodeColumns+`
|
||||
FROM nodes n
|
||||
JOIN node_inbounds ni ON ni.node_id = n.id AND ni.enabled = TRUE
|
||||
LEFT JOIN config_profiles cp ON cp.id = n.profile_id
|
||||
WHERE n.status = 'online' AND ni.inbound_id = ANY($1)`, pq.Array(ids))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var list []models.Node
|
||||
for rows.Next() {
|
||||
n, err := scanNode(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, *n)
|
||||
}
|
||||
return list, rows.Err()
|
||||
}
|
||||
|
||||
+43
-4
@@ -99,6 +99,9 @@ func scanInbound(scanner interface {
|
||||
err := scanner.Scan(
|
||||
&in.ID, &in.ProfileID, &in.Tag, &in.ProtocolID, &in.ProtocolCode, &in.ProtocolName,
|
||||
&in.Port, &in.Network, &in.Security, &in.Listen, &in.Enabled, &in.SortOrder, &in.Remark,
|
||||
&in.Path, &in.Host, &in.SNI, &in.Fingerprint, &in.Flow, &in.ALPN,
|
||||
&in.RealityDest, &in.RealityServerNames, &in.RealityPrivateKey, &in.RealityPublicKey, &in.RealityShortID,
|
||||
&in.SSMethod, &in.Password,
|
||||
&in.CreatedAt, &in.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -110,6 +113,9 @@ func scanInbound(scanner interface {
|
||||
const inboundColumns = `
|
||||
i.id, i.profile_id, i.tag, i.protocol_id, p.code, p.name,
|
||||
i.port, i.network, i.security, i.listen, i.enabled, i.sort_order, i.remark,
|
||||
i.path, i.host, i.sni, i.fingerprint, i.flow, i.alpn,
|
||||
i.reality_dest, i.reality_server_names, i.reality_private_key, i.reality_public_key, i.reality_short_id,
|
||||
i.ss_method, i.password,
|
||||
i.created_at, i.updated_at`
|
||||
|
||||
func ListInboundsByProfile(db *sql.DB, profileID uuid.UUID) ([]models.Inbound, error) {
|
||||
@@ -163,27 +169,57 @@ func CreateInbound(db *sql.DB, in *models.Inbound) error {
|
||||
if in.Listen == "" {
|
||||
in.Listen = "0.0.0.0"
|
||||
}
|
||||
if in.Fingerprint == "" {
|
||||
in.Fingerprint = "chrome"
|
||||
}
|
||||
if in.SSMethod == "" {
|
||||
in.SSMethod = "aes-128-gcm"
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
in.CreatedAt = now
|
||||
in.UpdatedAt = now
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO inbounds (
|
||||
id, profile_id, tag, protocol_id, port, network, security, listen, enabled, sort_order, remark, created_at, updated_at
|
||||
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)`,
|
||||
id, profile_id, tag, protocol_id, port, network, security, listen, enabled, sort_order, remark,
|
||||
path, host, sni, fingerprint, flow, alpn,
|
||||
reality_dest, reality_server_names, reality_private_key, reality_public_key, reality_short_id,
|
||||
ss_method, password, created_at, updated_at
|
||||
) VALUES (
|
||||
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,
|
||||
$12,$13,$14,$15,$16,$17,
|
||||
$18,$19,$20,$21,$22,
|
||||
$23,$24,$25,$26
|
||||
)`,
|
||||
in.ID, in.ProfileID, in.Tag, in.ProtocolID, in.Port, in.Network, in.Security, in.Listen,
|
||||
in.Enabled, in.SortOrder, in.Remark, in.CreatedAt, in.UpdatedAt,
|
||||
in.Enabled, in.SortOrder, in.Remark,
|
||||
in.Path, in.Host, in.SNI, in.Fingerprint, in.Flow, in.ALPN,
|
||||
in.RealityDest, in.RealityServerNames, in.RealityPrivateKey, in.RealityPublicKey, in.RealityShortID,
|
||||
in.SSMethod, in.Password, in.CreatedAt, in.UpdatedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func UpdateInbound(db *sql.DB, in *models.Inbound) error {
|
||||
if in.Fingerprint == "" {
|
||||
in.Fingerprint = "chrome"
|
||||
}
|
||||
if in.SSMethod == "" {
|
||||
in.SSMethod = "aes-128-gcm"
|
||||
}
|
||||
_, err := db.Exec(`
|
||||
UPDATE inbounds SET
|
||||
tag = $2, protocol_id = $3, port = $4, network = $5, security = $6,
|
||||
listen = $7, enabled = $8, sort_order = $9, remark = $10, updated_at = NOW()
|
||||
listen = $7, enabled = $8, sort_order = $9, remark = $10,
|
||||
path = $11, host = $12, sni = $13, fingerprint = $14, flow = $15, alpn = $16,
|
||||
reality_dest = $17, reality_server_names = $18, reality_private_key = $19,
|
||||
reality_public_key = $20, reality_short_id = $21, ss_method = $22, password = $23,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`,
|
||||
in.ID, in.Tag, in.ProtocolID, in.Port, in.Network, in.Security,
|
||||
in.Listen, in.Enabled, in.SortOrder, in.Remark,
|
||||
in.Path, in.Host, in.SNI, in.Fingerprint, in.Flow, in.ALPN,
|
||||
in.RealityDest, in.RealityServerNames, in.RealityPrivateKey, in.RealityPublicKey, in.RealityShortID,
|
||||
in.SSMethod, in.Password,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -270,6 +306,9 @@ ORDER BY i.sort_order ASC, i.tag ASC`, nodeID, *n.ProfileID)
|
||||
if err := rows.Scan(
|
||||
&in.ID, &in.ProfileID, &in.Tag, &in.ProtocolID, &in.ProtocolCode, &in.ProtocolName,
|
||||
&in.Port, &in.Network, &in.Security, &in.Listen, &in.Enabled, &in.SortOrder, &in.Remark,
|
||||
&in.Path, &in.Host, &in.SNI, &in.Fingerprint, &in.Flow, &in.ALPN,
|
||||
&in.RealityDest, &in.RealityServerNames, &in.RealityPrivateKey, &in.RealityPublicKey, &in.RealityShortID,
|
||||
&in.SSMethod, &in.Password,
|
||||
&in.CreatedAt, &in.UpdatedAt, &in.NodeEnabled,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user