Files
panel-vpn/cmd/node/main.go
T

234 lines
5.2 KiB
Go

package main
import (
"encoding/json"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
)
const defaultVersion = "0.3.0"
type Agent struct {
name string
secret string
version string
dataDir string
logPath string
startedAt time.Time
mu sync.RWMutex
config map[string]any
}
func main() {
port := env("NODE_PORT", "2222")
secret := os.Getenv("SECRET_KEY")
if secret == "" {
log.Fatal("SECRET_KEY is required")
}
name := env("NODE_NAME", "vpn-node")
version := env("AGENT_VERSION", defaultVersion)
dataDir := env("DATA_DIR", "/var/lib/vpn-node")
_ = os.MkdirAll(dataDir, 0o755)
logPath := filepath.Join(dataDir, "agent.log")
logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
if err == nil {
log.SetOutput(io.MultiWriter(os.Stderr, logFile))
defer logFile.Close()
}
a := &Agent{
name: name,
secret: secret,
version: version,
dataDir: dataDir,
logPath: logPath,
startedAt: time.Now().UTC(),
config: map[string]any{},
}
a.loadConfig(filepath.Join(dataDir, "config.json"))
a.applyXray(a.config)
mux := http.NewServeMux()
mux.HandleFunc("/health", a.auth(a.health))
mux.HandleFunc("/api/v1/config", a.auth(a.configHandler))
mux.HandleFunc("/api/v1/status", a.auth(a.status))
mux.HandleFunc("/api/v1/logs", a.auth(a.logs))
addr := ":" + port
log.Printf("vpn-node %s listening on %s name=%s", version, addr, name)
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatal(err)
}
}
func env(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
return def
}
func (a *Agent) auth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h := r.Header.Get("Authorization")
token := strings.TrimPrefix(h, "Bearer ")
if token == "" {
token = r.Header.Get("X-Node-Secret")
}
if token != a.secret {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next(w, r)
}
}
func stringList(cfg map[string]any, key string) []string {
out := []string{}
raw, ok := cfg[key]
if !ok {
return out
}
switch v := raw.(type) {
case []any:
for _, item := range v {
if s, ok := item.(string); ok && s != "" {
out = append(out, s)
}
}
case []string:
out = append(out, v...)
}
return out
}
func (a *Agent) health(w http.ResponseWriter, _ *http.Request) {
a.mu.RLock()
defer a.mu.RUnlock()
enabled := stringList(a.config, "protocols_enabled")
if len(enabled) == 0 {
enabled = stringList(a.config, "protocols")
}
installed := stringList(a.config, "protocols_installed")
if len(installed) == 0 {
installed = append([]string{}, enabled...)
}
writeJSON(w, map[string]any{
"ok": true,
"name": a.name,
"version": a.version,
"uptime_sec": int64(time.Since(a.startedAt).Seconds()),
"protocols": enabled,
"protocols_installed": installed,
"protocols_enabled": enabled,
})
}
func (a *Agent) status(w http.ResponseWriter, _ *http.Request) {
a.mu.RLock()
defer a.mu.RUnlock()
writeJSON(w, map[string]any{
"ok": true,
"name": a.name,
"version": a.version,
"config": a.config,
})
}
func (a *Agent) logs(w http.ResponseWriter, r *http.Request) {
lines := 300
if v := r.URL.Query().Get("lines"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 5000 {
lines = n
}
}
content, err := tailFile(a.logPath, lines)
if err != nil {
writeJSON(w, map[string]any{"ok": true, "logs": "(no agent.log yet)"})
return
}
writeJSON(w, map[string]any{"ok": true, "logs": content})
}
func tailFile(path string, maxLines int) (string, error) {
b, err := os.ReadFile(path)
if err != nil {
return "", err
}
text := string(b)
parts := strings.Split(text, "\n")
if len(parts) > maxLines {
parts = parts[len(parts)-maxLines:]
}
return strings.Join(parts, "\n"), nil
}
func (a *Agent) configHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
a.mu.RLock()
defer a.mu.RUnlock()
writeJSON(w, a.config)
case http.MethodPost:
var cfg map[string]any
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
http.Error(w, "bad json", http.StatusBadRequest)
return
}
a.mu.Lock()
a.config = cfg
a.mu.Unlock()
_ = a.saveConfig(filepath.Join(a.dataDir, "config.json"))
a.applyXray(cfg)
log.Printf("config updated: installed=%v enabled=%v inbounds=%d",
stringList(cfg, "protocols_installed"),
stringList(cfg, "protocols_enabled"),
len(anySlice(cfg["inbounds"])),
)
writeJSON(w, map[string]any{"ok": true})
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
func (a *Agent) loadConfig(path string) {
b, err := os.ReadFile(path)
if err != nil {
return
}
var cfg map[string]any
if json.Unmarshal(b, &cfg) == nil {
a.config = cfg
}
}
func (a *Agent) saveConfig(path string) error {
a.mu.RLock()
defer a.mu.RUnlock()
b, err := json.MarshalIndent(a.config, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, b, 0o600)
}
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
}
func anySlice(v any) []any {
if arr, ok := v.([]any); ok {
return arr
}
return nil
}