Apply inbounds on nodes via Xray with Reality settings and client sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
orohi
2026-07-29 05:11:45 +03:00
co-authored by Cursor
parent c41bae9852
commit 20a20168b6
15 changed files with 1081 additions and 63 deletions
+96 -8
View File
@@ -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()
}