package main import ( "encoding/json" "fmt" "log" "os" "os/exec" "path/filepath" "strings" "github.com/orohi/vpn-panel/internal/xraycfg" ) const xrayContainer = "vpn-panel-xray" // 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) { xrayDir := filepath.Join(a.dataDir, "xray") certDir := filepath.Join(xrayDir, "certs") _ = os.MkdirAll(certDir, 0o755) configPath := filepath.Join(xrayDir, "config.json") var list []xraycfg.Inbound inboundsRaw, _ := cfg["inbounds"].([]any) 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, "", " ") 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-core 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-core container ready (%s)", xrayImage) } func ensureXrayContainer(hostXrayDir string) error { if _, err := exec.LookPath("docker"); err != nil { 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) return dockerRun("run", "-d", "--name", xrayContainer, "--network", "host", "--restart", "unless-stopped", "-v", abs+":/usr/local/etc/xray", xrayImage, "run", "-c", "/usr/local/etc/xray/config.json", ) } 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 sanitizeFile(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 }