122 lines
3.0 KiB
Go
122 lines
3.0 KiB
Go
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
|
|
}
|