358 lines
7.7 KiB
Go
358 lines
7.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const xrayContainer = "vpn-panel-xray"
|
|
const xrayImage = "teddysun/xray:latest"
|
|
|
|
func (a *Agent) applyXray(cfg map[string]any) {
|
|
xrayDir := filepath.Join(a.dataDir, "xray")
|
|
_ = os.MkdirAll(xrayDir, 0o755)
|
|
configPath := filepath.Join(xrayDir, "config.json")
|
|
|
|
inboundsRaw, _ := cfg["inbounds"].([]any)
|
|
xrayCfg, n := buildXrayConfig(inboundsRaw)
|
|
b, err := json.MarshalIndent(xrayCfg, "", " ")
|
|
if err != nil {
|
|
log.Printf("xray marshal: %v", err)
|
|
return
|
|
}
|
|
if err := os.WriteFile(configPath, b, 0o600); err != nil {
|
|
log.Printf("xray write config: %v", err)
|
|
return
|
|
}
|
|
log.Printf("xray config written: %d inbound(s)", n)
|
|
|
|
if n == 0 {
|
|
_ = dockerRun("rm", "-f", xrayContainer)
|
|
log.Printf("xray stopped (no inbounds)")
|
|
return
|
|
}
|
|
hostData := os.Getenv("HOST_DATA_DIR")
|
|
if hostData == "" {
|
|
hostData = a.dataDir
|
|
}
|
|
hostXray := filepath.Join(hostData, "xray")
|
|
if err := ensureXrayContainer(hostXray); err != nil {
|
|
log.Printf("xray container: %v", err)
|
|
return
|
|
}
|
|
log.Printf("xray container ready")
|
|
}
|
|
|
|
func buildXrayConfig(inboundsRaw []any) (map[string]any, int) {
|
|
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 {
|
|
return fmt.Errorf("docker not found (mount docker.sock and install docker CLI): %w", err)
|
|
}
|
|
_ = dockerRun("rm", "-f", xrayContainer)
|
|
abs, err := filepath.Abs(xrayDir)
|
|
if err != nil {
|
|
abs = xrayDir
|
|
}
|
|
return dockerRun("run", "-d",
|
|
"--name", xrayContainer,
|
|
"--network", "host",
|
|
"--restart", "unless-stopped",
|
|
"-v", abs+":/etc/xray",
|
|
xrayImage,
|
|
)
|
|
}
|
|
|
|
func dockerRun(args ...string) error {
|
|
cmd := exec.Command("docker", args...)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("docker %v: %v (%s)", args, err, strings.TrimSpace(string(out)))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func strVal(m map[string]any, key string) string {
|
|
v, ok := m[key]
|
|
if !ok || v == nil {
|
|
return ""
|
|
}
|
|
switch t := v.(type) {
|
|
case string:
|
|
return t
|
|
case fmt.Stringer:
|
|
return t.String()
|
|
default:
|
|
return fmt.Sprint(t)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|