Run official Xray-core on nodes with full Reality, TLS, and routing.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -13,7 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultVersion = "0.2.0"
|
const defaultVersion = "0.3.0"
|
||||||
|
|
||||||
type Agent struct {
|
type Agent struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
+53
-289
@@ -8,18 +8,47 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/orohi/vpn-panel/internal/xraycfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
const xrayContainer = "vpn-panel-xray"
|
const xrayContainer = "vpn-panel-xray"
|
||||||
const xrayImage = "teddysun/xray:latest"
|
|
||||||
|
// Official Xray-core image: https://github.com/XTLS/Xray-core
|
||||||
|
const xrayImage = "ghcr.io/xtls/xray-core:latest"
|
||||||
|
|
||||||
func (a *Agent) applyXray(cfg map[string]any) {
|
func (a *Agent) applyXray(cfg map[string]any) {
|
||||||
xrayDir := filepath.Join(a.dataDir, "xray")
|
xrayDir := filepath.Join(a.dataDir, "xray")
|
||||||
_ = os.MkdirAll(xrayDir, 0o755)
|
certDir := filepath.Join(xrayDir, "certs")
|
||||||
|
_ = os.MkdirAll(certDir, 0o755)
|
||||||
configPath := filepath.Join(xrayDir, "config.json")
|
configPath := filepath.Join(xrayDir, "config.json")
|
||||||
|
|
||||||
|
var list []xraycfg.Inbound
|
||||||
inboundsRaw, _ := cfg["inbounds"].([]any)
|
inboundsRaw, _ := cfg["inbounds"].([]any)
|
||||||
xrayCfg, n := buildXrayConfig(inboundsRaw)
|
for _, raw := range inboundsRaw {
|
||||||
|
m, ok := raw.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
in := xraycfg.FromMap(m)
|
||||||
|
if in.TLSCertPEM != "" && in.TLSKeyPEM != "" {
|
||||||
|
id := in.ID
|
||||||
|
if id == "" {
|
||||||
|
id = sanitizeFile(in.Tag)
|
||||||
|
}
|
||||||
|
_ = os.WriteFile(filepath.Join(certDir, id+".crt"), []byte(in.TLSCertPEM), 0o600)
|
||||||
|
_ = os.WriteFile(filepath.Join(certDir, id+".key"), []byte(in.TLSKeyPEM), 0o600)
|
||||||
|
}
|
||||||
|
list = append(list, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paths inside the Xray container
|
||||||
|
containerCertDir := "/usr/local/etc/xray/certs"
|
||||||
|
xrayCfg, n, err := xraycfg.Build(list, containerCertDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("xray build: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
b, err := json.MarshalIndent(xrayCfg, "", " ")
|
b, err := json.MarshalIndent(xrayCfg, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("xray marshal: %v", err)
|
log.Printf("xray marshal: %v", err)
|
||||||
@@ -29,13 +58,14 @@ func (a *Agent) applyXray(cfg map[string]any) {
|
|||||||
log.Printf("xray write config: %v", err)
|
log.Printf("xray write config: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("xray config written: %d inbound(s)", n)
|
log.Printf("xray-core config written: %d inbound(s)", n)
|
||||||
|
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
_ = dockerRun("rm", "-f", xrayContainer)
|
_ = dockerRun("rm", "-f", xrayContainer)
|
||||||
log.Printf("xray stopped (no inbounds)")
|
log.Printf("xray stopped (no inbounds)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hostData := os.Getenv("HOST_DATA_DIR")
|
hostData := os.Getenv("HOST_DATA_DIR")
|
||||||
if hostData == "" {
|
if hostData == "" {
|
||||||
hostData = a.dataDir
|
hostData = a.dataDir
|
||||||
@@ -45,254 +75,26 @@ func (a *Agent) applyXray(cfg map[string]any) {
|
|||||||
log.Printf("xray container: %v", err)
|
log.Printf("xray container: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("xray container ready")
|
log.Printf("xray-core container ready (%s)", xrayImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildXrayConfig(inboundsRaw []any) (map[string]any, int) {
|
func ensureXrayContainer(hostXrayDir string) error {
|
||||||
var inbounds []map[string]any
|
|
||||||
for _, raw := range inboundsRaw {
|
|
||||||
m, ok := raw.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
proto := strings.ToLower(strVal(m, "protocol"))
|
|
||||||
switch proto {
|
|
||||||
case "vless", "vmess", "trojan", "shadowsocks":
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
in, ok := buildXrayInbound(m)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
inbounds = append(inbounds, in)
|
|
||||||
}
|
|
||||||
if inbounds == nil {
|
|
||||||
inbounds = []map[string]any{}
|
|
||||||
}
|
|
||||||
return map[string]any{
|
|
||||||
"log": map[string]any{
|
|
||||||
"loglevel": "warning",
|
|
||||||
"access": "/var/log/xray/access.log",
|
|
||||||
"error": "/var/log/xray/error.log",
|
|
||||||
},
|
|
||||||
"inbounds": inbounds,
|
|
||||||
"outbounds": []map[string]any{
|
|
||||||
{"protocol": "freedom", "tag": "direct"},
|
|
||||||
{"protocol": "blackhole", "tag": "block"},
|
|
||||||
},
|
|
||||||
}, len(inbounds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildXrayInbound(m map[string]any) (map[string]any, bool) {
|
|
||||||
proto := strings.ToLower(strVal(m, "protocol"))
|
|
||||||
tag := strVal(m, "tag")
|
|
||||||
if tag == "" {
|
|
||||||
tag = proto
|
|
||||||
}
|
|
||||||
listen := strVal(m, "listen")
|
|
||||||
if listen == "" {
|
|
||||||
listen = "0.0.0.0"
|
|
||||||
}
|
|
||||||
port := intVal(m, "port")
|
|
||||||
if port <= 0 {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
network := strVal(m, "network")
|
|
||||||
if network == "" {
|
|
||||||
network = "tcp"
|
|
||||||
}
|
|
||||||
security := strVal(m, "security")
|
|
||||||
if security == "" {
|
|
||||||
security = "none"
|
|
||||||
}
|
|
||||||
clients := parseClients(m["clients"])
|
|
||||||
settings := map[string]any{}
|
|
||||||
switch proto {
|
|
||||||
case "vless":
|
|
||||||
var list []map[string]any
|
|
||||||
for _, c := range clients {
|
|
||||||
item := map[string]any{"id": c.UUID, "email": c.EmailOrUser()}
|
|
||||||
if flow := strVal(m, "flow"); flow != "" {
|
|
||||||
item["flow"] = flow
|
|
||||||
}
|
|
||||||
list = append(list, item)
|
|
||||||
}
|
|
||||||
if list == nil {
|
|
||||||
list = []map[string]any{}
|
|
||||||
}
|
|
||||||
settings = map[string]any{"clients": list, "decryption": "none"}
|
|
||||||
case "vmess":
|
|
||||||
var list []map[string]any
|
|
||||||
for _, c := range clients {
|
|
||||||
list = append(list, map[string]any{"id": c.UUID, "email": c.EmailOrUser(), "alterId": 0})
|
|
||||||
}
|
|
||||||
if list == nil {
|
|
||||||
list = []map[string]any{}
|
|
||||||
}
|
|
||||||
settings = map[string]any{"clients": list}
|
|
||||||
case "trojan":
|
|
||||||
var list []map[string]any
|
|
||||||
inboundPass := strVal(m, "password")
|
|
||||||
for _, c := range clients {
|
|
||||||
pass := c.UUID
|
|
||||||
if inboundPass != "" {
|
|
||||||
pass = inboundPass
|
|
||||||
}
|
|
||||||
list = append(list, map[string]any{"password": pass, "email": c.EmailOrUser()})
|
|
||||||
}
|
|
||||||
if list == nil {
|
|
||||||
list = []map[string]any{}
|
|
||||||
}
|
|
||||||
settings = map[string]any{"clients": list}
|
|
||||||
case "shadowsocks":
|
|
||||||
method := strVal(m, "ss_method")
|
|
||||||
if method == "" {
|
|
||||||
method = "aes-128-gcm"
|
|
||||||
}
|
|
||||||
var list []map[string]any
|
|
||||||
for _, c := range clients {
|
|
||||||
list = append(list, map[string]any{"password": c.UUID, "email": c.EmailOrUser(), "method": method})
|
|
||||||
}
|
|
||||||
if list == nil {
|
|
||||||
list = []map[string]any{}
|
|
||||||
}
|
|
||||||
settings = map[string]any{
|
|
||||||
"method": method,
|
|
||||||
"password": strVal(m, "password"),
|
|
||||||
"clients": list,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stream := map[string]any{"network": network, "security": security}
|
|
||||||
switch network {
|
|
||||||
case "ws":
|
|
||||||
ws := map[string]any{}
|
|
||||||
if path := strVal(m, "path"); path != "" {
|
|
||||||
ws["path"] = path
|
|
||||||
}
|
|
||||||
if host := strVal(m, "host"); host != "" {
|
|
||||||
ws["headers"] = map[string]any{"Host": host}
|
|
||||||
}
|
|
||||||
stream["wsSettings"] = ws
|
|
||||||
case "grpc":
|
|
||||||
grpc := map[string]any{}
|
|
||||||
if path := strVal(m, "path"); path != "" {
|
|
||||||
grpc["serviceName"] = path
|
|
||||||
}
|
|
||||||
stream["grpcSettings"] = grpc
|
|
||||||
case "tcp":
|
|
||||||
// plain tcp
|
|
||||||
}
|
|
||||||
|
|
||||||
switch security {
|
|
||||||
case "tls":
|
|
||||||
tls := map[string]any{}
|
|
||||||
if sni := strVal(m, "sni"); sni != "" {
|
|
||||||
tls["serverName"] = sni
|
|
||||||
}
|
|
||||||
if alpn := strVal(m, "alpn"); alpn != "" {
|
|
||||||
tls["alpn"] = splitCSV(alpn)
|
|
||||||
}
|
|
||||||
stream["tlsSettings"] = tls
|
|
||||||
case "reality":
|
|
||||||
priv := strVal(m, "reality_private_key")
|
|
||||||
if priv == "" {
|
|
||||||
log.Printf("skip inbound %s: missing reality private key", tag)
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
dest := strVal(m, "reality_dest")
|
|
||||||
if dest == "" {
|
|
||||||
dest = "www.microsoft.com:443"
|
|
||||||
}
|
|
||||||
names := splitCSV(strVal(m, "reality_server_names"))
|
|
||||||
if len(names) == 0 {
|
|
||||||
names = []string{"www.microsoft.com"}
|
|
||||||
}
|
|
||||||
shortIDs := []string{strVal(m, "reality_short_id")}
|
|
||||||
if shortIDs[0] == "" {
|
|
||||||
shortIDs = []string{""}
|
|
||||||
}
|
|
||||||
reality := map[string]any{
|
|
||||||
"show": false,
|
|
||||||
"dest": dest,
|
|
||||||
"xver": 0,
|
|
||||||
"serverNames": names,
|
|
||||||
"privateKey": priv,
|
|
||||||
"shortIds": shortIDs,
|
|
||||||
}
|
|
||||||
stream["realitySettings"] = reality
|
|
||||||
}
|
|
||||||
|
|
||||||
return map[string]any{
|
|
||||||
"tag": tag,
|
|
||||||
"listen": listen,
|
|
||||||
"port": port,
|
|
||||||
"protocol": proto,
|
|
||||||
"settings": settings,
|
|
||||||
"streamSettings": stream,
|
|
||||||
"sniffing": map[string]any{
|
|
||||||
"enabled": true,
|
|
||||||
"destOverride": []string{"http", "tls", "quic"},
|
|
||||||
},
|
|
||||||
}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
type xrayClient struct {
|
|
||||||
UUID string
|
|
||||||
Email string
|
|
||||||
Username string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c xrayClient) EmailOrUser() string {
|
|
||||||
if c.Email != "" {
|
|
||||||
return c.Email
|
|
||||||
}
|
|
||||||
if c.Username != "" {
|
|
||||||
return c.Username
|
|
||||||
}
|
|
||||||
return c.UUID
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseClients(raw any) []xrayClient {
|
|
||||||
arr, ok := raw.([]any)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var out []xrayClient
|
|
||||||
for _, item := range arr {
|
|
||||||
m, ok := item.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
uid := strVal(m, "uuid")
|
|
||||||
if uid == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, xrayClient{
|
|
||||||
UUID: uid,
|
|
||||||
Email: strVal(m, "email"),
|
|
||||||
Username: strVal(m, "username"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureXrayContainer(xrayDir string) error {
|
|
||||||
if _, err := exec.LookPath("docker"); err != nil {
|
if _, err := exec.LookPath("docker"); err != nil {
|
||||||
return fmt.Errorf("docker not found (mount docker.sock and install docker CLI): %w", err)
|
return fmt.Errorf("docker not found (mount docker.sock): %w", err)
|
||||||
}
|
}
|
||||||
|
abs := hostXrayDir
|
||||||
|
if a, err := filepath.Abs(hostXrayDir); err == nil {
|
||||||
|
abs = a
|
||||||
|
}
|
||||||
|
// Prefer recreate for clean config pickup (official image has no inotify reload by default).
|
||||||
_ = dockerRun("rm", "-f", xrayContainer)
|
_ = dockerRun("rm", "-f", xrayContainer)
|
||||||
abs, err := filepath.Abs(xrayDir)
|
|
||||||
if err != nil {
|
|
||||||
abs = xrayDir
|
|
||||||
}
|
|
||||||
return dockerRun("run", "-d",
|
return dockerRun("run", "-d",
|
||||||
"--name", xrayContainer,
|
"--name", xrayContainer,
|
||||||
"--network", "host",
|
"--network", "host",
|
||||||
"--restart", "unless-stopped",
|
"--restart", "unless-stopped",
|
||||||
"-v", abs+":/etc/xray",
|
"-v", abs+":/usr/local/etc/xray",
|
||||||
xrayImage,
|
xrayImage,
|
||||||
|
"run", "-c", "/usr/local/etc/xray/config.json",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,53 +107,15 @@ func dockerRun(args ...string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func strVal(m map[string]any, key string) string {
|
func sanitizeFile(tag string) string {
|
||||||
v, ok := m[key]
|
tag = strings.Map(func(r rune) rune {
|
||||||
if !ok || v == nil {
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
|
||||||
return ""
|
return r
|
||||||
}
|
}
|
||||||
switch t := v.(type) {
|
return '_'
|
||||||
case string:
|
}, tag)
|
||||||
return t
|
if tag == "" {
|
||||||
case fmt.Stringer:
|
return "inbound"
|
||||||
return t.String()
|
|
||||||
default:
|
|
||||||
return fmt.Sprint(t)
|
|
||||||
}
|
}
|
||||||
}
|
return tag
|
||||||
|
|
||||||
func intVal(m map[string]any, key string) int {
|
|
||||||
v, ok := m[key]
|
|
||||||
if !ok || v == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
switch t := v.(type) {
|
|
||||||
case float64:
|
|
||||||
return int(t)
|
|
||||||
case int:
|
|
||||||
return t
|
|
||||||
case int64:
|
|
||||||
return int(t)
|
|
||||||
case json.Number:
|
|
||||||
n, _ := t.Int64()
|
|
||||||
return int(n)
|
|
||||||
case string:
|
|
||||||
var n int
|
|
||||||
_, _ = fmt.Sscanf(t, "%d", &n)
|
|
||||||
return n
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitCSV(s string) []string {
|
|
||||||
parts := strings.Split(s, ",")
|
|
||||||
var out []string
|
|
||||||
for _, p := range parts {
|
|
||||||
p = strings.TrimSpace(p)
|
|
||||||
if p != "" {
|
|
||||||
out = append(out, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ func ClientSubscriptionHosts(db *sql.DB, client *models.Client) ([]models.SubHos
|
|||||||
rows, err := db.Query(`
|
rows, err := db.Query(`
|
||||||
SELECT i.id, i.tag, i.remark, p.code, p.name, i.port, i.network, i.security,
|
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.path, i.host, i.sni, i.fingerprint, i.flow, i.alpn,
|
||||||
i.reality_public_key, i.reality_short_id, i.ss_method, i.password,
|
i.reality_public_key, i.reality_short_id, i.spider_x, i.ss_method, i.password,
|
||||||
COALESCE(n.host, ''), COALESCE(n.name, ''),
|
COALESCE(n.host, ''), COALESCE(n.name, ''),
|
||||||
COALESCE(n.status = 'online', FALSE),
|
COALESCE(n.status = 'online', FALSE),
|
||||||
COALESCE(ni.enabled AND n.status = 'online', FALSE)
|
COALESCE(ni.enabled AND n.status = 'online', FALSE)
|
||||||
@@ -256,7 +256,7 @@ ORDER BY i.sort_order ASC, i.tag ASC, n.name ASC NULLS LAST`, client.ID)
|
|||||||
&h.InboundID, &h.Tag, &h.Remark, &h.ProtocolCode, &h.ProtocolName,
|
&h.InboundID, &h.Tag, &h.Remark, &h.ProtocolCode, &h.ProtocolName,
|
||||||
&h.Port, &h.Network, &h.Security,
|
&h.Port, &h.Network, &h.Security,
|
||||||
&h.Path, &h.HostHeader, &h.SNI, &h.Fingerprint, &h.Flow, &h.ALPN,
|
&h.Path, &h.HostHeader, &h.SNI, &h.Fingerprint, &h.Flow, &h.ALPN,
|
||||||
&h.PublicKey, &h.ShortID, &h.SSMethod, &h.Password,
|
&h.PublicKey, &h.ShortID, &h.SpiderX, &h.SSMethod, &h.Password,
|
||||||
&h.Address, &h.NodeName,
|
&h.Address, &h.NodeName,
|
||||||
&h.NodeOnline, &h.Available,
|
&h.NodeOnline, &h.Available,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -363,6 +363,13 @@ func buildVLESSLink(uid, name string, h *models.SubHost) string {
|
|||||||
if h.ShortID != "" {
|
if h.ShortID != "" {
|
||||||
parts = append(parts, "sid="+url.QueryEscape(h.ShortID))
|
parts = append(parts, "sid="+url.QueryEscape(h.ShortID))
|
||||||
}
|
}
|
||||||
|
spx := h.SpiderX
|
||||||
|
if spx == "" && h.Security == "reality" {
|
||||||
|
spx = "/"
|
||||||
|
}
|
||||||
|
if spx != "" {
|
||||||
|
parts = append(parts, "spx="+url.QueryEscape(spx))
|
||||||
|
}
|
||||||
return fmt.Sprintf("vless://%s@%s:%d?%s#%s", uid, h.Address, h.Port, strings.Join(parts, "&"), name)
|
return fmt.Sprintf("vless://%s@%s:%d?%s#%s", uid, h.Address, h.Port, strings.Join(parts, "&"), name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -187,6 +187,15 @@ CREATE TABLE IF NOT EXISTS client_inbounds (
|
|||||||
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS reality_short_id 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 ss_method TEXT NOT NULL DEFAULT 'aes-128-gcm'`,
|
||||||
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS password TEXT NOT NULL DEFAULT ''`,
|
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS password TEXT NOT NULL DEFAULT ''`,
|
||||||
|
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS reality_short_ids TEXT NOT NULL DEFAULT ''`,
|
||||||
|
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS spider_x TEXT NOT NULL DEFAULT '/'`,
|
||||||
|
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS reality_xver INT NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS fallback_dest TEXT NOT NULL DEFAULT ''`,
|
||||||
|
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS fallbacks_json TEXT NOT NULL DEFAULT ''`,
|
||||||
|
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS tls_cert_pem TEXT NOT NULL DEFAULT ''`,
|
||||||
|
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS tls_key_pem TEXT NOT NULL DEFAULT ''`,
|
||||||
|
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS sniffing BOOLEAN NOT NULL DEFAULT TRUE`,
|
||||||
|
`ALTER TABLE inbounds ADD COLUMN IF NOT EXISTS sniffing_route_only BOOLEAN NOT NULL DEFAULT FALSE`,
|
||||||
} {
|
} {
|
||||||
if _, err := db.Exec(q); err != nil {
|
if _, err := db.Exec(q); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -347,8 +347,17 @@ ORDER BY p.sort_order`, nodeID)
|
|||||||
"reality_private_key": in.RealityPrivateKey,
|
"reality_private_key": in.RealityPrivateKey,
|
||||||
"reality_public_key": in.RealityPublicKey,
|
"reality_public_key": in.RealityPublicKey,
|
||||||
"reality_short_id": in.RealityShortID,
|
"reality_short_id": in.RealityShortID,
|
||||||
|
"reality_short_ids": in.RealityShortIDs,
|
||||||
|
"spider_x": in.SpiderX,
|
||||||
|
"reality_xver": in.RealityXver,
|
||||||
"ss_method": in.SSMethod,
|
"ss_method": in.SSMethod,
|
||||||
"password": in.Password,
|
"password": in.Password,
|
||||||
|
"fallback_dest": in.FallbackDest,
|
||||||
|
"fallbacks_json": in.FallbacksJSON,
|
||||||
|
"tls_cert_pem": in.TLSCertPEM,
|
||||||
|
"tls_key_pem": in.TLSKeyPEM,
|
||||||
|
"sniffing": in.Sniffing,
|
||||||
|
"sniffing_route_only": in.SniffingRouteOnly,
|
||||||
"clients": clientList,
|
"clients": clientList,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-4
@@ -101,7 +101,10 @@ func scanInbound(scanner interface {
|
|||||||
&in.Port, &in.Network, &in.Security, &in.Listen, &in.Enabled, &in.SortOrder, &in.Remark,
|
&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.Path, &in.Host, &in.SNI, &in.Fingerprint, &in.Flow, &in.ALPN,
|
||||||
&in.RealityDest, &in.RealityServerNames, &in.RealityPrivateKey, &in.RealityPublicKey, &in.RealityShortID,
|
&in.RealityDest, &in.RealityServerNames, &in.RealityPrivateKey, &in.RealityPublicKey, &in.RealityShortID,
|
||||||
|
&in.RealityShortIDs, &in.SpiderX, &in.RealityXver,
|
||||||
&in.SSMethod, &in.Password,
|
&in.SSMethod, &in.Password,
|
||||||
|
&in.FallbackDest, &in.FallbacksJSON, &in.TLSCertPEM, &in.TLSKeyPEM,
|
||||||
|
&in.Sniffing, &in.SniffingRouteOnly,
|
||||||
&in.CreatedAt, &in.UpdatedAt,
|
&in.CreatedAt, &in.UpdatedAt,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -115,7 +118,10 @@ 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.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.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.reality_dest, i.reality_server_names, i.reality_private_key, i.reality_public_key, i.reality_short_id,
|
||||||
|
i.reality_short_ids, i.spider_x, i.reality_xver,
|
||||||
i.ss_method, i.password,
|
i.ss_method, i.password,
|
||||||
|
i.fallback_dest, i.fallbacks_json, i.tls_cert_pem, i.tls_key_pem,
|
||||||
|
i.sniffing, i.sniffing_route_only,
|
||||||
i.created_at, i.updated_at`
|
i.created_at, i.updated_at`
|
||||||
|
|
||||||
func ListInboundsByProfile(db *sql.DB, profileID uuid.UUID) ([]models.Inbound, error) {
|
func ListInboundsByProfile(db *sql.DB, profileID uuid.UUID) ([]models.Inbound, error) {
|
||||||
@@ -175,6 +181,12 @@ func CreateInbound(db *sql.DB, in *models.Inbound) error {
|
|||||||
if in.SSMethod == "" {
|
if in.SSMethod == "" {
|
||||||
in.SSMethod = "aes-128-gcm"
|
in.SSMethod = "aes-128-gcm"
|
||||||
}
|
}
|
||||||
|
if in.SpiderX == "" {
|
||||||
|
in.SpiderX = "/"
|
||||||
|
}
|
||||||
|
if in.RealityShortIDs == "" && in.RealityShortID != "" {
|
||||||
|
in.RealityShortIDs = in.RealityShortID
|
||||||
|
}
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
in.CreatedAt = now
|
in.CreatedAt = now
|
||||||
in.UpdatedAt = now
|
in.UpdatedAt = now
|
||||||
@@ -183,18 +195,27 @@ INSERT INTO inbounds (
|
|||||||
id, profile_id, tag, protocol_id, port, network, security, listen, enabled, sort_order, remark,
|
id, profile_id, tag, protocol_id, port, network, security, listen, enabled, sort_order, remark,
|
||||||
path, host, sni, fingerprint, flow, alpn,
|
path, host, sni, fingerprint, flow, alpn,
|
||||||
reality_dest, reality_server_names, reality_private_key, reality_public_key, reality_short_id,
|
reality_dest, reality_server_names, reality_private_key, reality_public_key, reality_short_id,
|
||||||
ss_method, password, created_at, updated_at
|
reality_short_ids, spider_x, reality_xver,
|
||||||
|
ss_method, password,
|
||||||
|
fallback_dest, fallbacks_json, tls_cert_pem, tls_key_pem,
|
||||||
|
sniffing, sniffing_route_only, created_at, updated_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,
|
$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,
|
||||||
$12,$13,$14,$15,$16,$17,
|
$12,$13,$14,$15,$16,$17,
|
||||||
$18,$19,$20,$21,$22,
|
$18,$19,$20,$21,$22,
|
||||||
$23,$24,$25,$26
|
$23,$24,$25,
|
||||||
|
$26,$27,
|
||||||
|
$28,$29,$30,$31,
|
||||||
|
$32,$33,$34,$35
|
||||||
)`,
|
)`,
|
||||||
in.ID, in.ProfileID, in.Tag, in.ProtocolID, in.Port, in.Network, in.Security, in.Listen,
|
in.ID, in.ProfileID, in.Tag, in.ProtocolID, in.Port, in.Network, in.Security, in.Listen,
|
||||||
in.Enabled, in.SortOrder, in.Remark,
|
in.Enabled, in.SortOrder, in.Remark,
|
||||||
in.Path, in.Host, in.SNI, in.Fingerprint, in.Flow, in.ALPN,
|
in.Path, in.Host, in.SNI, in.Fingerprint, in.Flow, in.ALPN,
|
||||||
in.RealityDest, in.RealityServerNames, in.RealityPrivateKey, in.RealityPublicKey, in.RealityShortID,
|
in.RealityDest, in.RealityServerNames, in.RealityPrivateKey, in.RealityPublicKey, in.RealityShortID,
|
||||||
in.SSMethod, in.Password, in.CreatedAt, in.UpdatedAt,
|
in.RealityShortIDs, in.SpiderX, in.RealityXver,
|
||||||
|
in.SSMethod, in.Password,
|
||||||
|
in.FallbackDest, in.FallbacksJSON, in.TLSCertPEM, in.TLSKeyPEM,
|
||||||
|
in.Sniffing, in.SniffingRouteOnly, in.CreatedAt, in.UpdatedAt,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -206,20 +227,33 @@ func UpdateInbound(db *sql.DB, in *models.Inbound) error {
|
|||||||
if in.SSMethod == "" {
|
if in.SSMethod == "" {
|
||||||
in.SSMethod = "aes-128-gcm"
|
in.SSMethod = "aes-128-gcm"
|
||||||
}
|
}
|
||||||
|
if in.SpiderX == "" {
|
||||||
|
in.SpiderX = "/"
|
||||||
|
}
|
||||||
|
if in.RealityShortIDs == "" && in.RealityShortID != "" {
|
||||||
|
in.RealityShortIDs = in.RealityShortID
|
||||||
|
}
|
||||||
_, err := db.Exec(`
|
_, err := db.Exec(`
|
||||||
UPDATE inbounds SET
|
UPDATE inbounds SET
|
||||||
tag = $2, protocol_id = $3, port = $4, network = $5, security = $6,
|
tag = $2, protocol_id = $3, port = $4, network = $5, security = $6,
|
||||||
listen = $7, enabled = $8, sort_order = $9, remark = $10,
|
listen = $7, enabled = $8, sort_order = $9, remark = $10,
|
||||||
path = $11, host = $12, sni = $13, fingerprint = $14, flow = $15, alpn = $16,
|
path = $11, host = $12, sni = $13, fingerprint = $14, flow = $15, alpn = $16,
|
||||||
reality_dest = $17, reality_server_names = $18, reality_private_key = $19,
|
reality_dest = $17, reality_server_names = $18, reality_private_key = $19,
|
||||||
reality_public_key = $20, reality_short_id = $21, ss_method = $22, password = $23,
|
reality_public_key = $20, reality_short_id = $21,
|
||||||
|
reality_short_ids = $22, spider_x = $23, reality_xver = $24,
|
||||||
|
ss_method = $25, password = $26,
|
||||||
|
fallback_dest = $27, fallbacks_json = $28, tls_cert_pem = $29, tls_key_pem = $30,
|
||||||
|
sniffing = $31, sniffing_route_only = $32,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $1`,
|
WHERE id = $1`,
|
||||||
in.ID, in.Tag, in.ProtocolID, in.Port, in.Network, in.Security,
|
in.ID, in.Tag, in.ProtocolID, in.Port, in.Network, in.Security,
|
||||||
in.Listen, in.Enabled, in.SortOrder, in.Remark,
|
in.Listen, in.Enabled, in.SortOrder, in.Remark,
|
||||||
in.Path, in.Host, in.SNI, in.Fingerprint, in.Flow, in.ALPN,
|
in.Path, in.Host, in.SNI, in.Fingerprint, in.Flow, in.ALPN,
|
||||||
in.RealityDest, in.RealityServerNames, in.RealityPrivateKey, in.RealityPublicKey, in.RealityShortID,
|
in.RealityDest, in.RealityServerNames, in.RealityPrivateKey, in.RealityPublicKey, in.RealityShortID,
|
||||||
|
in.RealityShortIDs, in.SpiderX, in.RealityXver,
|
||||||
in.SSMethod, in.Password,
|
in.SSMethod, in.Password,
|
||||||
|
in.FallbackDest, in.FallbacksJSON, in.TLSCertPEM, in.TLSKeyPEM,
|
||||||
|
in.Sniffing, in.SniffingRouteOnly,
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -308,7 +342,10 @@ ORDER BY i.sort_order ASC, i.tag ASC`, nodeID, *n.ProfileID)
|
|||||||
&in.Port, &in.Network, &in.Security, &in.Listen, &in.Enabled, &in.SortOrder, &in.Remark,
|
&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.Path, &in.Host, &in.SNI, &in.Fingerprint, &in.Flow, &in.ALPN,
|
||||||
&in.RealityDest, &in.RealityServerNames, &in.RealityPrivateKey, &in.RealityPublicKey, &in.RealityShortID,
|
&in.RealityDest, &in.RealityServerNames, &in.RealityPrivateKey, &in.RealityPublicKey, &in.RealityShortID,
|
||||||
|
&in.RealityShortIDs, &in.SpiderX, &in.RealityXver,
|
||||||
&in.SSMethod, &in.Password,
|
&in.SSMethod, &in.Password,
|
||||||
|
&in.FallbackDest, &in.FallbacksJSON, &in.TLSCertPEM, &in.TLSKeyPEM,
|
||||||
|
&in.Sniffing, &in.SniffingRouteOnly,
|
||||||
&in.CreatedAt, &in.UpdatedAt, &in.NodeEnabled,
|
&in.CreatedAt, &in.UpdatedAt, &in.NodeEnabled,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -162,8 +162,22 @@ func parseInboundForm(r *http.Request, profileID, protocolID uuid.UUID, proto *m
|
|||||||
RealityPrivateKey: strings.TrimSpace(r.FormValue("reality_private_key")),
|
RealityPrivateKey: strings.TrimSpace(r.FormValue("reality_private_key")),
|
||||||
RealityPublicKey: strings.TrimSpace(r.FormValue("reality_public_key")),
|
RealityPublicKey: strings.TrimSpace(r.FormValue("reality_public_key")),
|
||||||
RealityShortID: strings.TrimSpace(r.FormValue("reality_short_id")),
|
RealityShortID: strings.TrimSpace(r.FormValue("reality_short_id")),
|
||||||
|
RealityShortIDs: strings.TrimSpace(r.FormValue("reality_short_ids")),
|
||||||
|
SpiderX: strings.TrimSpace(r.FormValue("spider_x")),
|
||||||
SSMethod: strings.TrimSpace(r.FormValue("ss_method")),
|
SSMethod: strings.TrimSpace(r.FormValue("ss_method")),
|
||||||
Password: strings.TrimSpace(r.FormValue("password")),
|
Password: strings.TrimSpace(r.FormValue("password")),
|
||||||
|
FallbackDest: strings.TrimSpace(r.FormValue("fallback_dest")),
|
||||||
|
FallbacksJSON: strings.TrimSpace(r.FormValue("fallbacks_json")),
|
||||||
|
TLSCertPEM: strings.TrimSpace(r.FormValue("tls_cert_pem")),
|
||||||
|
TLSKeyPEM: strings.TrimSpace(r.FormValue("tls_key_pem")),
|
||||||
|
Sniffing: formCheckbox(r, "sniffing", true),
|
||||||
|
SniffingRouteOnly: formCheckbox(r, "sniffing_route_only", false),
|
||||||
|
}
|
||||||
|
if xver, err := strconv.Atoi(r.FormValue("reality_xver")); err == nil {
|
||||||
|
in.RealityXver = xver
|
||||||
|
}
|
||||||
|
if in.SpiderX == "" {
|
||||||
|
in.SpiderX = "/"
|
||||||
}
|
}
|
||||||
if in.Security == "reality" {
|
if in.Security == "reality" {
|
||||||
priv, pub, sid, dest, names, err := xraykeys.EnsureReality(
|
priv, pub, sid, dest, names, err := xraykeys.EnsureReality(
|
||||||
@@ -175,6 +189,9 @@ func parseInboundForm(r *http.Request, profileID, protocolID uuid.UUID, proto *m
|
|||||||
}
|
}
|
||||||
in.RealityPrivateKey, in.RealityPublicKey, in.RealityShortID = priv, pub, sid
|
in.RealityPrivateKey, in.RealityPublicKey, in.RealityShortID = priv, pub, sid
|
||||||
in.RealityDest, in.RealityServerNames = dest, names
|
in.RealityDest, in.RealityServerNames = dest, names
|
||||||
|
if in.RealityShortIDs == "" {
|
||||||
|
in.RealityShortIDs = sid
|
||||||
|
}
|
||||||
if in.SNI == "" {
|
if in.SNI == "" {
|
||||||
in.SNI = strings.Split(names, ",")[0]
|
in.SNI = strings.Split(names, ",")[0]
|
||||||
}
|
}
|
||||||
@@ -301,6 +318,20 @@ func inboundProtocols(all []models.Protocol) []models.Protocol {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formCheckbox(r *http.Request, name string, defaultOn bool) bool {
|
||||||
|
vals := r.Form[name]
|
||||||
|
if len(vals) == 0 {
|
||||||
|
return defaultOn
|
||||||
|
}
|
||||||
|
on := false
|
||||||
|
for _, v := range vals {
|
||||||
|
if v == "on" || v == "true" || v == "1" {
|
||||||
|
on = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return on
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) inboundUpdate(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) inboundUpdate(w http.ResponseWriter, r *http.Request) {
|
||||||
profileID, err := uuid.Parse(mux.Vars(r)["id"])
|
profileID, err := uuid.Parse(mux.Vars(r)["id"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -153,8 +153,17 @@ type Inbound struct {
|
|||||||
RealityPrivateKey string `json:"reality_private_key"`
|
RealityPrivateKey string `json:"reality_private_key"`
|
||||||
RealityPublicKey string `json:"reality_public_key"`
|
RealityPublicKey string `json:"reality_public_key"`
|
||||||
RealityShortID string `json:"reality_short_id"`
|
RealityShortID string `json:"reality_short_id"`
|
||||||
|
RealityShortIDs string `json:"reality_short_ids"`
|
||||||
|
SpiderX string `json:"spider_x"`
|
||||||
|
RealityXver int `json:"reality_xver"`
|
||||||
SSMethod string `json:"ss_method"`
|
SSMethod string `json:"ss_method"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
FallbackDest string `json:"fallback_dest"`
|
||||||
|
FallbacksJSON string `json:"fallbacks_json"`
|
||||||
|
TLSCertPEM string `json:"tls_cert_pem"`
|
||||||
|
TLSKeyPEM string `json:"tls_key_pem"`
|
||||||
|
Sniffing bool `json:"sniffing"`
|
||||||
|
SniffingRouteOnly bool `json:"sniffing_route_only"`
|
||||||
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
@@ -260,6 +269,7 @@ type SubHost struct {
|
|||||||
ALPN string `json:"alpn,omitempty"`
|
ALPN string `json:"alpn,omitempty"`
|
||||||
PublicKey string `json:"public_key,omitempty"`
|
PublicKey string `json:"public_key,omitempty"`
|
||||||
ShortID string `json:"short_id,omitempty"`
|
ShortID string `json:"short_id,omitempty"`
|
||||||
|
SpiderX string `json:"spider_x,omitempty"`
|
||||||
SSMethod string `json:"ss_method,omitempty"`
|
SSMethod string `json:"ss_method,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/orohi/vpn-panel/internal/models"
|
"github.com/orohi/vpn-panel/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
const AgentVersion = "0.2.0"
|
const AgentVersion = "0.3.0"
|
||||||
|
|
||||||
func ComposeYAML(n *models.Node) string {
|
func ComposeYAML(n *models.Node) string {
|
||||||
return fmt.Sprintf(`services:
|
return fmt.Sprintf(`services:
|
||||||
|
|||||||
@@ -0,0 +1,591 @@
|
|||||||
|
// Package xraycfg builds Xray-core (XTLS) JSON configs from panel inbound payloads.
|
||||||
|
// Spec: https://github.com/XTLS/Xray-core
|
||||||
|
package xraycfg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Inbound is a panel→node inbound description used to generate Xray inbound objects.
|
||||||
|
type Inbound struct {
|
||||||
|
ID string
|
||||||
|
Tag string
|
||||||
|
Protocol string
|
||||||
|
Port int
|
||||||
|
Listen string
|
||||||
|
Network string
|
||||||
|
Security string
|
||||||
|
Remark string
|
||||||
|
Path string
|
||||||
|
Host string
|
||||||
|
SNI string
|
||||||
|
Fingerprint string
|
||||||
|
Flow string
|
||||||
|
ALPN string
|
||||||
|
RealityDest string
|
||||||
|
RealityServerNames string
|
||||||
|
RealityPrivateKey string
|
||||||
|
RealityPublicKey string
|
||||||
|
RealityShortID string
|
||||||
|
RealityShortIDs string // comma-separated; overrides RealityShortID when set
|
||||||
|
RealitySpiderX string
|
||||||
|
RealityXver int
|
||||||
|
SSMethod string
|
||||||
|
Password string
|
||||||
|
FallbackDest string // simple fallback, e.g. 127.0.0.1:80
|
||||||
|
FallbacksJSON string // optional raw JSON array of fallback objects
|
||||||
|
TLSCertPEM string
|
||||||
|
TLSKeyPEM string
|
||||||
|
Sniffing bool
|
||||||
|
SniffingRouteOnly bool
|
||||||
|
Clients []Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
ID string
|
||||||
|
UUID string
|
||||||
|
Email string
|
||||||
|
Username string
|
||||||
|
Flow string // optional per-client override
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) EmailOrUser() string {
|
||||||
|
if c.Email != "" {
|
||||||
|
return c.Email
|
||||||
|
}
|
||||||
|
if c.Username != "" {
|
||||||
|
return c.Username
|
||||||
|
}
|
||||||
|
if c.UUID != "" {
|
||||||
|
return c.UUID
|
||||||
|
}
|
||||||
|
return c.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build produces a full Xray-core config.json object.
|
||||||
|
func Build(inbounds []Inbound, certDir string) (map[string]any, int, error) {
|
||||||
|
var xIn []map[string]any
|
||||||
|
for _, in := range inbounds {
|
||||||
|
obj, ok, err := buildInbound(in, certDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
xIn = append(xIn, obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if xIn == nil {
|
||||||
|
xIn = []map[string]any{}
|
||||||
|
}
|
||||||
|
cfg := map[string]any{
|
||||||
|
"log": map[string]any{
|
||||||
|
"loglevel": "warning",
|
||||||
|
"access": "none",
|
||||||
|
"error": "",
|
||||||
|
},
|
||||||
|
"dns": map[string]any{
|
||||||
|
"servers": []any{
|
||||||
|
"1.1.1.1",
|
||||||
|
"1.0.0.1",
|
||||||
|
"8.8.8.8",
|
||||||
|
"localhost",
|
||||||
|
},
|
||||||
|
"queryStrategy": "UseIP",
|
||||||
|
},
|
||||||
|
"inbounds": xIn,
|
||||||
|
"outbounds": []map[string]any{
|
||||||
|
{
|
||||||
|
"tag": "direct",
|
||||||
|
"protocol": "freedom",
|
||||||
|
"settings": map[string]any{
|
||||||
|
"domainStrategy": "UseIP",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "block",
|
||||||
|
"protocol": "blackhole",
|
||||||
|
"settings": map[string]any{
|
||||||
|
"response": map[string]any{"type": "http"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"routing": map[string]any{
|
||||||
|
"domainStrategy": "AsIs",
|
||||||
|
"rules": []map[string]any{
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"protocol": []string{"bittorrent"},
|
||||||
|
"outboundTag": "block",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"ip": []string{"geoip:private"},
|
||||||
|
"outboundTag": "block",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"policy": map[string]any{
|
||||||
|
"levels": map[string]any{
|
||||||
|
"0": map[string]any{
|
||||||
|
"handshake": 4,
|
||||||
|
"connIdle": 300,
|
||||||
|
"uplinkOnly": 2,
|
||||||
|
"downlinkOnly": 5,
|
||||||
|
"statsUserUplink": true,
|
||||||
|
"statsUserDownlink": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"system": map[string]any{
|
||||||
|
"statsInboundUplink": true,
|
||||||
|
"statsInboundDownlink": true,
|
||||||
|
"statsOutboundUplink": true,
|
||||||
|
"statsOutboundDownlink": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"stats": map[string]any{},
|
||||||
|
}
|
||||||
|
return cfg, len(xIn), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildInbound(in Inbound, certDir string) (map[string]any, bool, error) {
|
||||||
|
proto := strings.ToLower(strings.TrimSpace(in.Protocol))
|
||||||
|
switch proto {
|
||||||
|
case "vless", "vmess", "trojan", "shadowsocks":
|
||||||
|
default:
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
tag := in.Tag
|
||||||
|
if tag == "" {
|
||||||
|
tag = proto
|
||||||
|
}
|
||||||
|
listen := in.Listen
|
||||||
|
if listen == "" {
|
||||||
|
listen = "0.0.0.0"
|
||||||
|
}
|
||||||
|
if in.Port <= 0 {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
network := strings.ToLower(or(in.Network, "tcp"))
|
||||||
|
security := strings.ToLower(or(in.Security, "none"))
|
||||||
|
|
||||||
|
settings, err := protocolSettings(proto, in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
stream, err := streamSettings(in, network, security, certDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := map[string]any{
|
||||||
|
"tag": tag,
|
||||||
|
"listen": listen,
|
||||||
|
"port": in.Port,
|
||||||
|
"protocol": proto,
|
||||||
|
"settings": settings,
|
||||||
|
"streamSettings": stream,
|
||||||
|
"sniffing": map[string]any{
|
||||||
|
"enabled": in.Sniffing,
|
||||||
|
"destOverride": []string{"http", "tls", "quic"},
|
||||||
|
"routeOnly": in.SniffingRouteOnly,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return obj, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func protocolSettings(proto string, in Inbound) (map[string]any, error) {
|
||||||
|
switch proto {
|
||||||
|
case "vless":
|
||||||
|
var list []map[string]any
|
||||||
|
for _, c := range in.Clients {
|
||||||
|
item := map[string]any{
|
||||||
|
"id": c.UUID,
|
||||||
|
"email": c.EmailOrUser(),
|
||||||
|
}
|
||||||
|
flow := c.Flow
|
||||||
|
if flow == "" {
|
||||||
|
flow = in.Flow
|
||||||
|
}
|
||||||
|
if flow == "" && in.Security == "reality" && or(in.Network, "tcp") == "tcp" {
|
||||||
|
flow = "xtls-rprx-vision"
|
||||||
|
}
|
||||||
|
if flow != "" {
|
||||||
|
item["flow"] = flow
|
||||||
|
}
|
||||||
|
list = append(list, item)
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
list = []map[string]any{}
|
||||||
|
}
|
||||||
|
settings := map[string]any{
|
||||||
|
"clients": list,
|
||||||
|
"decryption": "none",
|
||||||
|
}
|
||||||
|
if fb := parseFallbacks(in); len(fb) > 0 {
|
||||||
|
settings["fallbacks"] = fb
|
||||||
|
}
|
||||||
|
return settings, nil
|
||||||
|
|
||||||
|
case "vmess":
|
||||||
|
var list []map[string]any
|
||||||
|
for _, c := range in.Clients {
|
||||||
|
list = append(list, map[string]any{
|
||||||
|
"id": c.UUID,
|
||||||
|
"email": c.EmailOrUser(),
|
||||||
|
"alterId": 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
list = []map[string]any{}
|
||||||
|
}
|
||||||
|
return map[string]any{"clients": list}, nil
|
||||||
|
|
||||||
|
case "trojan":
|
||||||
|
var list []map[string]any
|
||||||
|
for _, c := range in.Clients {
|
||||||
|
pass := c.UUID
|
||||||
|
if in.Password != "" {
|
||||||
|
pass = in.Password
|
||||||
|
}
|
||||||
|
list = append(list, map[string]any{
|
||||||
|
"password": pass,
|
||||||
|
"email": c.EmailOrUser(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
list = []map[string]any{}
|
||||||
|
}
|
||||||
|
settings := map[string]any{"clients": list}
|
||||||
|
if fb := parseFallbacks(in); len(fb) > 0 {
|
||||||
|
settings["fallbacks"] = fb
|
||||||
|
}
|
||||||
|
return settings, nil
|
||||||
|
|
||||||
|
case "shadowsocks":
|
||||||
|
method := or(in.SSMethod, "aes-128-gcm")
|
||||||
|
var list []map[string]any
|
||||||
|
for _, c := range in.Clients {
|
||||||
|
list = append(list, map[string]any{
|
||||||
|
"password": c.UUID,
|
||||||
|
"email": c.EmailOrUser(),
|
||||||
|
"method": method,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if list == nil {
|
||||||
|
list = []map[string]any{}
|
||||||
|
}
|
||||||
|
return map[string]any{
|
||||||
|
"method": method,
|
||||||
|
"password": or(in.Password, "panel-ss"),
|
||||||
|
"clients": list,
|
||||||
|
"network": "tcp,udp",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unsupported protocol %s", proto)
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamSettings(in Inbound, network, security, certDir string) (map[string]any, error) {
|
||||||
|
stream := map[string]any{
|
||||||
|
"network": network,
|
||||||
|
"security": security,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch network {
|
||||||
|
case "ws":
|
||||||
|
ws := map[string]any{}
|
||||||
|
if in.Path != "" {
|
||||||
|
ws["path"] = in.Path
|
||||||
|
}
|
||||||
|
if in.Host != "" {
|
||||||
|
ws["headers"] = map[string]any{"Host": in.Host}
|
||||||
|
}
|
||||||
|
stream["wsSettings"] = ws
|
||||||
|
case "grpc", "gun":
|
||||||
|
grpc := map[string]any{
|
||||||
|
"multiMode": false,
|
||||||
|
}
|
||||||
|
if in.Path != "" {
|
||||||
|
grpc["serviceName"] = in.Path
|
||||||
|
}
|
||||||
|
stream["grpcSettings"] = grpc
|
||||||
|
case "httpupgrade":
|
||||||
|
hu := map[string]any{}
|
||||||
|
if in.Path != "" {
|
||||||
|
hu["path"] = in.Path
|
||||||
|
}
|
||||||
|
if in.Host != "" {
|
||||||
|
hu["host"] = in.Host
|
||||||
|
}
|
||||||
|
stream["httpupgradeSettings"] = hu
|
||||||
|
case "xhttp", "splithttp":
|
||||||
|
xh := map[string]any{
|
||||||
|
"mode": "auto",
|
||||||
|
}
|
||||||
|
if in.Path != "" {
|
||||||
|
xh["path"] = in.Path
|
||||||
|
}
|
||||||
|
if in.Host != "" {
|
||||||
|
xh["host"] = in.Host
|
||||||
|
}
|
||||||
|
stream["xhttpSettings"] = xh
|
||||||
|
case "tcp":
|
||||||
|
// optional HTTP camouflage when path/host set without ws
|
||||||
|
if in.Path != "" || in.Host != "" {
|
||||||
|
stream["tcpSettings"] = map[string]any{
|
||||||
|
"header": map[string]any{
|
||||||
|
"type": "http",
|
||||||
|
"request": map[string]any{
|
||||||
|
"path": splitPaths(in.Path),
|
||||||
|
"headers": map[string]any{
|
||||||
|
"Host": splitCSV(in.Host),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "kcp", "mkcp":
|
||||||
|
stream["kcpSettings"] = map[string]any{
|
||||||
|
"mtu": 1350,
|
||||||
|
"tti": 50,
|
||||||
|
"uplinkCapacity": 5,
|
||||||
|
"downlinkCapacity": 20,
|
||||||
|
"congestion": false,
|
||||||
|
"header": map[string]any{"type": "none"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch security {
|
||||||
|
case "tls":
|
||||||
|
tls := map[string]any{
|
||||||
|
"rejectUnknownSni": false,
|
||||||
|
"minVersion": "1.2",
|
||||||
|
}
|
||||||
|
if in.SNI != "" {
|
||||||
|
tls["serverName"] = in.SNI
|
||||||
|
}
|
||||||
|
if in.ALPN != "" {
|
||||||
|
tls["alpn"] = splitCSV(in.ALPN)
|
||||||
|
}
|
||||||
|
if in.Fingerprint != "" {
|
||||||
|
tls["fingerprint"] = in.Fingerprint
|
||||||
|
}
|
||||||
|
if in.TLSCertPEM != "" && in.TLSKeyPEM != "" && certDir != "" {
|
||||||
|
id := in.ID
|
||||||
|
if id == "" {
|
||||||
|
id = sanitizeTag(in.Tag)
|
||||||
|
}
|
||||||
|
certFile := certDir + "/" + id + ".crt"
|
||||||
|
keyFile := certDir + "/" + id + ".key"
|
||||||
|
tls["certificates"] = []map[string]any{{
|
||||||
|
"certificateFile": certFile,
|
||||||
|
"keyFile": keyFile,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
stream["tlsSettings"] = tls
|
||||||
|
|
||||||
|
case "reality":
|
||||||
|
if in.RealityPrivateKey == "" {
|
||||||
|
return nil, fmt.Errorf("inbound %s: reality private key required", in.Tag)
|
||||||
|
}
|
||||||
|
dest := or(in.RealityDest, "www.microsoft.com:443")
|
||||||
|
names := splitCSV(in.RealityServerNames)
|
||||||
|
if len(names) == 0 {
|
||||||
|
names = []string{"www.microsoft.com"}
|
||||||
|
}
|
||||||
|
shortIDs := shortIDList(in)
|
||||||
|
spiderX := in.RealitySpiderX
|
||||||
|
if spiderX == "" {
|
||||||
|
spiderX = "/"
|
||||||
|
}
|
||||||
|
reality := map[string]any{
|
||||||
|
"show": false,
|
||||||
|
"dest": dest,
|
||||||
|
"xver": in.RealityXver,
|
||||||
|
"serverNames": names,
|
||||||
|
"privateKey": in.RealityPrivateKey,
|
||||||
|
"shortIds": shortIDs,
|
||||||
|
"spiderX": spiderX,
|
||||||
|
}
|
||||||
|
stream["realitySettings"] = reality
|
||||||
|
}
|
||||||
|
|
||||||
|
stream["sockopt"] = map[string]any{
|
||||||
|
"tcpFastOpen": false,
|
||||||
|
"tcpKeepAliveIdle": 100,
|
||||||
|
"domainStrategy": "UseIP",
|
||||||
|
}
|
||||||
|
return stream, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFallbacks(in Inbound) []map[string]any {
|
||||||
|
if strings.TrimSpace(in.FallbacksJSON) != "" {
|
||||||
|
var raw []map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(in.FallbacksJSON), &raw); err == nil && len(raw) > 0 {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dest := strings.TrimSpace(in.FallbackDest); dest != "" {
|
||||||
|
return []map[string]any{{"dest": dest}}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func shortIDList(in Inbound) []string {
|
||||||
|
raw := in.RealityShortIDs
|
||||||
|
if strings.TrimSpace(raw) == "" {
|
||||||
|
raw = in.RealityShortID
|
||||||
|
}
|
||||||
|
ids := splitCSV(raw)
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return []string{""}
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitCSV(s string) []string {
|
||||||
|
parts := strings.Split(s, ",")
|
||||||
|
var out []string
|
||||||
|
for _, p := range parts {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if p != "" {
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitPaths(path string) []string {
|
||||||
|
if path == "" {
|
||||||
|
return []string{"/"}
|
||||||
|
}
|
||||||
|
parts := splitCSV(path)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return []string{"/"}
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
func or(v, def string) string {
|
||||||
|
if strings.TrimSpace(v) == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeTag(tag string) string {
|
||||||
|
tag = strings.Map(func(r rune) rune {
|
||||||
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return '_'
|
||||||
|
}, tag)
|
||||||
|
if tag == "" {
|
||||||
|
return "inbound"
|
||||||
|
}
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromMap converts a loose JSON map (agent payload item) into Inbound.
|
||||||
|
func FromMap(m map[string]any) Inbound {
|
||||||
|
in := Inbound{
|
||||||
|
ID: str(m, "id"),
|
||||||
|
Tag: str(m, "tag"),
|
||||||
|
Protocol: str(m, "protocol"),
|
||||||
|
Port: asInt(m["port"]),
|
||||||
|
Listen: str(m, "listen"),
|
||||||
|
Network: str(m, "network"),
|
||||||
|
Security: str(m, "security"),
|
||||||
|
Remark: str(m, "remark"),
|
||||||
|
Path: str(m, "path"),
|
||||||
|
Host: str(m, "host"),
|
||||||
|
SNI: str(m, "sni"),
|
||||||
|
Fingerprint: str(m, "fingerprint"),
|
||||||
|
Flow: str(m, "flow"),
|
||||||
|
ALPN: str(m, "alpn"),
|
||||||
|
RealityDest: str(m, "reality_dest"),
|
||||||
|
RealityServerNames: str(m, "reality_server_names"),
|
||||||
|
RealityPrivateKey: str(m, "reality_private_key"),
|
||||||
|
RealityPublicKey: str(m, "reality_public_key"),
|
||||||
|
RealityShortID: str(m, "reality_short_id"),
|
||||||
|
RealityShortIDs: str(m, "reality_short_ids"),
|
||||||
|
RealitySpiderX: str(m, "spider_x"),
|
||||||
|
RealityXver: asInt(m["reality_xver"]),
|
||||||
|
SSMethod: str(m, "ss_method"),
|
||||||
|
Password: str(m, "password"),
|
||||||
|
FallbackDest: str(m, "fallback_dest"),
|
||||||
|
FallbacksJSON: str(m, "fallbacks_json"),
|
||||||
|
TLSCertPEM: str(m, "tls_cert_pem"),
|
||||||
|
TLSKeyPEM: str(m, "tls_key_pem"),
|
||||||
|
Sniffing: asBoolDefault(m["sniffing"], true),
|
||||||
|
SniffingRouteOnly: asBoolDefault(m["sniffing_route_only"], false),
|
||||||
|
}
|
||||||
|
if arr, ok := m["clients"].([]any); ok {
|
||||||
|
for _, item := range arr {
|
||||||
|
cm, ok := item.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
uid := str(cm, "uuid")
|
||||||
|
if uid == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
in.Clients = append(in.Clients, Client{
|
||||||
|
ID: str(cm, "id"),
|
||||||
|
UUID: uid,
|
||||||
|
Email: str(cm, "email"),
|
||||||
|
Username: str(cm, "username"),
|
||||||
|
Flow: str(cm, "flow"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
|
func str(m map[string]any, key string) string {
|
||||||
|
v, ok := m[key]
|
||||||
|
if !ok || v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch t := v.(type) {
|
||||||
|
case string:
|
||||||
|
return t
|
||||||
|
default:
|
||||||
|
return fmt.Sprint(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func asInt(v any) int {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case float64:
|
||||||
|
return int(t)
|
||||||
|
case int:
|
||||||
|
return t
|
||||||
|
case int64:
|
||||||
|
return int(t)
|
||||||
|
case json.Number:
|
||||||
|
n, _ := t.Int64()
|
||||||
|
return int(n)
|
||||||
|
case string:
|
||||||
|
var n int
|
||||||
|
_, _ = fmt.Sscanf(t, "%d", &n)
|
||||||
|
return n
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func asBoolDefault(v any, def bool) bool {
|
||||||
|
if v == nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
switch t := v.(type) {
|
||||||
|
case bool:
|
||||||
|
return t
|
||||||
|
case string:
|
||||||
|
return t == "1" || strings.EqualFold(t, "true") || t == "on"
|
||||||
|
default:
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -467,10 +467,14 @@ textarea { resize: vertical; font-family: var(--mono); font-size: 0.85rem; }
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
input.mono, .mono {
|
input.mono, .mono, textarea.mono {
|
||||||
font-family: "JetBrains Mono", ui-monospace, monospace;
|
font-family: "JetBrains Mono", ui-monospace, monospace;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
textarea.mono {
|
||||||
|
width: 100%;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes rise {
|
@keyframes rise {
|
||||||
from { opacity: 0; transform: translateY(12px); }
|
from { opacity: 0; transform: translateY(12px); }
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<header class="admin-header row-head">
|
<header class="admin-header row-head">
|
||||||
<div>
|
<div>
|
||||||
<h1>Inbound <code>{{.Inbound.Tag}}</code></h1>
|
<h1>Inbound <code>{{.Inbound.Tag}}</code></h1>
|
||||||
<p class="muted">Профиль {{.Profile.Name}}</p>
|
<p class="muted">Xray-core · профиль {{.Profile.Name}}</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-ghost" href="/admin/profiles/{{.Profile.ID}}">К профилю</a>
|
<a class="btn btn-ghost" href="/admin/profiles/{{.Profile.ID}}">К профилю</a>
|
||||||
</header>
|
</header>
|
||||||
@@ -58,11 +58,13 @@
|
|||||||
</label>
|
</label>
|
||||||
<label>Network
|
<label>Network
|
||||||
<select name="network">
|
<select name="network">
|
||||||
<option value="tcp" {{if eq .Inbound.Network "tcp"}}selected{{end}}>tcp</option>
|
{{$n := .Inbound.Network}}
|
||||||
<option value="ws" {{if eq .Inbound.Network "ws"}}selected{{end}}>ws</option>
|
<option value="tcp" {{if eq $n "tcp"}}selected{{end}}>tcp</option>
|
||||||
<option value="grpc" {{if eq .Inbound.Network "grpc"}}selected{{end}}>grpc</option>
|
<option value="ws" {{if eq $n "ws"}}selected{{end}}>ws</option>
|
||||||
<option value="quic" {{if eq .Inbound.Network "quic"}}selected{{end}}>quic</option>
|
<option value="grpc" {{if eq $n "grpc"}}selected{{end}}>grpc</option>
|
||||||
<option value="udp" {{if eq .Inbound.Network "udp"}}selected{{end}}>udp</option>
|
<option value="httpupgrade" {{if eq $n "httpupgrade"}}selected{{end}}>httpupgrade</option>
|
||||||
|
<option value="xhttp" {{if eq $n "xhttp"}}selected{{end}}>xhttp</option>
|
||||||
|
<option value="kcp" {{if eq $n "kcp"}}selected{{end}}>kcp</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Security
|
<label>Security
|
||||||
@@ -73,7 +75,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Path / serviceName
|
<label>Path / serviceName
|
||||||
<input name="path" value="{{.Inbound.Path}}" placeholder="/ws или grpc service">
|
<input name="path" value="{{.Inbound.Path}}">
|
||||||
</label>
|
</label>
|
||||||
<label>Host header
|
<label>Host header
|
||||||
<input name="host" value="{{.Inbound.Host}}">
|
<input name="host" value="{{.Inbound.Host}}">
|
||||||
@@ -82,13 +84,13 @@
|
|||||||
<input name="sni" value="{{.Inbound.SNI}}">
|
<input name="sni" value="{{.Inbound.SNI}}">
|
||||||
</label>
|
</label>
|
||||||
<label>Fingerprint
|
<label>Fingerprint
|
||||||
<input name="fingerprint" value="{{.Inbound.Fingerprint}}" placeholder="chrome">
|
<input name="fingerprint" value="{{.Inbound.Fingerprint}}">
|
||||||
</label>
|
</label>
|
||||||
<label>Flow
|
<label>Flow
|
||||||
<input name="flow" value="{{.Inbound.Flow}}" placeholder="xtls-rprx-vision">
|
<input name="flow" value="{{.Inbound.Flow}}">
|
||||||
</label>
|
</label>
|
||||||
<label>ALPN
|
<label>ALPN
|
||||||
<input name="alpn" value="{{.Inbound.ALPN}}" placeholder="h2,http/1.1">
|
<input name="alpn" value="{{.Inbound.ALPN}}">
|
||||||
</label>
|
</label>
|
||||||
<label>Порядок
|
<label>Порядок
|
||||||
<input name="sort_order" type="number" value="{{.Inbound.SortOrder}}">
|
<input name="sort_order" type="number" value="{{.Inbound.SortOrder}}">
|
||||||
@@ -97,33 +99,68 @@
|
|||||||
<input name="remark" value="{{.Inbound.Remark}}">
|
<input name="remark" value="{{.Inbound.Remark}}">
|
||||||
</label>
|
</label>
|
||||||
<label>SS method
|
<label>SS method
|
||||||
<input name="ss_method" value="{{.Inbound.SSMethod}}" placeholder="aes-128-gcm">
|
<input name="ss_method" value="{{.Inbound.SSMethod}}">
|
||||||
</label>
|
</label>
|
||||||
<label>Password (trojan shared, optional)
|
<label>Password (shared, optional)
|
||||||
<input name="password" value="{{.Inbound.Password}}">
|
<input name="password" value="{{.Inbound.Password}}">
|
||||||
</label>
|
</label>
|
||||||
|
<label>Fallback dest
|
||||||
|
<input name="fallback_dest" value="{{.Inbound.FallbackDest}}" placeholder="127.0.0.1:80">
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="section-sub">Reality</h3>
|
<h3 class="section-sub">Reality (XTLS)</h3>
|
||||||
<p class="muted">Пустые ключи сгенерируются автоматически при security=reality.</p>
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label>Dest
|
<label>Dest
|
||||||
<input name="reality_dest" value="{{.Inbound.RealityDest}}" placeholder="www.microsoft.com:443">
|
<input name="reality_dest" value="{{.Inbound.RealityDest}}" placeholder="www.microsoft.com:443">
|
||||||
</label>
|
</label>
|
||||||
<label>Server names
|
<label>Server names
|
||||||
<input name="reality_server_names" value="{{.Inbound.RealityServerNames}}" placeholder="www.microsoft.com">
|
<input name="reality_server_names" value="{{.Inbound.RealityServerNames}}">
|
||||||
</label>
|
</label>
|
||||||
<label>Private key
|
<label>SpiderX
|
||||||
<input name="reality_private_key" value="{{.Inbound.RealityPrivateKey}}" class="mono">
|
<input name="spider_x" value="{{.Inbound.SpiderX}}">
|
||||||
</label>
|
</label>
|
||||||
<label>Public key (для клиентов)
|
<label>xver
|
||||||
<input name="reality_public_key" value="{{.Inbound.RealityPublicKey}}" class="mono">
|
<input name="reality_xver" type="number" value="{{.Inbound.RealityXver}}">
|
||||||
</label>
|
</label>
|
||||||
<label>Short ID
|
<label>Short ID
|
||||||
<input name="reality_short_id" value="{{.Inbound.RealityShortID}}" class="mono">
|
<input name="reality_short_id" value="{{.Inbound.RealityShortID}}" class="mono">
|
||||||
</label>
|
</label>
|
||||||
|
<label>Short IDs (через запятую)
|
||||||
|
<input name="reality_short_ids" value="{{.Inbound.RealityShortIDs}}" class="mono">
|
||||||
|
</label>
|
||||||
|
<label>Private key
|
||||||
|
<input name="reality_private_key" value="{{.Inbound.RealityPrivateKey}}" class="mono">
|
||||||
|
</label>
|
||||||
|
<label>Public key
|
||||||
|
<input name="reality_public_key" value="{{.Inbound.RealityPublicKey}}" class="mono">
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3 class="section-sub">TLS certificates (PEM)</h3>
|
||||||
|
<p class="muted">Нужны только при security=tls. На ноде пишутся в /usr/local/etc/xray/certs/</p>
|
||||||
|
<div class="form-grid">
|
||||||
|
<label>Certificate PEM
|
||||||
|
<textarea name="tls_cert_pem" rows="4" class="mono">{{.Inbound.TLSCertPEM}}</textarea>
|
||||||
|
</label>
|
||||||
|
<label>Private key PEM
|
||||||
|
<textarea name="tls_key_pem" rows="4" class="mono">{{.Inbound.TLSKeyPEM}}</textarea>
|
||||||
|
</label>
|
||||||
|
<label>Fallbacks JSON
|
||||||
|
<textarea name="fallbacks_json" rows="3" class="mono" placeholder='[{"dest":"127.0.0.1:80"}]'>{{.Inbound.FallbacksJSON}}</textarea>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="check-row">
|
||||||
|
<input type="hidden" name="sniffing" value="off">
|
||||||
|
<input type="checkbox" name="sniffing" value="on" {{if .Inbound.Sniffing}}checked{{end}}>
|
||||||
|
Sniffing
|
||||||
|
</label>
|
||||||
|
<label class="check-row">
|
||||||
|
<input type="hidden" name="sniffing_route_only" value="off">
|
||||||
|
<input type="checkbox" name="sniffing_route_only" value="on" {{if .Inbound.SniffingRouteOnly}}checked{{end}}>
|
||||||
|
Sniffing routeOnly
|
||||||
|
</label>
|
||||||
<label class="check-row">
|
<label class="check-row">
|
||||||
<input type="hidden" name="enabled" value="off">
|
<input type="hidden" name="enabled" value="off">
|
||||||
<input type="checkbox" name="enabled" value="on" {{if .Inbound.Enabled}}checked{{end}}>
|
<input type="checkbox" name="enabled" value="on" {{if .Inbound.Enabled}}checked{{end}}>
|
||||||
|
|||||||
@@ -123,8 +123,9 @@
|
|||||||
<option value="tcp" selected>tcp</option>
|
<option value="tcp" selected>tcp</option>
|
||||||
<option value="ws">ws</option>
|
<option value="ws">ws</option>
|
||||||
<option value="grpc">grpc</option>
|
<option value="grpc">grpc</option>
|
||||||
<option value="quic">quic</option>
|
<option value="httpupgrade">httpupgrade</option>
|
||||||
<option value="udp">udp</option>
|
<option value="xhttp">xhttp</option>
|
||||||
|
<option value="kcp">kcp</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Security
|
<label>Security
|
||||||
@@ -138,16 +139,16 @@
|
|||||||
<input name="path" placeholder="/ws или grpc service">
|
<input name="path" placeholder="/ws или grpc service">
|
||||||
</label>
|
</label>
|
||||||
<label>Host header
|
<label>Host header
|
||||||
<input name="host" placeholder="для ws">
|
<input name="host" placeholder="для ws/xhttp">
|
||||||
</label>
|
</label>
|
||||||
<label>SNI
|
<label>SNI
|
||||||
<input name="sni" placeholder="авто из reality names">
|
<input name="sni" placeholder="авто из reality names">
|
||||||
</label>
|
</label>
|
||||||
<label>Fingerprint
|
<label>Fingerprint (клиент)
|
||||||
<input name="fingerprint" value="chrome">
|
<input name="fingerprint" value="chrome">
|
||||||
</label>
|
</label>
|
||||||
<label>Flow
|
<label>Flow
|
||||||
<input name="flow" value="xtls-rprx-vision" placeholder="xtls-rprx-vision для VLESS+Reality">
|
<input name="flow" value="xtls-rprx-vision" placeholder="xtls-rprx-vision">
|
||||||
</label>
|
</label>
|
||||||
<label>ALPN
|
<label>ALPN
|
||||||
<input name="alpn" placeholder="h2,http/1.1">
|
<input name="alpn" placeholder="h2,http/1.1">
|
||||||
@@ -159,16 +160,22 @@
|
|||||||
<input name="remark" placeholder="Sweden RU (имя в подписке)">
|
<input name="remark" placeholder="Sweden RU (имя в подписке)">
|
||||||
</label>
|
</label>
|
||||||
<label>Reality dest
|
<label>Reality dest
|
||||||
<input name="reality_dest" value="www.microsoft.com:443" placeholder="www.microsoft.com:443">
|
<input name="reality_dest" value="www.microsoft.com:443">
|
||||||
</label>
|
</label>
|
||||||
<label>Reality server names
|
<label>Reality server names
|
||||||
<input name="reality_server_names" value="www.microsoft.com" placeholder="www.microsoft.com">
|
<input name="reality_server_names" value="www.microsoft.com">
|
||||||
|
</label>
|
||||||
|
<label>SpiderX
|
||||||
|
<input name="spider_x" value="/">
|
||||||
|
</label>
|
||||||
|
<label>Fallback dest
|
||||||
|
<input name="fallback_dest" placeholder="127.0.0.1:80">
|
||||||
</label>
|
</label>
|
||||||
<label>SS method
|
<label>SS method
|
||||||
<input name="ss_method" value="aes-128-gcm">
|
<input name="ss_method" value="aes-128-gcm">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted">Только Xray-протоколы (VLESS/VMess/Trojan/SS). Для Reality ключи и shortId генерируются автоматически.</p>
|
<p class="muted">Xray-core (XTLS/Xray-core): Reality-ключи и shortId генерируются автоматически. На ноде поднимается официальный образ ghcr.io/xtls/xray-core.</p>
|
||||||
<button class="btn btn-primary" type="submit">Добавить inbound</button>
|
<button class="btn btn-primary" type="submit">Добавить inbound</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user