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 }