Add node mode with SSH auto-install and Remnawave-style manual deploy.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultVersion = "0.1.0"
|
||||
|
||||
type Agent struct {
|
||||
name string
|
||||
secret string
|
||||
version 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)
|
||||
|
||||
a := &Agent{
|
||||
name: name,
|
||||
secret: secret,
|
||||
version: version,
|
||||
startedAt: time.Now().UTC(),
|
||||
config: map[string]any{},
|
||||
}
|
||||
a.loadConfig(filepath.Join(dataDir, "config.json"))
|
||||
|
||||
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))
|
||||
|
||||
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 (a *Agent) health(w http.ResponseWriter, _ *http.Request) {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
protocols := []string{}
|
||||
if raw, ok := a.config["protocols"].([]any); ok {
|
||||
for _, p := range raw {
|
||||
if s, ok := p.(string); ok {
|
||||
protocols = append(protocols, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
writeJSON(w, map[string]any{
|
||||
"ok": true,
|
||||
"name": a.name,
|
||||
"version": a.version,
|
||||
"uptime_sec": int64(time.Since(a.startedAt).Seconds()),
|
||||
"protocols": protocols,
|
||||
})
|
||||
}
|
||||
|
||||
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) 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("/var/lib/vpn-node/config.json")
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user