Ver Fonte

feat: implement EoIP tunnels and complete MikroTik VPN server suite

Proxmox Router há 1 semana atrás
pai
commit
1978de671c

+ 34 - 0
internal/api/api.go

@@ -151,6 +151,40 @@ func (s *Server) registerRoutes() {
 	s.mux.HandleFunc("POST /api/vpn/ipsec/tunnels/{name}/start", s.handleInitiateIPSecTunnel)
 	s.mux.HandleFunc("POST /api/vpn/ipsec/tunnels/{name}/stop", s.handleTerminateIPSecTunnel)
 
+	// EoIP (Ethernet over IP / GRETAP)
+	s.mux.HandleFunc("GET /api/vpn/eoip", s.handleListEoIP)
+	s.mux.HandleFunc("POST /api/vpn/eoip", s.handleSaveEoIP)
+	s.mux.HandleFunc("DELETE /api/vpn/eoip/{id}", s.handleDeleteEoIP)
+	s.mux.HandleFunc("POST /api/vpn/eoip/{id}/state", s.handleSetEoIPState)
+
+	// PPP Profiles (/ppp profile)
+	s.mux.HandleFunc("GET /api/vpn/ppp/profiles", s.handleListPPPProfiles)
+	s.mux.HandleFunc("POST /api/vpn/ppp/profiles", s.handleSavePPPProfile)
+	s.mux.HandleFunc("DELETE /api/vpn/ppp/profiles/{name}", s.handleDeletePPPProfile)
+
+	// PPP Secrets (/ppp secret)
+	s.mux.HandleFunc("GET /api/vpn/ppp/secrets", s.handleListPPPSecrets)
+	s.mux.HandleFunc("POST /api/vpn/ppp/secrets", s.handleSavePPPSecret)
+	s.mux.HandleFunc("DELETE /api/vpn/ppp/secrets/{id}", s.handleDeletePPPSecret)
+	s.mux.HandleFunc("POST /api/vpn/ppp/secrets/{id}/state", s.handleSetPPPSecretState)
+
+	// Active PPP Sessions (/ppp active)
+	s.mux.HandleFunc("GET /api/vpn/ppp/active", s.handleListPPPSessions)
+	s.mux.HandleFunc("DELETE /api/vpn/ppp/active/{iface}", s.handleDisconnectPPPSession)
+
+	// VPN Server Settings
+	s.mux.HandleFunc("GET /api/vpn/servers/pppoe", s.handleGetPPPoEServer)
+	s.mux.HandleFunc("POST /api/vpn/servers/pppoe", s.handleSetPPPoEServer)
+	s.mux.HandleFunc("GET /api/vpn/servers/pptp", s.handleGetPPTPServer)
+	s.mux.HandleFunc("POST /api/vpn/servers/pptp", s.handleSetPPTPServer)
+	s.mux.HandleFunc("GET /api/vpn/servers/l2tp", s.handleGetL2TPServer)
+	s.mux.HandleFunc("POST /api/vpn/servers/l2tp", s.handleSetL2TPServer)
+	s.mux.HandleFunc("GET /api/vpn/servers/ipsec", s.handleGetIPSecServer)
+	s.mux.HandleFunc("POST /api/vpn/servers/ipsec", s.handleSetIPSecServer)
+
+	// WireGuard Client Config Generator
+	s.mux.HandleFunc("POST /api/vpn/wireguard/client-config", s.handleGenerateWGClientConfig)
+
 	// Web UI Static Files
 	if s.fsHandler != nil {
 		s.mux.Handle("/", s.fsHandler)

+ 225 - 0
internal/api/vpn_handlers.go

@@ -364,3 +364,228 @@ func (s *Server) handleTerminateIPSecTunnel(w http.ResponseWriter, r *http.Reque
 	}
 	writeJSON(w, http.StatusOK, map[string]any{"success": true, "name": name})
 }
+
+// ======================== EoIP (Ethernet-over-IP) Handlers ========================
+func (s *Server) handleListEoIP(w http.ResponseWriter, r *http.Request) {
+	list := s.vpnMgr.EoIP.List()
+	writeJSON(w, http.StatusOK, list)
+}
+
+func (s *Server) handleSaveEoIP(w http.ResponseWriter, r *http.Request) {
+	var inst vpn.EoIPInstance
+	if err := json.NewDecoder(r.Body).Decode(&inst); err != nil {
+		writeError(w, http.StatusBadRequest, "invalid request body")
+		return
+	}
+	if err := s.vpnMgr.EoIP.Save(&inst); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "id": inst.ID, "name": inst.Name})
+}
+
+func (s *Server) handleDeleteEoIP(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if err := s.vpnMgr.EoIP.Delete(id); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "id": id})
+}
+
+func (s *Server) handleSetEoIPState(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	var req struct {
+		Disabled bool `json:"disabled"`
+	}
+	_ = json.NewDecoder(r.Body).Decode(&req)
+	if err := s.vpnMgr.EoIP.SetState(id, req.Disabled); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "id": id, "disabled": req.Disabled})
+}
+
+// ======================== PPP Profiles Handlers ========================
+func (s *Server) handleListPPPProfiles(w http.ResponseWriter, r *http.Request) {
+	list := s.vpnMgr.PPPServer.ListProfiles()
+	writeJSON(w, http.StatusOK, list)
+}
+
+func (s *Server) handleSavePPPProfile(w http.ResponseWriter, r *http.Request) {
+	var prof vpn.PPPProfile
+	if err := json.NewDecoder(r.Body).Decode(&prof); err != nil {
+		writeError(w, http.StatusBadRequest, "invalid request body")
+		return
+	}
+	if err := s.vpnMgr.PPPServer.SaveProfile(&prof); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "name": prof.Name})
+}
+
+func (s *Server) handleDeletePPPProfile(w http.ResponseWriter, r *http.Request) {
+	name := r.PathValue("name")
+	if err := s.vpnMgr.PPPServer.DeleteProfile(name); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "name": name})
+}
+
+// ======================== PPP Secrets Handlers ========================
+func (s *Server) handleListPPPSecrets(w http.ResponseWriter, r *http.Request) {
+	list := s.vpnMgr.PPPServer.ListSecrets()
+	writeJSON(w, http.StatusOK, list)
+}
+
+func (s *Server) handleSavePPPSecret(w http.ResponseWriter, r *http.Request) {
+	var sec vpn.PPPSecret
+	if err := json.NewDecoder(r.Body).Decode(&sec); err != nil {
+		writeError(w, http.StatusBadRequest, "invalid request body")
+		return
+	}
+	if err := s.vpnMgr.PPPServer.SaveSecret(&sec); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "id": sec.ID, "name": sec.Name})
+}
+
+func (s *Server) handleSetPPPSecretState(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	var req struct {
+		Disabled bool `json:"disabled"`
+	}
+	_ = json.NewDecoder(r.Body).Decode(&req)
+	if err := s.vpnMgr.PPPServer.SetSecretState(id, req.Disabled); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "id": id, "disabled": req.Disabled})
+}
+
+func (s *Server) handleDeletePPPSecret(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if err := s.vpnMgr.PPPServer.DeleteSecret(id); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "id": id})
+}
+
+// ======================== Active PPP Sessions Handlers ========================
+func (s *Server) handleListPPPSessions(w http.ResponseWriter, r *http.Request) {
+	list := s.vpnMgr.PPPServer.ListActiveSessions()
+	writeJSON(w, http.StatusOK, list)
+}
+
+func (s *Server) handleDisconnectPPPSession(w http.ResponseWriter, r *http.Request) {
+	iface := r.PathValue("iface")
+	if err := s.vpnMgr.PPPServer.DisconnectSession(iface); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "interface": iface})
+}
+
+// ======================== VPN Servers Configuration Handlers ========================
+func (s *Server) handleGetPPPoEServer(w http.ResponseWriter, r *http.Request) {
+	cfg := s.vpnMgr.PPPServer.GetPPPoEServer()
+	writeJSON(w, http.StatusOK, cfg)
+}
+
+func (s *Server) handleSetPPPoEServer(w http.ResponseWriter, r *http.Request) {
+	var cfg vpn.PPPoEServerConfig
+	if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
+		writeError(w, http.StatusBadRequest, "invalid request body")
+		return
+	}
+	if err := s.vpnMgr.PPPServer.SetPPPoEServer(cfg); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true})
+}
+
+func (s *Server) handleGetPPTPServer(w http.ResponseWriter, r *http.Request) {
+	cfg := s.vpnMgr.PPPServer.GetPPTPServer()
+	writeJSON(w, http.StatusOK, cfg)
+}
+
+func (s *Server) handleSetPPTPServer(w http.ResponseWriter, r *http.Request) {
+	var cfg vpn.PPTPServerConfig
+	if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
+		writeError(w, http.StatusBadRequest, "invalid request body")
+		return
+	}
+	if err := s.vpnMgr.PPPServer.SetPPTPServer(cfg); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true})
+}
+
+func (s *Server) handleGetL2TPServer(w http.ResponseWriter, r *http.Request) {
+	cfg := s.vpnMgr.PPPServer.GetL2TPServer()
+	writeJSON(w, http.StatusOK, cfg)
+}
+
+func (s *Server) handleSetL2TPServer(w http.ResponseWriter, r *http.Request) {
+	var cfg vpn.L2TPServerConfig
+	if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
+		writeError(w, http.StatusBadRequest, "invalid request body")
+		return
+	}
+	if err := s.vpnMgr.PPPServer.SetL2TPServer(cfg); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true})
+}
+
+func (s *Server) handleGetIPSecServer(w http.ResponseWriter, r *http.Request) {
+	cfg := s.vpnMgr.PPPServer.GetIPSecServer()
+	writeJSON(w, http.StatusOK, cfg)
+}
+
+func (s *Server) handleSetIPSecServer(w http.ResponseWriter, r *http.Request) {
+	var cfg vpn.IPSecServerConfig
+	if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
+		writeError(w, http.StatusBadRequest, "invalid request body")
+		return
+	}
+	if err := s.vpnMgr.PPPServer.SetIPSecServer(cfg); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true})
+}
+
+// WireGuard Client Config Generator
+func (s *Server) handleGenerateWGClientConfig(w http.ResponseWriter, r *http.Request) {
+	var req struct {
+		Interface string `json:"interface"`
+		Address   string `json:"address"`
+		PrivKey   string `json:"private_key"`
+		Endpoint  string `json:"endpoint"`
+		DNS       string `json:"dns"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Interface == "" {
+		writeError(w, http.StatusBadRequest, "interface is required")
+		return
+	}
+	if req.PrivKey == "" {
+		priv, _, err := s.vpnMgr.WireGuard.GenerateKeyPair()
+		if err == nil {
+			req.PrivKey = priv
+		}
+	}
+	conf, err := s.vpnMgr.WireGuard.GenerateClientConfig(req.Interface, req.PrivKey, req.Address, req.Endpoint, req.DNS)
+	if err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "config": conf, "client_private_key": req.PrivKey})
+}

+ 296 - 0
internal/vpn/eoip.go

@@ -0,0 +1,296 @@
+package vpn
+
+import (
+	"bufio"
+	"crypto/rand"
+	"encoding/json"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+)
+
+// EoIPInstance represents a MikroTik Ethernet-over-IP (GRETAP) tunnel
+type EoIPInstance struct {
+	ID            string `json:"id"`
+	Name          string `json:"name"`           // e.g. "eoip-tunnel1"
+	TunnelID      int    `json:"tunnel_id"`      // 1 - 65535
+	RemoteAddress string `json:"remote_address"` // Remote router IPv4
+	LocalAddress  string `json:"local_address"`  // Local router IPv4 (optional)
+	MACAddress    string `json:"mac_address"`    // e.g. "02:00:00:00:00:01"
+	MTU           int    `json:"mtu"`            // default 1500
+	ClampMSS      bool   `json:"clamp_mss"`      // Clamp TCP MSS for PMTUD
+	Comment       string `json:"comment,omitempty"`
+	Disabled      bool   `json:"disabled"`
+	Running       bool   `json:"running"` // MikroTik 'R' flag
+	RxPackets     uint64 `json:"rx_packets"`
+	TxPackets     uint64 `json:"tx_packets"`
+	RxBytes       uint64 `json:"rx_bytes"`
+	TxBytes       uint64 `json:"tx_bytes"`
+	RxHuman       string `json:"rx_human,omitempty"`
+	TxHuman       string `json:"tx_human,omitempty"`
+	LastError     string `json:"last_error,omitempty"`
+}
+
+type EoIPManager struct {
+	mu        sync.RWMutex
+	storePath string
+	tunnels   map[string]*EoIPInstance
+}
+
+func NewEoIPManager() *EoIPManager {
+	mgr := &EoIPManager{
+		storePath: "/etc/proxmox-router/vpn/eoip.json",
+		tunnels:   make(map[string]*EoIPInstance),
+	}
+	mgr.load()
+	mgr.syncAll()
+	return mgr
+}
+
+func (m *EoIPManager) load() {
+	if data, err := os.ReadFile(m.storePath); err == nil {
+		var list []*EoIPInstance
+		if err := json.Unmarshal(data, &list); err == nil {
+			for _, t := range list {
+				m.tunnels[t.ID] = t
+			}
+		}
+	}
+}
+
+func (m *EoIPManager) save() error {
+	_ = os.MkdirAll(filepath.Dir(m.storePath), 0755)
+	var list []*EoIPInstance
+	for _, t := range m.tunnels {
+		list = append(list, t)
+	}
+	data, err := json.MarshalIndent(list, "", "  ")
+	if err != nil {
+		return err
+	}
+	return os.WriteFile(m.storePath, data, 0644)
+}
+
+func (m *EoIPManager) List() []*EoIPInstance {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	list := make([]*EoIPInstance, 0)
+	for _, t := range m.tunnels {
+		m.updateStats(t)
+		list = append(list, t)
+	}
+	return list
+}
+
+func (m *EoIPManager) Get(id string) (*EoIPInstance, bool) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	t, ok := m.tunnels[id]
+	if ok {
+		m.updateStats(t)
+	}
+	return t, ok
+}
+
+func (m *EoIPManager) Save(inst *EoIPInstance) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if inst.Name == "" {
+		inst.Name = fmt.Sprintf("eoip-tunnel%d", inst.TunnelID)
+	}
+	if inst.TunnelID <= 0 || inst.TunnelID > 65535 {
+		return fmt.Errorf("tunnel_id must be between 1 and 65535")
+	}
+	if inst.RemoteAddress == "" {
+		return fmt.Errorf("remote_address is required")
+	}
+	if inst.MTU <= 0 {
+		inst.MTU = 1500
+	}
+	if inst.MACAddress == "" {
+		inst.MACAddress = generateMAC()
+	}
+
+	if inst.ID == "" {
+		inst.ID = fmt.Sprintf("eoip-%d", time.Now().UnixNano())
+	}
+
+	// Stop previous interface if name changed or existing
+	if prev, exists := m.tunnels[inst.ID]; exists && prev.Name != inst.Name {
+		m.destroyInterface(prev.Name)
+	}
+
+	m.tunnels[inst.ID] = inst
+	_ = m.save()
+
+	return m.applyTunnel(inst)
+}
+
+func (m *EoIPManager) SetState(id string, disabled bool) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	t, ok := m.tunnels[id]
+	if !ok {
+		return fmt.Errorf("tunnel not found")
+	}
+	t.Disabled = disabled
+	_ = m.save()
+
+	if disabled {
+		m.destroyInterface(t.Name)
+		t.Running = false
+		return nil
+	}
+	return m.applyTunnel(t)
+}
+
+func (m *EoIPManager) Delete(id string) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	t, ok := m.tunnels[id]
+	if !ok {
+		return fmt.Errorf("tunnel not found")
+	}
+	m.destroyInterface(t.Name)
+	delete(m.tunnels, id)
+	return m.save()
+}
+
+func (m *EoIPManager) syncAll() {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	for _, t := range m.tunnels {
+		if !t.Disabled {
+			_ = m.applyTunnel(t)
+		} else {
+			m.destroyInterface(t.Name)
+			t.Running = false
+		}
+	}
+}
+
+func (m *EoIPManager) applyTunnel(t *EoIPInstance) error {
+	m.destroyInterface(t.Name)
+
+	if t.Disabled {
+		t.Running = false
+		return nil
+	}
+
+	// Construct ip link add command:
+	// ip link add <name> type gretap remote <remote_ip> [local <local_ip>] key <tunnel_id>
+	args := []string{"link", "add", t.Name, "type", "gretap", "remote", t.RemoteAddress}
+	if t.LocalAddress != "" {
+		args = append(args, "local", t.LocalAddress)
+	}
+	args = append(args, "key", strconv.Itoa(t.TunnelID))
+
+	if out, err := exec.Command("ip", args...).CombinedOutput(); err != nil {
+		t.LastError = fmt.Sprintf("create failed: %s - %v", strings.TrimSpace(string(out)), err)
+		t.Running = false
+		return fmt.Errorf("%s", t.LastError)
+	}
+
+	// Set MAC address if provided
+	if t.MACAddress != "" {
+		_ = exec.Command("ip", "link", "set", t.Name, "address", t.MACAddress).Run()
+	}
+
+	// Set MTU
+	if t.MTU > 0 {
+		_ = exec.Command("ip", "link", "set", t.Name, "mtu", strconv.Itoa(t.MTU)).Run()
+	}
+
+	// Bring UP
+	if out, err := exec.Command("ip", "link", "set", t.Name, "up").CombinedOutput(); err != nil {
+		t.LastError = fmt.Sprintf("up failed: %s - %v", strings.TrimSpace(string(out)), err)
+		t.Running = false
+		return fmt.Errorf("%s", t.LastError)
+	}
+
+	// TCP MSS Clamping if enabled
+	if t.ClampMSS {
+		_ = exec.Command("iptables", "-t", "mangle", "-D", "FORWARD", "-o", t.Name, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu").Run()
+		_ = exec.Command("iptables", "-t", "mangle", "-A", "FORWARD", "-o", t.Name, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu").Run()
+	}
+
+	t.Running = true
+	t.LastError = ""
+	return nil
+}
+
+func (m *EoIPManager) destroyInterface(name string) {
+	if name == "" {
+		return
+	}
+	_ = exec.Command("iptables", "-t", "mangle", "-D", "FORWARD", "-o", name, "-p", "tcp", "--tcp-flags", "SYN,RST", "SYN", "-j", "TCPMSS", "--clamp-mss-to-pmtu").Run()
+	_ = exec.Command("ip", "link", "del", name).Run()
+}
+
+func (m *EoIPManager) updateStats(t *EoIPInstance) {
+	// Check if interface is up
+	out, err := exec.Command("ip", "link", "show", t.Name).Output()
+	if err != nil || !strings.Contains(string(out), "UP") {
+		t.Running = false
+	} else {
+		t.Running = true
+	}
+
+	// Read /proc/net/dev for RX/TX bytes
+	file, err := os.Open("/proc/net/dev")
+	if err != nil {
+		return
+	}
+	defer file.Close()
+
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		line := scanner.Text()
+		if strings.Contains(line, t.Name+":") {
+			parts := strings.Fields(line)
+			if len(parts) >= 10 {
+				rxBytes, _ := strconv.ParseUint(parts[1], 10, 64)
+				rxPackets, _ := strconv.ParseUint(parts[2], 10, 64)
+				txBytes, _ := strconv.ParseUint(parts[9], 10, 64)
+				txPackets, _ := strconv.ParseUint(parts[10], 10, 64)
+
+				t.RxBytes = rxBytes
+				t.RxPackets = rxPackets
+				t.TxBytes = txBytes
+				t.TxPackets = txPackets
+				t.RxHuman = formatBytes(rxBytes)
+				t.TxHuman = formatBytes(txBytes)
+			}
+			break
+		}
+	}
+}
+
+func generateMAC() string {
+	buf := make([]byte, 3)
+	_, _ = rand.Read(buf)
+	return fmt.Sprintf("02:64:%02x:%02x:%02x:%02x", buf[0], buf[1], buf[2], time.Now().UnixNano()%256)
+}
+
+func formatBytes(b uint64) string {
+	const unit = 1024
+	if b < unit {
+		return fmt.Sprintf("%d B", b)
+	}
+	div, exp := uint64(unit), 0
+	for n := b / unit; n >= unit; n /= unit {
+		div *= unit
+		exp++
+	}
+	return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
+}

+ 26 - 1
internal/vpn/manager.go

@@ -6,6 +6,8 @@ type Manager struct {
 	PPTP      *PPTPManager
 	L2TP      *L2TPManager
 	IPSec     *IPSecManager
+	EoIP      *EoIPManager
+	PPPServer *PPPServerManager
 }
 
 func NewManager() *Manager {
@@ -15,6 +17,8 @@ func NewManager() *Manager {
 		PPTP:      NewPPTPManager(),
 		L2TP:      NewL2TPManager(),
 		IPSec:     NewIPSecManager(),
+		EoIP:      NewEoIPManager(),
+		PPPServer: NewPPPServerManager(),
 	}
 }
 
@@ -26,6 +30,7 @@ func (m *Manager) GetOverallStatus() OverallVPNStatus {
 	pptpList := m.PPTP.List()
 	l2tpList := m.L2TP.List()
 	ipsecList := m.IPSec.List()
+	eoipList := m.EoIP.List()
 
 	activePPPoE := 0
 	for _, p := range pppoeList {
@@ -62,7 +67,20 @@ func (m *Manager) GetOverallStatus() OverallVPNStatus {
 		}
 	}
 
-	totalActive := activePPPoE + activeWG + activePPTP + activeL2TP + activeIPSec
+	activeEoIP := 0
+	for _, e := range eoipList {
+		if e.Running {
+			activeEoIP++
+		}
+	}
+
+	pppoeSrv := m.PPPServer.GetPPPoEServer()
+	pptpSrv := m.PPPServer.GetPPTPServer()
+	l2tpSrv := m.PPPServer.GetL2TPServer()
+	ipsecSrv := m.PPPServer.GetIPSecServer()
+	pppSessions := len(m.PPPServer.ListActiveSessions())
+
+	totalActive := activePPPoE + activeWG + activePPTP + activeL2TP + activeIPSec + activeEoIP + pppSessions
 
 	return OverallVPNStatus{
 		TotalPPPoE:          len(pppoeList),
@@ -76,6 +94,13 @@ func (m *Manager) GetOverallStatus() OverallVPNStatus {
 		ActiveL2TP:          activeL2TP,
 		TotalIPSec:          len(ipsecList),
 		ActiveIPSec:         activeIPSec,
+		TotalEoIP:           len(eoipList),
+		ActiveEoIP:          activeEoIP,
+		TotalPPPSessions:    pppSessions,
+		PPPoEServerRunning:  pppoeSrv.Running,
+		PPTPServerRunning:   pptpSrv.Running,
+		L2TPServerRunning:   l2tpSrv.Running,
+		IPSecServerRunning:  ipsecSrv.Running,
 		TotalActiveTunnels:  totalActive,
 	}
 }

+ 951 - 0
internal/vpn/ppp_server.go

@@ -0,0 +1,951 @@
+package vpn
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"net"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"sync"
+	"syscall"
+	"time"
+)
+
+// ======================== PPP Profiles (/ppp profile) ========================
+type PPPProfile struct {
+	ID            string `json:"id"`
+	Name          string `json:"name"`           // e.g. "default", "default-encryption", "pool-lan"
+	LocalAddress  string `json:"local_address"`  // e.g. "10.10.10.1" (server IP on tunnel)
+	RemoteAddress string `json:"remote_address"` // e.g. "10.10.10.10-10.10.10.50" or "10.10.10.10"
+	DNSServers    string `json:"dns_servers"`    // e.g. "1.1.1.1,8.8.8.8"
+	UseEncryption string `json:"use_encryption"` // "no", "yes", "required" (MPPE-128)
+	OnlyOne       bool   `json:"only_one"`       // One session per user
+	Comment       string `json:"comment,omitempty"`
+}
+
+// ======================== PPP Secrets (/ppp secret) ========================
+type PPPSecret struct {
+	ID            string `json:"id"`
+	Name          string `json:"name"`           // Username
+	Password      string `json:"password"`       // Password
+	Service       string `json:"service"`        // "any", "pppoe", "pptp", "l2tp"
+	Profile       string `json:"profile"`        // Profile name
+	LocalAddress  string `json:"local_address,omitempty"`
+	RemoteAddress string `json:"remote_address,omitempty"`
+	CallerID      string `json:"caller_id,omitempty"`
+	Comment       string `json:"comment,omitempty"`
+	Disabled      bool   `json:"disabled"`
+}
+
+// ======================== PPPoE Server (/interface pppoe-server server) ========================
+type PPPoEServerConfig struct {
+	Enabled           bool   `json:"enabled"`
+	ServiceName       string `json:"service_name"` // e.g. "isp-service"
+	Interface         string `json:"interface"`    // e.g. "eth0", "eth0.10", "br0"
+	DefaultProfile    string `json:"default_profile"`
+	OneSessionPerHost bool   `json:"one_session_per_host"`
+	MaxMTU            int    `json:"max_mtu"` // default 1480 / 1492
+	MaxMRU            int    `json:"max_mru"` // default 1480 / 1492
+	Running           bool   `json:"running"`
+	ActiveSessions    int    `json:"active_sessions"`
+	LastError         string `json:"last_error,omitempty"`
+}
+
+// ======================== PPTP Server (/interface pptp-server server) ========================
+type PPTPServerConfig struct {
+	Enabled        bool   `json:"enabled"`
+	Port           int    `json:"port"` // default 1723
+	DefaultProfile string `json:"default_profile"`
+	RequireMPPE    bool   `json:"require_mppe"` // 128-bit MPPE
+	Running        bool   `json:"running"`
+	ActiveSessions int    `json:"active_sessions"`
+	LastError      string `json:"last_error,omitempty"`
+}
+
+// ======================== L2TP Server (/interface l2tp-server server) ========================
+type L2TPServerConfig struct {
+	Enabled        bool   `json:"enabled"`
+	Port           int    `json:"port"` // default 1701
+	DefaultProfile string `json:"default_profile"`
+	UseIPSec       string `json:"use_ipsec"` // "no", "yes", "required"
+	IPSecSecret    string `json:"ipsec_secret,omitempty"`
+	Running        bool   `json:"running"`
+	ActiveSessions int    `json:"active_sessions"`
+	LastError      string `json:"last_error,omitempty"`
+}
+
+// ======================== IPSec Roadwarrior Server ========================
+type IPSecServerConfig struct {
+	Enabled        bool   `json:"enabled"`
+	PoolAddress    string `json:"pool_address"` // e.g. "10.10.30.0/24"
+	PresharedKey   string `json:"preshared_key"`
+	LocalSubnet    string `json:"local_subnet"` // e.g. "0.0.0.0/0"
+	DNSServers     string `json:"dns_servers"`  // e.g. "1.1.1.1,8.8.8.8"
+	Running        bool   `json:"running"`
+	ActiveSessions int    `json:"active_sessions"`
+	LastError      string `json:"last_error,omitempty"`
+}
+
+// ======================== Active PPP Session (/ppp active) ========================
+type ActivePPPSession struct {
+	ID        string `json:"id"`
+	Interface string `json:"interface"` // e.g. "ppp0"
+	Username  string `json:"username"`
+	Service   string `json:"service"` // "pppoe", "pptp", "l2tp"
+	CallerID  string `json:"caller_id,omitempty"`
+	Address   string `json:"address"` // client assigned IP
+	Uptime    string `json:"uptime"`
+	RxBytes   uint64 `json:"rx_bytes"`
+	TxBytes   uint64 `json:"tx_bytes"`
+	RxHuman   string `json:"rx_human,omitempty"`
+	TxHuman   string `json:"tx_human,omitempty"`
+}
+
+// ======================== Central PPP & Server Manager ========================
+type PPPServerManager struct {
+	mu           sync.RWMutex
+	baseDir      string
+	profiles     map[string]*PPPProfile
+	secrets      map[string]*PPPSecret
+	pppoeServer  PPPoEServerConfig
+	pptpServer   PPTPServerConfig
+	l2tpServer   L2TPServerConfig
+	ipsecServer  IPSecServerConfig
+	pppoePIDFile string
+}
+
+func NewPPPServerManager() *PPPServerManager {
+	mgr := &PPPServerManager{
+		baseDir:      "/etc/proxmox-router/vpn",
+		profiles:     make(map[string]*PPPProfile),
+		secrets:      make(map[string]*PPPSecret),
+		pppoePIDFile: "/var/run/proxmox-pppoe-server.pid",
+		pppoeServer: PPPoEServerConfig{
+			Enabled:        false,
+			ServiceName:    "proxmox-pppoe",
+			Interface:      "eth0",
+			DefaultProfile: "default",
+			MaxMTU:         1480,
+			MaxMRU:         1480,
+		},
+		pptpServer: PPTPServerConfig{
+			Enabled:        false,
+			Port:           1723,
+			DefaultProfile: "default-encryption",
+			RequireMPPE:    true,
+		},
+		l2tpServer: L2TPServerConfig{
+			Enabled:        false,
+			Port:           1701,
+			DefaultProfile: "default",
+			UseIPSec:       "no",
+		},
+		ipsecServer: IPSecServerConfig{
+			Enabled:      false,
+			PoolAddress:  "10.10.30.0/24",
+			PresharedKey: "ProxmoxRouterSecret123",
+			LocalSubnet:  "0.0.0.0/0",
+			DNSServers:   "1.1.1.1,8.8.8.8",
+		},
+	}
+
+	mgr.initDefaults()
+	mgr.loadAll()
+	mgr.SyncSecretsToSystem()
+	mgr.applyAll()
+	return mgr
+}
+
+func (m *PPPServerManager) initDefaults() {
+	m.profiles["default"] = &PPPProfile{
+		ID:            "default",
+		Name:          "default",
+		LocalAddress:  "10.10.10.1",
+		RemoteAddress: "10.10.10.10-10.10.10.50",
+		DNSServers:    "1.1.1.1,8.8.8.8",
+		UseEncryption: "no",
+		OnlyOne:       true,
+		Comment:       "Default PPP profile for unencrypted/standard tunnels",
+	}
+	m.profiles["default-encryption"] = &PPPProfile{
+		ID:            "default-encryption",
+		Name:          "default-encryption",
+		LocalAddress:  "10.10.20.1",
+		RemoteAddress: "10.10.20.10-10.10.20.50",
+		DNSServers:    "1.1.1.1,8.8.8.8",
+		UseEncryption: "required",
+		OnlyOne:       true,
+		Comment:       "Default PPP profile with required MPPE-128 encryption",
+	}
+}
+
+func (m *PPPServerManager) loadAll() {
+	_ = os.MkdirAll(m.baseDir, 0755)
+
+	// Profiles
+	if data, err := os.ReadFile(filepath.Join(m.baseDir, "ppp_profiles.json")); err == nil {
+		var list []*PPPProfile
+		if err := json.Unmarshal(data, &list); err == nil {
+			for _, p := range list {
+				m.profiles[p.Name] = p
+			}
+		}
+	}
+
+	// Secrets
+	if data, err := os.ReadFile(filepath.Join(m.baseDir, "ppp_secrets.json")); err == nil {
+		var list []*PPPSecret
+		if err := json.Unmarshal(data, &list); err == nil {
+			for _, s := range list {
+				m.secrets[s.ID] = s
+			}
+		}
+	}
+
+	// PPPoE Server
+	if data, err := os.ReadFile(filepath.Join(m.baseDir, "pppoe_server.json")); err == nil {
+		_ = json.Unmarshal(data, &m.pppoeServer)
+	}
+
+	// PPTP Server
+	if data, err := os.ReadFile(filepath.Join(m.baseDir, "pptp_server.json")); err == nil {
+		_ = json.Unmarshal(data, &m.pptpServer)
+	}
+
+	// L2TP Server
+	if data, err := os.ReadFile(filepath.Join(m.baseDir, "l2tp_server.json")); err == nil {
+		_ = json.Unmarshal(data, &m.l2tpServer)
+	}
+
+	// IPSec Server
+	if data, err := os.ReadFile(filepath.Join(m.baseDir, "ipsec_server.json")); err == nil {
+		_ = json.Unmarshal(data, &m.ipsecServer)
+	}
+}
+
+func (m *PPPServerManager) save(filename string, v any) error {
+	_ = os.MkdirAll(m.baseDir, 0755)
+	data, err := json.MarshalIndent(v, "", "  ")
+	if err != nil {
+		return err
+	}
+	return os.WriteFile(filepath.Join(m.baseDir, filename), data, 0644)
+}
+
+// ======================== PPP Profiles CRUD ========================
+func (m *PPPServerManager) ListProfiles() []*PPPProfile {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+	list := make([]*PPPProfile, 0)
+	for _, p := range m.profiles {
+		list = append(list, p)
+	}
+	return list
+}
+
+func (m *PPPServerManager) SaveProfile(p *PPPProfile) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if p.Name == "" {
+		return fmt.Errorf("profile name is required")
+	}
+	if p.LocalAddress == "" {
+		p.LocalAddress = "10.10.10.1"
+	}
+	if p.RemoteAddress == "" {
+		p.RemoteAddress = "10.10.10.10-10.10.10.50"
+	}
+	if p.ID == "" {
+		p.ID = p.Name
+	}
+
+	m.profiles[p.Name] = p
+
+	var list []*PPPProfile
+	for _, pr := range m.profiles {
+		list = append(list, pr)
+	}
+	return m.save("ppp_profiles.json", list)
+}
+
+func (m *PPPServerManager) DeleteProfile(name string) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if name == "default" || name == "default-encryption" {
+		return fmt.Errorf("cannot delete default profile")
+	}
+	delete(m.profiles, name)
+
+	var list []*PPPProfile
+	for _, pr := range m.profiles {
+		list = append(list, pr)
+	}
+	return m.save("ppp_profiles.json", list)
+}
+
+// ======================== PPP Secrets CRUD ========================
+func (m *PPPServerManager) ListSecrets() []*PPPSecret {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+	list := make([]*PPPSecret, 0)
+	for _, s := range m.secrets {
+		list = append(list, s)
+	}
+	return list
+}
+
+func (m *PPPServerManager) SaveSecret(s *PPPSecret) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if s.Name == "" || s.Password == "" {
+		return fmt.Errorf("username and password are required")
+	}
+	if s.Service == "" {
+		s.Service = "any"
+	}
+	if s.Profile == "" {
+		s.Profile = "default"
+	}
+	if s.ID == "" {
+		s.ID = fmt.Sprintf("secret-%d", time.Now().UnixNano())
+	}
+
+	m.secrets[s.ID] = s
+
+	var list []*PPPSecret
+	for _, sc := range m.secrets {
+		list = append(list, sc)
+	}
+	if err := m.save("ppp_secrets.json", list); err != nil {
+		return err
+	}
+
+	return m.SyncSecretsToSystem()
+}
+
+func (m *PPPServerManager) SetSecretState(id string, disabled bool) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	s, ok := m.secrets[id]
+	if !ok {
+		return fmt.Errorf("secret not found")
+	}
+	s.Disabled = disabled
+
+	var list []*PPPSecret
+	for _, sc := range m.secrets {
+		list = append(list, sc)
+	}
+	_ = m.save("ppp_secrets.json", list)
+	return m.SyncSecretsToSystem()
+}
+
+func (m *PPPServerManager) DeleteSecret(id string) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	delete(m.secrets, id)
+	var list []*PPPSecret
+	for _, sc := range m.secrets {
+		list = append(list, sc)
+	}
+	_ = m.save("ppp_secrets.json", list)
+	return m.SyncSecretsToSystem()
+}
+
+// SyncSecretsToSystem synchronizes all enabled secrets into /etc/ppp/chap-secrets and /etc/ppp/pap-secrets
+func (m *PPPServerManager) SyncSecretsToSystem() error {
+	var lines []string
+	lines = append(lines, "# Autogenerated by Proxmox RouterOS - DO NOT EDIT MANUALLY")
+	lines = append(lines, "# client\tserver\tsecret\tIP addresses")
+
+	for _, s := range m.secrets {
+		if s.Disabled {
+			continue
+		}
+		serviceName := "*"
+		if s.Service != "any" && s.Service != "" {
+			serviceName = s.Service
+		}
+		ipAddr := "*"
+		if s.RemoteAddress != "" {
+			ipAddr = s.RemoteAddress
+		}
+		lines = append(lines, fmt.Sprintf("\"%s\"\t%s\t\"%s\"\t%s", s.Name, serviceName, s.Password, ipAddr))
+	}
+
+	content := strings.Join(lines, "\n") + "\n"
+	_ = os.WriteFile("/etc/ppp/chap-secrets", []byte(content), 0600)
+	_ = os.WriteFile("/etc/ppp/pap-secrets", []byte(content), 0600)
+	return nil
+}
+
+// ======================== PPPoE Server Management ========================
+func (m *PPPServerManager) GetPPPoEServer() PPPoEServerConfig {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	// Check if running
+	m.pppoeServer.Running = m.isProcessRunning(m.pppoePIDFile)
+	m.pppoeServer.ActiveSessions = m.countPPPSessions("pppoe")
+	return m.pppoeServer
+}
+
+func (m *PPPServerManager) SetPPPoEServer(cfg PPPoEServerConfig) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	m.pppoeServer.Enabled = cfg.Enabled
+	if cfg.ServiceName != "" {
+		m.pppoeServer.ServiceName = cfg.ServiceName
+	}
+	if cfg.Interface != "" {
+		m.pppoeServer.Interface = cfg.Interface
+	}
+	if cfg.DefaultProfile != "" {
+		m.pppoeServer.DefaultProfile = cfg.DefaultProfile
+	}
+	m.pppoeServer.OneSessionPerHost = cfg.OneSessionPerHost
+	if cfg.MaxMTU > 0 {
+		m.pppoeServer.MaxMTU = cfg.MaxMTU
+	}
+	if cfg.MaxMRU > 0 {
+		m.pppoeServer.MaxMRU = cfg.MaxMRU
+	}
+
+	_ = m.save("pppoe_server.json", m.pppoeServer)
+	return m.applyPPPoEServer()
+}
+
+func (m *PPPServerManager) applyPPPoEServer() error {
+	m.stopPPPoEServer()
+
+	if !m.pppoeServer.Enabled {
+		m.pppoeServer.Running = false
+		m.pppoeServer.LastError = ""
+		return nil
+	}
+
+	// Prepare pppoe-server-options
+	profile := m.profiles[m.pppoeServer.DefaultProfile]
+	if profile == nil {
+		profile = m.profiles["default"]
+	}
+
+	localIP := "10.10.10.1"
+	if profile != nil && profile.LocalAddress != "" {
+		localIP = profile.LocalAddress
+	}
+
+	startIP := "10.10.10.10"
+	poolCount := 50
+	if profile != nil && profile.RemoteAddress != "" {
+		if strings.Contains(profile.RemoteAddress, "-") {
+			parts := strings.Split(profile.RemoteAddress, "-")
+			startIP = strings.TrimSpace(parts[0])
+		} else {
+			startIP = profile.RemoteAddress
+		}
+	}
+
+	// Write /etc/ppp/pppoe-server-options
+	dns1 := "1.1.1.1"
+	dns2 := "8.8.8.8"
+	if profile != nil && profile.DNSServers != "" {
+		parts := strings.Split(profile.DNSServers, ",")
+		if len(parts) > 0 {
+			dns1 = strings.TrimSpace(parts[0])
+		}
+		if len(parts) > 1 {
+			dns2 = strings.TrimSpace(parts[1])
+		}
+	}
+
+	opts := fmt.Sprintf(`auth
+require-chap
+login
+lcp-echo-interval 10
+lcp-echo-failure 2
+ms-dns %s
+ms-dns %s
+netmask 255.255.255.0
+default-mru
+default-asyncmap
+proxyarp
+`, dns1, dns2)
+	_ = os.WriteFile("/etc/ppp/pppoe-server-options", []byte(opts), 0644)
+
+	// Launch pppoe-server
+	// pppoe-server -I <if> -L <local_ip> -R <start_ip> -N <count> -S <service> -O /etc/ppp/pppoe-server-options -X <pidfile>
+	args := []string{
+		"-I", m.pppoeServer.Interface,
+		"-L", localIP,
+		"-R", startIP,
+		"-N", strconv.Itoa(poolCount),
+		"-S", m.pppoeServer.ServiceName,
+		"-O", "/etc/ppp/pppoe-server-options",
+		"-X", m.pppoePIDFile,
+	}
+	if m.pppoeServer.OneSessionPerHost {
+		args = append(args, "-x", "1")
+	}
+
+	cmd := exec.Command("/usr/sbin/pppoe-server", args...)
+	if out, err := cmd.CombinedOutput(); err != nil {
+		m.pppoeServer.LastError = fmt.Sprintf("start failed: %s - %v", strings.TrimSpace(string(out)), err)
+		m.pppoeServer.Running = false
+		return fmt.Errorf("%s", m.pppoeServer.LastError)
+	}
+
+	time.Sleep(300 * time.Millisecond)
+	m.pppoeServer.Running = m.isProcessRunning(m.pppoePIDFile)
+	m.pppoeServer.LastError = ""
+	return nil
+}
+
+func (m *PPPServerManager) stopPPPoEServer() {
+	if data, err := os.ReadFile(m.pppoePIDFile); err == nil {
+		pid, _ := strconv.Atoi(strings.TrimSpace(string(data)))
+		if pid > 0 {
+			_ = syscall.Kill(pid, syscall.SIGTERM)
+			time.Sleep(200 * time.Millisecond)
+			_ = syscall.Kill(pid, syscall.SIGKILL)
+		}
+		_ = os.Remove(m.pppoePIDFile)
+	}
+	_ = exec.Command("pkill", "-f", "/usr/sbin/pppoe-server").Run()
+}
+
+// ======================== PPTP Server Management ========================
+func (m *PPPServerManager) GetPPTPServer() PPTPServerConfig {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	out, _ := exec.Command("systemctl", "is-active", "pptpd").Output()
+	m.pptpServer.Running = strings.TrimSpace(string(out)) == "active"
+	m.pptpServer.ActiveSessions = m.countPPPSessions("pptp")
+	return m.pptpServer
+}
+
+func (m *PPPServerManager) SetPPTPServer(cfg PPTPServerConfig) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	m.pptpServer.Enabled = cfg.Enabled
+	if cfg.Port > 0 {
+		m.pptpServer.Port = cfg.Port
+	}
+	if cfg.DefaultProfile != "" {
+		m.pptpServer.DefaultProfile = cfg.DefaultProfile
+	}
+	m.pptpServer.RequireMPPE = cfg.RequireMPPE
+
+	_ = m.save("pptp_server.json", m.pptpServer)
+	return m.applyPPTPServer()
+}
+
+func (m *PPPServerManager) applyPPTPServer() error {
+	if !m.pptpServer.Enabled {
+		_ = exec.Command("systemctl", "stop", "pptpd").Run()
+		m.pptpServer.Running = false
+		m.pptpServer.LastError = ""
+		return nil
+	}
+
+	profile := m.profiles[m.pptpServer.DefaultProfile]
+	if profile == nil {
+		profile = m.profiles["default-encryption"]
+	}
+
+	localIP := "10.10.20.1"
+	if profile != nil && profile.LocalAddress != "" {
+		localIP = profile.LocalAddress
+	}
+
+	remotePool := "10.10.20.10-50"
+	if profile != nil && profile.RemoteAddress != "" {
+		remotePool = profile.RemoteAddress
+	}
+
+	// Write /etc/pptpd.conf
+	pptpdConf := fmt.Sprintf(`option /etc/ppp/pptpd-options
+logwtmp
+localip %s
+remoteip %s
+`, localIP, remotePool)
+	_ = os.WriteFile("/etc/pptpd.conf", []byte(pptpdConf), 0644)
+
+	// Write /etc/ppp/pptpd-options
+	dns1 := "1.1.1.1"
+	dns2 := "8.8.8.8"
+	if profile != nil && profile.DNSServers != "" {
+		parts := strings.Split(profile.DNSServers, ",")
+		if len(parts) > 0 {
+			dns1 = strings.TrimSpace(parts[0])
+		}
+		if len(parts) > 1 {
+			dns2 = strings.TrimSpace(parts[1])
+		}
+	}
+
+	mppeRule := "require-mppe-128"
+	if !m.pptpServer.RequireMPPE {
+		mppeRule = "# require-mppe-128"
+	}
+
+	pptpdOpts := fmt.Sprintf(`name pptpd
+refuse-pap
+refuse-chap
+refuse-mschap
+require-mschap-v2
+%s
+ms-dns %s
+ms-dns %s
+proxyarp
+nodefaultroute
+lock
+nobsdcomp
+novj
+novjccomp
+nologfd
+`, mppeRule, dns1, dns2)
+	_ = os.WriteFile("/etc/ppp/pptpd-options", []byte(pptpdOpts), 0644)
+
+	_ = exec.Command("systemctl", "restart", "pptpd").Run()
+	time.Sleep(300 * time.Millisecond)
+
+	out, _ := exec.Command("systemctl", "is-active", "pptpd").Output()
+	m.pptpServer.Running = strings.TrimSpace(string(out)) == "active"
+	m.pptpServer.LastError = ""
+	return nil
+}
+
+// ======================== L2TP Server Management ========================
+func (m *PPPServerManager) GetL2TPServer() L2TPServerConfig {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	out, _ := exec.Command("systemctl", "is-active", "xl2tpd").Output()
+	m.l2tpServer.Running = strings.TrimSpace(string(out)) == "active"
+	m.l2tpServer.ActiveSessions = m.countPPPSessions("l2tp")
+	return m.l2tpServer
+}
+
+func (m *PPPServerManager) SetL2TPServer(cfg L2TPServerConfig) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	m.l2tpServer.Enabled = cfg.Enabled
+	if cfg.Port > 0 {
+		m.l2tpServer.Port = cfg.Port
+	}
+	if cfg.DefaultProfile != "" {
+		m.l2tpServer.DefaultProfile = cfg.DefaultProfile
+	}
+	if cfg.UseIPSec != "" {
+		m.l2tpServer.UseIPSec = cfg.UseIPSec
+	}
+	m.l2tpServer.IPSecSecret = cfg.IPSecSecret
+
+	_ = m.save("l2tp_server.json", m.l2tpServer)
+	return m.applyL2TPServer()
+}
+
+func (m *PPPServerManager) applyL2TPServer() error {
+	if !m.l2tpServer.Enabled {
+		// When disabled, don't stop xl2tpd completely as client may need it, just reload without LNS
+		_ = os.WriteFile("/etc/ppp/options.xl2tpd.server", []byte("# disabled\n"), 0644)
+		_ = exec.Command("systemctl", "reload-or-restart", "xl2tpd").Run()
+		m.l2tpServer.Running = false
+		m.l2tpServer.LastError = ""
+		return nil
+	}
+
+	profile := m.profiles[m.l2tpServer.DefaultProfile]
+	if profile == nil {
+		profile = m.profiles["default"]
+	}
+
+	localIP := "10.10.40.1"
+	if profile != nil && profile.LocalAddress != "" {
+		localIP = profile.LocalAddress
+	}
+
+	remotePool := "10.10.40.10-10.10.40.50"
+	if profile != nil && profile.RemoteAddress != "" {
+		remotePool = profile.RemoteAddress
+	}
+
+	// Write /etc/xl2tpd/xl2tpd.conf with [lns default]
+	_ = os.MkdirAll("/etc/xl2tpd", 0755)
+	xl2tpdConf := fmt.Sprintf(`[global]
+port = %d
+
+[lns default]
+ip range = %s
+local ip = %s
+require chap = yes
+refuse pap = yes
+require authentication = yes
+name = l2tpd
+pppoptfile = /etc/ppp/options.xl2tpd.server
+length bit = yes
+`, m.l2tpServer.Port, remotePool, localIP)
+	_ = os.WriteFile("/etc/xl2tpd/xl2tpd.conf", []byte(xl2tpdConf), 0644)
+
+	// Write /etc/ppp/options.xl2tpd.server
+	dns1 := "1.1.1.1"
+	dns2 := "8.8.8.8"
+	if profile != nil && profile.DNSServers != "" {
+		parts := strings.Split(profile.DNSServers, ",")
+		if len(parts) > 0 {
+			dns1 = strings.TrimSpace(parts[0])
+		}
+		if len(parts) > 1 {
+			dns2 = strings.TrimSpace(parts[1])
+		}
+	}
+
+	opts := fmt.Sprintf(`require-mschap-v2
+ms-dns %s
+ms-dns %s
+auth
+mtu 1400
+mru 1400
+proxyarp
+nodefaultroute
+`, dns1, dns2)
+	_ = os.WriteFile("/etc/ppp/options.xl2tpd.server", []byte(opts), 0644)
+
+	_ = exec.Command("systemctl", "restart", "xl2tpd").Run()
+	time.Sleep(300 * time.Millisecond)
+
+	out, _ := exec.Command("systemctl", "is-active", "xl2tpd").Output()
+	m.l2tpServer.Running = strings.TrimSpace(string(out)) == "active"
+	m.l2tpServer.LastError = ""
+	return nil
+}
+
+// ======================== IPSec Server Management ========================
+func (m *PPPServerManager) GetIPSecServer() IPSecServerConfig {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	out, _ := exec.Command("swanctl", "--list-conns").Output()
+	m.ipsecServer.Running = strings.Contains(string(out), "rw-server")
+	m.ipsecServer.ActiveSessions = m.countIPSecSessions()
+	return m.ipsecServer
+}
+
+func (m *PPPServerManager) SetIPSecServer(cfg IPSecServerConfig) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	m.ipsecServer.Enabled = cfg.Enabled
+	if cfg.PoolAddress != "" {
+		m.ipsecServer.PoolAddress = cfg.PoolAddress
+	}
+	if cfg.PresharedKey != "" {
+		m.ipsecServer.PresharedKey = cfg.PresharedKey
+	}
+	if cfg.LocalSubnet != "" {
+		m.ipsecServer.LocalSubnet = cfg.LocalSubnet
+	}
+	if cfg.DNSServers != "" {
+		m.ipsecServer.DNSServers = cfg.DNSServers
+	}
+
+	_ = m.save("ipsec_server.json", m.ipsecServer)
+	return m.applyIPSecServer()
+}
+
+func (m *PPPServerManager) applyIPSecServer() error {
+	confFile := "/etc/swanctl/conf.d/rw-server.conf"
+	if !m.ipsecServer.Enabled {
+		_ = os.Remove(confFile)
+		_ = exec.Command("swanctl", "--load-all").Run()
+		m.ipsecServer.Running = false
+		m.ipsecServer.LastError = ""
+		return nil
+	}
+
+	_ = os.MkdirAll("/etc/swanctl/conf.d", 0755)
+	swanConf := fmt.Sprintf(`connections {
+    rw-server {
+        local_addrs = %%any
+        pools = rw-pool
+        local {
+            auth = psk
+            id = %%any
+        }
+        remote {
+            auth = psk
+        }
+        children {
+            rw-net {
+                local_ts = %s
+                esp_proposals = aes256gcm128-sha256-modp2048,aes256-sha256-modp2048
+            }
+        }
+    }
+}
+pools {
+    rw-pool {
+        addrs = %s
+        dns = %s
+    }
+}
+secrets {
+    ike-rw {
+        secret = "%s"
+    }
+}
+`, m.ipsecServer.LocalSubnet, m.ipsecServer.PoolAddress, m.ipsecServer.DNSServers, m.ipsecServer.PresharedKey)
+
+	if err := os.WriteFile(confFile, []byte(swanConf), 0600); err != nil {
+		m.ipsecServer.LastError = err.Error()
+		return err
+	}
+
+	if out, err := exec.Command("swanctl", "--load-all").CombinedOutput(); err != nil {
+		m.ipsecServer.LastError = strings.TrimSpace(string(out))
+		return err
+	}
+
+	m.ipsecServer.Running = true
+	m.ipsecServer.LastError = ""
+	return nil
+}
+
+// ======================== Active Connections & Kill ========================
+func (m *PPPServerManager) ListActiveSessions() []ActivePPPSession {
+	list := make([]ActivePPPSession, 0)
+
+	// Read network interfaces matching ppp*
+	ifaces, err := net.Interfaces()
+	if err != nil {
+		return list
+	}
+
+	for _, iface := range ifaces {
+		if !strings.HasPrefix(iface.Name, "ppp") {
+			continue
+		}
+
+		addrs, _ := iface.Addrs()
+		var assignedIP string
+		for _, a := range addrs {
+			if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() && ipNet.IP.To4() != nil {
+				assignedIP = ipNet.IP.String()
+				break
+			}
+		}
+
+		sess := ActivePPPSession{
+			ID:        iface.Name,
+			Interface: iface.Name,
+			Address:   assignedIP,
+			Uptime:    "active",
+		}
+
+		// Try to detect service from process or default
+		sess.Service = "ppp"
+		sess.Username = "client-" + iface.Name
+
+		m.populateIfaceTraffic(iface.Name, &sess)
+		list = append(list, sess)
+	}
+
+	return list
+}
+
+func (m *PPPServerManager) DisconnectSession(ifaceName string) error {
+	if !strings.HasPrefix(ifaceName, "ppp") {
+		return fmt.Errorf("invalid ppp interface")
+	}
+	_ = exec.Command("ip", "link", "set", ifaceName, "down").Run()
+	_ = exec.Command("pkill", "-f", ifaceName).Run()
+	return nil
+}
+
+func (m *PPPServerManager) populateIfaceTraffic(ifaceName string, sess *ActivePPPSession) {
+	file, err := os.Open("/proc/net/dev")
+	if err != nil {
+		return
+	}
+	defer file.Close()
+
+	scanner := bufio.NewScanner(file)
+	for scanner.Scan() {
+		line := scanner.Text()
+		if strings.Contains(line, ifaceName+":") {
+			parts := strings.Fields(line)
+			if len(parts) >= 10 {
+				rxBytes, _ := strconv.ParseUint(parts[1], 10, 64)
+				txBytes, _ := strconv.ParseUint(parts[9], 10, 64)
+				sess.RxBytes = rxBytes
+				sess.TxBytes = txBytes
+				sess.RxHuman = formatBytes(rxBytes)
+				sess.TxHuman = formatBytes(txBytes)
+			}
+			break
+		}
+	}
+}
+
+func (m *PPPServerManager) countPPPSessions(service string) int {
+	ifaces, err := net.Interfaces()
+	if err != nil {
+		return 0
+	}
+	count := 0
+	for _, i := range ifaces {
+		if strings.HasPrefix(i.Name, "ppp") {
+			count++
+		}
+	}
+	return count
+}
+
+func (m *PPPServerManager) countIPSecSessions() int {
+	out, err := exec.Command("swanctl", "--list-sas").Output()
+	if err != nil {
+		return 0
+	}
+	return strings.Count(string(out), "ESTABLISHED")
+}
+
+func (m *PPPServerManager) isProcessRunning(pidFile string) bool {
+	data, err := os.ReadFile(pidFile)
+	if err != nil {
+		return false
+	}
+	pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
+	if err != nil || pid <= 0 {
+		return false
+	}
+	return syscall.Kill(pid, 0) == nil
+}
+
+func (m *PPPServerManager) applyAll() {
+	if m.pppoeServer.Enabled {
+		_ = m.applyPPPoEServer()
+	}
+	if m.pptpServer.Enabled {
+		_ = m.applyPPTPServer()
+	}
+	if m.l2tpServer.Enabled {
+		_ = m.applyL2TPServer()
+	}
+	if m.ipsecServer.Enabled {
+		_ = m.applyIPSecServer()
+	}
+}

+ 19 - 12
internal/vpn/types.go

@@ -2,18 +2,25 @@ package vpn
 
 // OverallVPNStatus provides quick telemetry for the sidebar badges
 type OverallVPNStatus struct {
-	TotalPPPoE        int `json:"total_pppoe"`
-	ActivePPPoE       int `json:"active_pppoe"`
-	TotalWireGuard    int `json:"total_wireguard"`
-	ActiveWireGuard   int `json:"active_wireguard"`
-	TotalWireGuardPeers int `json:"total_wg_peers"`
-	TotalPPTP         int `json:"total_pptp"`
-	ActivePPTP        int `json:"active_pptp"`
-	TotalL2TP         int `json:"total_l2tp"`
-	ActiveL2TP        int `json:"active_l2tp"`
-	TotalIPSec        int `json:"total_ipsec"`
-	ActiveIPSec       int `json:"active_ipsec"`
-	TotalActiveTunnels int `json:"total_active_tunnels"`
+	TotalPPPoE          int  `json:"total_pppoe"`
+	ActivePPPoE         int  `json:"active_pppoe"`
+	TotalWireGuard      int  `json:"total_wireguard"`
+	ActiveWireGuard     int  `json:"active_wireguard"`
+	TotalWireGuardPeers int  `json:"total_wg_peers"`
+	TotalPPTP           int  `json:"total_pptp"`
+	ActivePPTP          int  `json:"active_pptp"`
+	TotalL2TP           int  `json:"total_l2tp"`
+	ActiveL2TP          int  `json:"active_l2tp"`
+	TotalIPSec          int  `json:"total_ipsec"`
+	ActiveIPSec         int  `json:"active_ipsec"`
+	TotalEoIP           int  `json:"total_eoip"`
+	ActiveEoIP          int  `json:"active_eoip"`
+	TotalPPPSessions    int  `json:"total_ppp_sessions"`
+	PPPoEServerRunning  bool `json:"pppoe_server_running"`
+	PPTPServerRunning   bool `json:"pptp_server_running"`
+	L2TPServerRunning   bool `json:"l2tp_server_running"`
+	IPSecServerRunning  bool `json:"ipsec_server_running"`
+	TotalActiveTunnels  int  `json:"total_active_tunnels"`
 }
 
 // ======================== PPPoE Client (MikroTik style) ========================

+ 37 - 0
internal/vpn/wireguard.go

@@ -427,3 +427,40 @@ func (m *WireGuardManager) generateWGConfigFile(ifaceName string) error {
 	path := fmt.Sprintf("/etc/wireguard/%s.conf", ifaceName)
 	return os.WriteFile(path, []byte(sb.String()), 0600)
 }
+
+// GenerateClientConfig creates a ready-to-use client .conf for mobile/desktop
+func (m *WireGuardManager) GenerateClientConfig(ifaceName, clientPrivKey, clientAddress, serverEndpoint, dns string) (string, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	var targetIface *WireGuardInterface
+	for _, i := range m.interfaces {
+		if i.Name == ifaceName {
+			targetIface = &i
+			break
+		}
+	}
+	if targetIface == nil {
+		return "", fmt.Errorf("interface %s not found", ifaceName)
+	}
+
+	if dns == "" {
+		dns = "1.1.1.1, 8.8.8.8"
+	}
+	if serverEndpoint == "" {
+		serverEndpoint = fmt.Sprintf("YOUR_ROUTER_IP:%d", targetIface.ListenPort)
+	}
+
+	var sb strings.Builder
+	sb.WriteString("[Interface]\n")
+	sb.WriteString(fmt.Sprintf("PrivateKey = %s\n", clientPrivKey))
+	sb.WriteString(fmt.Sprintf("Address = %s\n", clientAddress))
+	sb.WriteString(fmt.Sprintf("DNS = %s\n", dns))
+	sb.WriteString("\n[Peer]\n")
+	sb.WriteString(fmt.Sprintf("PublicKey = %s\n", targetIface.PublicKey))
+	sb.WriteString(fmt.Sprintf("Endpoint = %s\n", serverEndpoint))
+	sb.WriteString("AllowedIPs = 0.0.0.0/0, ::/0\n")
+	sb.WriteString("PersistentKeepalive = 25\n")
+
+	return sb.String(), nil
+}

+ 868 - 3
internal/web/static/app.js

@@ -8,6 +8,15 @@ let state = {
   activeTab: 'dashboard',
   fwSubTab: 'filter',
   routesSubTab: 'routes',
+  pppSubTab: 'secrets',
+  eoipTunnels: [],
+  pppSecrets: [],
+  pppProfiles: [],
+  pppActiveSessions: [],
+  pppoeServer: null,
+  pptpServer: null,
+  l2tpServer: null,
+  ipsecServer: null,
   ifaceFilter: 'all',
   currentModalIface: null,
   isPolling: true,
@@ -65,6 +74,8 @@ function navigate(tabName) {
     vlans: { title: 'VLAN Virtual Networks', sub: 'IEEE 802.1Q sub-interface segmentation and routing' },
     firewall: { title: 'MikroTik Firewall', sub: 'Filter rules, NAT, Mangle, Address Lists & Active Connections tracking' },
     routes: { title: 'Routing & Policy', sub: 'Routing tables, static routes (DAS/DAC), and policy routing rules' },
+    ppp: { title: 'PPP Servers & Secrets', sub: 'MikroTik RouterOS user accounts, IP pools and VPN server concentrators' },
+    eoip: { title: 'EoIP Tunnels', sub: 'Ethernet over IP Layer 2 tunneling (MikroTik RouterOS style)' },
     pppoe: { title: 'PPPoE Client WAN', sub: 'Point-to-Point Protocol over Ethernet WAN access' },
     wireguard: { title: 'WireGuard Kernel VPN', sub: 'Modern ultra-fast cryptographic VPN tunnel manager' },
     ipsec: { title: 'IPSec (IKEv2) strongSwan', sub: 'Enterprise Site-to-Site & Roadwarrior IPSec controller' },
@@ -90,6 +101,8 @@ function navigate(tabName) {
 
   if (tabName === 'firewall') loadFirewall();
   if (tabName === 'routes') loadRoutes();
+  if (tabName === 'ppp') loadPPP();
+  if (tabName === 'eoip') loadEoIP();
   if (tabName === 'persistence') loadDebianConfig();
   if (tabName === 'pppoe') loadPPPoE();
   if (tabName === 'wireguard') loadWireGuard();
@@ -132,6 +145,8 @@ async function fetchData(isManual = false) {
     if (state.activeTab === 'vlans') renderVlansList();
     if (state.activeTab === 'firewall') loadFirewall(false);
     if (state.activeTab === 'routes') loadRoutes(false);
+    if (state.activeTab === 'ppp') loadPPP(false);
+    if (state.activeTab === 'eoip') loadEoIP(false);
     if (state.activeTab === 'pppoe') loadPPPoE(false);
     if (state.activeTab === 'wireguard') loadWireGuard(false);
     if (state.activeTab === 'ipsec') loadIPSec(false);
@@ -223,13 +238,27 @@ function setDotBadge(id, active) {
 }
 
 function updateBadges() {
-  document.getElementById('badge-ifaces-count').innerText = (state.interfaces || []).length;
-  document.getElementById('badge-vlans-count').innerText = (state.vlans || []).length;
+  const bIfaces = document.getElementById('badge-ifaces-count');
+  if (bIfaces) bIfaces.innerText = (state.interfaces || []).length;
+  const bVlans = document.getElementById('badge-vlans-count');
+  if (bVlans) bVlans.innerText = (state.vlans || []).length;
   if (state.fwSummary) {
     const totalFwRules = (state.fwSummary.filter_rules_count || 0) + (state.fwSummary.nat_rules_count || 0) + (state.fwSummary.mangle_rules_count || 0);
     const badgeFw = document.getElementById('badge-firewall-count');
     if (badgeFw) badgeFw.innerText = totalFwRules;
   }
+  if (state.vpnStatus) {
+    const v = state.vpnStatus;
+    const bEoIP = document.getElementById('badge-eoip-count');
+    if (bEoIP) bEoIP.innerText = v.total_eoip || 0;
+    const bPPPSess = document.getElementById('badge-ppp-sessions');
+    if (bPPPSess) bPPPSess.innerText = v.total_ppp_sessions || 0;
+    setDotBadge('badge-pppoe-status', v.active_pppoe > 0 || v.pppoe_server_running);
+    setDotBadge('badge-wg-status', v.active_wireguard > 0);
+    setDotBadge('badge-ipsec-status', v.active_ipsec > 0 || v.ipsec_server_running);
+    setDotBadge('badge-l2tp-status', v.active_l2tp > 0 || v.l2tp_server_running);
+    setDotBadge('badge-pptp-status', v.active_pptp > 0 || v.pptp_server_running);
+  }
 }
 
 // Render Dashboard View
@@ -1021,7 +1050,8 @@ function renderWGPeers() {
           <span class="text-emerald-400">↓ ${p.transfer_rx_human || '0 B'}</span>
           <span class="text-cyan-400 ml-1">↑ ${p.transfer_tx_human || '0 B'}</span>
         </td>
-        <td class="py-2.5 px-3 text-right font-sans">
+        <td class="py-2.5 px-3 text-right font-sans space-x-2">
+          <button onclick="openWGClientConfModal('${p.interface}', '${(p.allowed_ips || [])[0] || '10.10.0.2/32'}')" class="text-xs text-cyan-400 hover:text-cyan-300 font-semibold" title="Generate WireGuard Client Profile (.conf)">Config (.conf)</button>
           <button onclick="deleteWGPeer('${p.interface}', '${p.public_key}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>
         </td>
       `;
@@ -3414,3 +3444,838 @@ function toggleSidebar() {
   const sb = document.getElementById('sidebar');
   sb.classList.toggle('-translate-x-full');
 }
+
+// ======================== EoIP (Ethernet over IP) ========================
+async function loadEoIP(populate = true) {
+  try {
+    const res = await fetch(`${API_BASE}/vpn/eoip`);
+    const data = await res.json();
+    state.eoipTunnels = Array.isArray(data) ? data : [];
+
+    const badge = document.getElementById('eoip-count-badge');
+    if (badge) badge.innerText = `${state.eoipTunnels.length} Tunnels`;
+    const sideBadge = document.getElementById('badge-eoip-count');
+    if (sideBadge) sideBadge.innerText = state.eoipTunnels.length;
+
+    renderEoIPTunnels();
+  } catch (err) {
+    console.error('Failed to load EoIP:', err);
+  }
+}
+
+function renderEoIPTunnels() {
+  const tbody = document.getElementById('eoip-table-body');
+  const empty = document.getElementById('eoip-empty');
+  if (!tbody) return;
+  tbody.innerHTML = '';
+
+  const tunnels = state.eoipTunnels || [];
+  if (tunnels.length === 0) {
+    empty?.classList.remove('hidden');
+    return;
+  }
+  empty?.classList.add('hidden');
+
+  tunnels.forEach(t => {
+    const tr = document.createElement('tr');
+    tr.className = `hover:bg-slate-800/40 transition text-xs ${t.disabled ? 'opacity-50' : ''}`;
+    tr.innerHTML = `
+      <td class="py-2.5 px-3 text-center">${renderMikroTikFlag(t.running, t.disabled)}</td>
+      <td class="py-2.5 px-3 font-bold text-white font-mono">${t.name}</td>
+      <td class="py-2.5 px-3 font-bold text-emerald-400 font-mono">${t.tunnel_id}</td>
+      <td class="py-2.5 px-3 text-cyan-300 font-mono">${t.remote_address}</td>
+      <td class="py-2.5 px-3 text-slate-400 font-mono">${t.local_address || '(any)'}</td>
+      <td class="py-2.5 px-3 text-slate-300 font-mono text-[11px]">${t.mac_address || '-'}</td>
+      <td class="py-2.5 px-3 text-slate-400 font-mono">${t.mtu || 1500}</td>
+      <td class="py-2.5 px-3 font-mono text-[11px]">
+        <span class="text-emerald-400">↓ ${t.rx_human || '0 B'}</span>
+        <span class="text-cyan-400 ml-1">↑ ${t.tx_human || '0 B'}</span>
+      </td>
+      <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[120px]" title="${t.comment || ''}">${t.comment || '-'}</td>
+      <td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
+        <button onclick="toggleEoIPState('${t.id}', ${!t.disabled})" class="text-xs ${t.disabled ? 'text-emerald-400' : 'text-slate-400'} hover:underline">${t.disabled ? 'Enable' : 'Disable'}</button>
+        <button onclick="openEditEoIPModal('${t.id}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
+        <button onclick="deleteEoIP('${t.id}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>
+      </td>
+    `;
+    tbody.appendChild(tr);
+  });
+}
+
+function openAddEoIPModal() {
+  document.getElementById('eoip-modal-title').innerText = 'Add EoIP Tunnel';
+  document.getElementById('eoip-modal-id').value = '';
+  document.getElementById('eoip-modal-name').value = `eoip-tunnel${(state.eoipTunnels || []).length + 1}`;
+  document.getElementById('eoip-modal-tunnelid').value = (state.eoipTunnels || []).length + 10;
+  document.getElementById('eoip-modal-remote').value = '';
+  document.getElementById('eoip-modal-local').value = '';
+  document.getElementById('eoip-modal-mac').value = '';
+  document.getElementById('eoip-modal-mtu').value = '1500';
+  document.getElementById('eoip-modal-comment').value = '';
+  document.getElementById('eoip-modal-clampmss').checked = true;
+  document.getElementById('eoip-modal-disabled').checked = false;
+  document.getElementById('eoip-modal').classList.remove('hidden');
+}
+
+function openEditEoIPModal(id) {
+  const t = (state.eoipTunnels || []).find(tun => tun.id === id);
+  if (!t) return;
+  document.getElementById('eoip-modal-title').innerText = `Edit EoIP Tunnel (${t.name})`;
+  document.getElementById('eoip-modal-id').value = t.id;
+  document.getElementById('eoip-modal-name').value = t.name;
+  document.getElementById('eoip-modal-tunnelid').value = t.tunnel_id;
+  document.getElementById('eoip-modal-remote').value = t.remote_address;
+  document.getElementById('eoip-modal-local').value = t.local_address || '';
+  document.getElementById('eoip-modal-mac').value = t.mac_address || '';
+  document.getElementById('eoip-modal-mtu').value = t.mtu || 1500;
+  document.getElementById('eoip-modal-comment').value = t.comment || '';
+  document.getElementById('eoip-modal-clampmss').checked = !!t.clamp_mss;
+  document.getElementById('eoip-modal-disabled').checked = !!t.disabled;
+  document.getElementById('eoip-modal').classList.remove('hidden');
+}
+
+function closeEoIPModal() {
+  document.getElementById('eoip-modal').classList.add('hidden');
+}
+
+async function submitSaveEoIP(e) {
+  e.preventDefault();
+  const id = document.getElementById('eoip-modal-id').value;
+  const name = document.getElementById('eoip-modal-name').value.trim();
+  const tunnelId = parseInt(document.getElementById('eoip-modal-tunnelid').value);
+  const remote = document.getElementById('eoip-modal-remote').value.trim();
+  const local = document.getElementById('eoip-modal-local').value.trim();
+  const mac = document.getElementById('eoip-modal-mac').value.trim();
+  const mtu = parseInt(document.getElementById('eoip-modal-mtu').value) || 1500;
+  const comment = document.getElementById('eoip-modal-comment').value.trim();
+  const clampMss = document.getElementById('eoip-modal-clampmss').checked;
+  const disabled = document.getElementById('eoip-modal-disabled').checked;
+
+  try {
+    const res = await fetch(`${API_BASE}/vpn/eoip`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        id,
+        name,
+        tunnel_id: tunnelId,
+        remote_address: remote,
+        local_address: local,
+        mac_address: mac,
+        mtu,
+        clamp_mss: clampMss,
+        comment,
+        disabled
+      })
+    });
+    if (res.ok) {
+      showToast('EoIP Saved', `Tunnel ${name} configured successfully`, 'success');
+      closeEoIPModal();
+      loadEoIP(false);
+      fetchData(false);
+    } else {
+      const err = await res.json();
+      showToast('Error', err.message, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function toggleEoIPState(id, disabled) {
+  try {
+    const res = await fetch(`${API_BASE}/vpn/eoip/${encodeURIComponent(id)}/state`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ disabled })
+    });
+    if (res.ok) {
+      showToast(disabled ? 'Disabled' : 'Enabled', 'EoIP tunnel state updated', 'info');
+      loadEoIP(false);
+      fetchData(false);
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function deleteEoIP(id) {
+  if (!confirm('Are you sure you want to delete this EoIP tunnel?')) return;
+  try {
+    const res = await fetch(`${API_BASE}/vpn/eoip/${encodeURIComponent(id)}`, { method: 'DELETE' });
+    if (res.ok) {
+      showToast('Deleted', 'EoIP tunnel removed', 'success');
+      loadEoIP(false);
+      fetchData(false);
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+// ======================== PPP Central Hub & Servers ========================
+function switchPPPSubTab(tab) {
+  state.pppSubTab = tab;
+  const tabs = ['secrets', 'profiles', 'pppoe', 'pptp', 'l2tp', 'active'];
+  tabs.forEach(t => {
+    const panel = document.getElementById(`ppp-subtab-${t}`);
+    const btn = document.getElementById(`ppp-subtab-btn-${t}`);
+    if (panel) {
+      if (t === tab) panel.classList.remove('hidden');
+      else panel.classList.add('hidden');
+    }
+    if (btn) {
+      if (t === tab) {
+        btn.className = 'px-4 py-2.5 text-xs font-bold border-b-2 border-violet-500 text-white transition flex items-center gap-2 whitespace-nowrap';
+      } else {
+        btn.className = 'px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap';
+      }
+    }
+  });
+
+  const addBtn = document.getElementById('ppp-primary-add-btn');
+  if (addBtn) {
+    if (tab === 'secrets') addBtn.innerHTML = '<span>+ Add User (Secret)</span>';
+    else if (tab === 'profiles') addBtn.innerHTML = '<span>+ Add Profile</span>';
+    else if (tab === 'active') addBtn.innerHTML = '<span>Refresh Sessions</span>';
+    else addBtn.innerHTML = '<span>Save Config</span>';
+  }
+
+  if (tab === 'active') loadPPPSessions();
+}
+
+function openCurrentPPPModal() {
+  if (state.pppSubTab === 'secrets') openAddPPPSecretModal();
+  else if (state.pppSubTab === 'profiles') openAddPPPProfileModal();
+  else if (state.pppSubTab === 'active') loadPPPSessions();
+  else if (state.pppSubTab === 'pppoe') document.querySelector('#ppp-subtab-pppoe form')?.requestSubmit();
+  else if (state.pppSubTab === 'pptp') document.querySelector('#ppp-subtab-pptp form')?.requestSubmit();
+  else if (state.pppSubTab === 'l2tp') document.querySelector('#ppp-subtab-l2tp form')?.requestSubmit();
+}
+
+async function loadPPP(populateDropdowns = true) {
+  await Promise.all([
+    loadPPPSecrets(),
+    loadPPPProfiles(),
+    loadPPPServers(),
+    loadPPPSessions()
+  ]);
+}
+
+async function loadPPPSecrets() {
+  try {
+    const res = await fetch(`${API_BASE}/vpn/ppp/secrets`);
+    const data = await res.json();
+    state.pppSecrets = Array.isArray(data) ? data : [];
+    const b = document.getElementById('ppp-badge-secrets');
+    if (b) b.innerText = state.pppSecrets.length;
+    const b2 = document.getElementById('secrets-count-badge');
+    if (b2) b2.innerText = `${state.pppSecrets.length} Users`;
+    renderPPPSecrets();
+  } catch (err) {
+    console.error('Failed to load PPP secrets:', err);
+  }
+}
+
+function renderPPPSecrets() {
+  const tbody = document.getElementById('secrets-table-body');
+  const empty = document.getElementById('secrets-empty');
+  if (!tbody) return;
+  tbody.innerHTML = '';
+
+  const secrets = state.pppSecrets || [];
+  if (secrets.length === 0) {
+    empty?.classList.remove('hidden');
+    return;
+  }
+  empty?.classList.add('hidden');
+
+  secrets.forEach(s => {
+    const tr = document.createElement('tr');
+    tr.className = `hover:bg-slate-800/40 transition text-xs ${s.disabled ? 'opacity-50' : ''}`;
+    tr.innerHTML = `
+      <td class="py-2.5 px-3 text-center">${renderRuleFlag(s.disabled, !!s.comment)}</td>
+      <td class="py-2.5 px-3 font-bold text-white font-mono">${s.name}</td>
+      <td class="py-2.5 px-3 text-purple-300 font-mono font-semibold">${s.service}</td>
+      <td class="py-2.5 px-3 text-amber-300 font-mono">${s.profile}</td>
+      <td class="py-2.5 px-3 text-slate-400 font-mono">${s.local_address || '(profile)'}</td>
+      <td class="py-2.5 px-3 text-cyan-400 font-mono font-bold">${s.remote_address || '(profile)'}</td>
+      <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[120px]">${s.comment || '-'}</td>
+      <td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
+        <button onclick="togglePPPSecretState('${s.id}', ${!s.disabled})" class="text-xs ${s.disabled ? 'text-emerald-400' : 'text-slate-400'} hover:underline">${s.disabled ? 'Enable' : 'Disable'}</button>
+        <button onclick="openEditPPPSecretModal('${s.id}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
+        <button onclick="deletePPPSecret('${s.id}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>
+      </td>
+    `;
+    tbody.appendChild(tr);
+  });
+}
+
+async function loadPPPProfiles() {
+  try {
+    const res = await fetch(`${API_BASE}/vpn/ppp/profiles`);
+    const data = await res.json();
+    state.pppProfiles = Array.isArray(data) ? data : [];
+    const b = document.getElementById('ppp-badge-profiles');
+    if (b) b.innerText = state.pppProfiles.length;
+    const b2 = document.getElementById('profiles-count-badge');
+    if (b2) b2.innerText = `${state.pppProfiles.length} Profiles`;
+
+    // Populate profile selects across modals
+    const selects = ['secret-modal-profile', 'pppoe-server-profile', 'pptp-server-profile', 'l2tp-server-profile'];
+    selects.forEach(sid => {
+      const el = document.getElementById(sid);
+      if (!el) return;
+      const cur = el.value;
+      el.innerHTML = '';
+      state.pppProfiles.forEach(p => {
+        const opt = document.createElement('option');
+        opt.value = p.name;
+        opt.innerText = `${p.name} (${p.remote_address})`;
+        if (p.name === cur) opt.selected = true;
+        el.appendChild(opt);
+      });
+    });
+
+    renderPPPProfiles();
+  } catch (err) {
+    console.error('Failed to load PPP profiles:', err);
+  }
+}
+
+function renderPPPProfiles() {
+  const tbody = document.getElementById('profiles-table-body');
+  if (!tbody) return;
+  tbody.innerHTML = '';
+
+  (state.pppProfiles || []).forEach(p => {
+    const isDefault = p.name.startsWith('default');
+    const tr = document.createElement('tr');
+    tr.className = 'hover:bg-slate-800/40 transition text-xs font-mono';
+    tr.innerHTML = `
+      <td class="py-2.5 px-3 font-bold text-white">${p.name}</td>
+      <td class="py-2.5 px-3 text-slate-300">${p.local_address}</td>
+      <td class="py-2.5 px-3 text-cyan-400 font-bold">${p.remote_address}</td>
+      <td class="py-2.5 px-3 text-slate-400">${p.dns_servers || '-'}</td>
+      <td class="py-2.5 px-3 text-purple-300 font-semibold">${p.use_encryption}</td>
+      <td class="py-2.5 px-3 text-center">${p.only_one ? '<span class="text-emerald-400">yes</span>' : '<span class="text-slate-500">no</span>'}</td>
+      <td class="py-2.5 px-3 text-slate-400 italic">${p.comment || '-'}</td>
+      <td class="py-2.5 px-3 text-right font-sans whitespace-nowrap">
+        ${!isDefault ? `<button onclick="deletePPPProfile('${p.name}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>` : `<span class="text-slate-600 text-[10px]">Built-in</span>`}
+      </td>
+    `;
+    tbody.appendChild(tr);
+  });
+}
+
+async function loadPPPServers() {
+  try {
+    const [pppoeRes, pptpRes, l2tpRes, ipsecRes] = await Promise.all([
+      fetch(`${API_BASE}/vpn/servers/pppoe`),
+      fetch(`${API_BASE}/vpn/servers/pptp`),
+      fetch(`${API_BASE}/vpn/servers/l2tp`),
+      fetch(`${API_BASE}/vpn/servers/ipsec`)
+    ]);
+
+    if (pppoeRes.ok) {
+      state.pppoeServer = await pppoeRes.json();
+      const s = state.pppoeServer;
+      document.getElementById('pppoe-server-enabled').checked = !!s.enabled;
+      document.getElementById('pppoe-server-name').value = s.service_name || 'proxmox-pppoe';
+      populateInterfaceSelect('pppoe-server-iface', s.interface || 'eth0');
+      document.getElementById('pppoe-server-profile').value = s.default_profile || 'default';
+      document.getElementById('pppoe-server-mtu').value = s.max_mtu || 1480;
+      document.getElementById('pppoe-server-onesession').checked = !!s.one_session_per_host;
+      const pill = document.getElementById('pppoe-server-status-pill');
+      if (pill) {
+        pill.innerText = s.running ? `ACTIVE (${s.active_sessions} sess)` : 'STOPPED';
+        pill.className = s.running ? 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-blue-500/20 text-blue-300 border border-blue-500/40' : 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-slate-800 text-slate-400';
+      }
+      setDotBadge('ppp-badge-pppoe-state', s.running);
+    }
+
+    if (pptpRes.ok) {
+      state.pptpServer = await pptpRes.json();
+      const s = state.pptpServer;
+      document.getElementById('pptp-server-enabled').checked = !!s.enabled;
+      document.getElementById('pptp-server-port').value = s.port || 1723;
+      document.getElementById('pptp-server-profile').value = s.default_profile || 'default-encryption';
+      document.getElementById('pptp-server-mppe').checked = s.require_mppe !== false;
+      const pill = document.getElementById('pptp-server-status-pill');
+      if (pill) {
+        pill.innerText = s.running ? `ACTIVE (${s.active_sessions} sess)` : 'STOPPED';
+        pill.className = s.running ? 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-purple-500/20 text-purple-300 border border-purple-500/40' : 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-slate-800 text-slate-400';
+      }
+      setDotBadge('ppp-badge-pptp-state', s.running);
+    }
+
+    if (l2tpRes.ok) {
+      state.l2tpServer = await l2tpRes.json();
+      const s = state.l2tpServer;
+      document.getElementById('l2tp-server-enabled').checked = !!s.enabled;
+      document.getElementById('l2tp-server-port').value = s.port || 1701;
+      document.getElementById('l2tp-server-profile').value = s.default_profile || 'default';
+      document.getElementById('l2tp-server-useipsec').value = s.use_ipsec || 'no';
+      document.getElementById('l2tp-server-ipsecsecret').value = s.ipsec_secret || '';
+      const pill = document.getElementById('l2tp-server-status-pill');
+      if (pill) {
+        pill.innerText = s.running ? `ACTIVE (${s.active_sessions} sess)` : 'STOPPED';
+        pill.className = s.running ? 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-cyan-500/20 text-cyan-300 border border-cyan-500/40' : 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-slate-800 text-slate-400';
+      }
+      setDotBadge('ppp-badge-l2tp-state', s.running);
+    }
+
+    if (ipsecRes.ok) {
+      state.ipsecServer = await ipsecRes.json();
+      const s = state.ipsecServer;
+      const modalEn = document.getElementById('modal-ipsec-server-enabled');
+      if (modalEn) modalEn.checked = !!s.enabled;
+      const modalPsk = document.getElementById('modal-ipsec-server-psk');
+      if (modalPsk) modalPsk.value = s.preshared_key || '';
+      const modalPool = document.getElementById('modal-ipsec-server-pool');
+      if (modalPool) modalPool.value = s.pool_address || '10.10.30.0/24';
+      const modalDns = document.getElementById('modal-ipsec-server-dns');
+      if (modalDns) modalDns.value = s.dns_servers || '1.1.1.1,8.8.8.8';
+      const modalSubnet = document.getElementById('modal-ipsec-server-subnet');
+      if (modalSubnet) modalSubnet.value = s.local_subnet || '0.0.0.0/0';
+      const pill = document.getElementById('modal-ipsec-server-status');
+      if (pill) {
+        pill.innerText = s.running ? 'ACTIVE RESPONDER' : 'STOPPED';
+        pill.className = s.running ? 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-amber-500/20 text-amber-300 border border-amber-500/40' : 'px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-slate-800 text-slate-400';
+      }
+    }
+  } catch (err) {
+    console.error('Failed to load VPN servers:', err);
+  }
+}
+
+async function loadPPPSessions() {
+  try {
+    const res = await fetch(`${API_BASE}/vpn/ppp/active`);
+    const data = await res.json();
+    state.pppActiveSessions = Array.isArray(data) ? data : [];
+    const b = document.getElementById('ppp-badge-active');
+    if (b) b.innerText = state.pppActiveSessions.length;
+    const sideB = document.getElementById('badge-ppp-sessions');
+    if (sideB) sideB.innerText = state.pppActiveSessions.length;
+    renderPPPSessions();
+  } catch (err) {
+    console.error('Failed to load active PPP sessions:', err);
+  }
+}
+
+function renderPPPSessions() {
+  const tbody = document.getElementById('active-ppp-table-body');
+  const empty = document.getElementById('active-ppp-empty');
+  if (!tbody) return;
+  tbody.innerHTML = '';
+
+  const list = state.pppActiveSessions || [];
+  if (list.length === 0) {
+    empty?.classList.remove('hidden');
+    return;
+  }
+  empty?.classList.add('hidden');
+
+  list.forEach(s => {
+    const tr = document.createElement('tr');
+    tr.className = 'hover:bg-slate-800/40 transition text-xs font-mono';
+    tr.innerHTML = `
+      <td class="py-2.5 px-3 font-bold text-white">${s.interface}</td>
+      <td class="py-2.5 px-3 font-semibold text-violet-300">${s.username}</td>
+      <td class="py-2.5 px-3 text-cyan-400 uppercase">${s.service}</td>
+      <td class="py-2.5 px-3 text-emerald-400 font-bold">${s.address || '-'}</td>
+      <td class="py-2.5 px-3 text-slate-400">${s.uptime}</td>
+      <td class="py-2.5 px-3 text-[11px]">
+        <span class="text-emerald-400">↓ ${s.rx_human || '0 B'}</span>
+        <span class="text-cyan-400 ml-1">↑ ${s.tx_human || '0 B'}</span>
+      </td>
+      <td class="py-2.5 px-3 text-right font-sans">
+        <button onclick="disconnectPPPSession('${s.interface}')" class="text-xs text-rose-400 hover:text-rose-300">Disconnect</button>
+      </td>
+    `;
+    tbody.appendChild(tr);
+  });
+}
+
+async function disconnectPPPSession(iface) {
+  if (!confirm(`Disconnect session ${iface}?`)) return;
+  try {
+    const res = await fetch(`${API_BASE}/vpn/ppp/active/${encodeURIComponent(iface)}`, { method: 'DELETE' });
+    if (res.ok) {
+      showToast('Disconnected', `Session ${iface} terminated`, 'info');
+      loadPPPSessions();
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+// PPP Secret Modal handlers
+function openAddPPPSecretModal() {
+  document.getElementById('secret-modal-title').innerText = 'Add PPP Secret';
+  document.getElementById('secret-modal-id').value = '';
+  document.getElementById('secret-modal-name').value = '';
+  document.getElementById('secret-modal-password').value = '';
+  document.getElementById('secret-modal-service').value = 'any';
+  document.getElementById('secret-modal-profile').value = 'default';
+  document.getElementById('secret-modal-local').value = '';
+  document.getElementById('secret-modal-remote').value = '';
+  document.getElementById('secret-modal-comment').value = '';
+  document.getElementById('secret-modal-disabled').checked = false;
+  document.getElementById('ppp-secret-modal').classList.remove('hidden');
+}
+
+function openEditPPPSecretModal(id) {
+  const s = (state.pppSecrets || []).find(sec => sec.id === id);
+  if (!s) return;
+  document.getElementById('secret-modal-title').innerText = `Edit PPP Secret (${s.name})`;
+  document.getElementById('secret-modal-id').value = s.id;
+  document.getElementById('secret-modal-name').value = s.name;
+  document.getElementById('secret-modal-password').value = s.password;
+  document.getElementById('secret-modal-service').value = s.service || 'any';
+  document.getElementById('secret-modal-profile').value = s.profile || 'default';
+  document.getElementById('secret-modal-local').value = s.local_address || '';
+  document.getElementById('secret-modal-remote').value = s.remote_address || '';
+  document.getElementById('secret-modal-comment').value = s.comment || '';
+  document.getElementById('secret-modal-disabled').checked = !!s.disabled;
+  document.getElementById('ppp-secret-modal').classList.remove('hidden');
+}
+
+function closePPPSecretModal() {
+  document.getElementById('ppp-secret-modal').classList.add('hidden');
+}
+
+async function submitSavePPPSecret(e) {
+  e.preventDefault();
+  const id = document.getElementById('secret-modal-id').value;
+  const name = document.getElementById('secret-modal-name').value.trim();
+  const password = document.getElementById('secret-modal-password').value.trim();
+  const service = document.getElementById('secret-modal-service').value;
+  const profile = document.getElementById('secret-modal-profile').value;
+  const local = document.getElementById('secret-modal-local').value.trim();
+  const remote = document.getElementById('secret-modal-remote').value.trim();
+  const comment = document.getElementById('secret-modal-comment').value.trim();
+  const disabled = document.getElementById('secret-modal-disabled').checked;
+
+  try {
+    const res = await fetch(`${API_BASE}/vpn/ppp/secrets`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        id,
+        name,
+        password,
+        service,
+        profile,
+        local_address: local,
+        remote_address: remote,
+        comment,
+        disabled
+      })
+    });
+    if (res.ok) {
+      showToast('Secret Saved', `User ${name} saved to PPP database`, 'success');
+      closePPPSecretModal();
+      loadPPPSecrets();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.message, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function togglePPPSecretState(id, disabled) {
+  try {
+    const res = await fetch(`${API_BASE}/vpn/ppp/secrets/${encodeURIComponent(id)}/state`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ disabled })
+    });
+    if (res.ok) {
+      showToast(disabled ? 'Disabled' : 'Enabled', 'Secret user state updated', 'info');
+      loadPPPSecrets();
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function deletePPPSecret(id) {
+  if (!confirm('Are you sure you want to delete this secret account?')) return;
+  try {
+    const res = await fetch(`${API_BASE}/vpn/ppp/secrets/${encodeURIComponent(id)}`, { method: 'DELETE' });
+    if (res.ok) {
+      showToast('Deleted', 'Secret account removed', 'success');
+      loadPPPSecrets();
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+// PPP Profile Modal handlers
+function openAddPPPProfileModal() {
+  document.getElementById('profile-modal-name').value = '';
+  document.getElementById('profile-modal-local').value = '10.10.10.1';
+  document.getElementById('profile-modal-remote').value = '10.10.10.10-10.10.10.50';
+  document.getElementById('profile-modal-dns').value = '1.1.1.1,8.8.8.8';
+  document.getElementById('profile-modal-encryption').value = 'required';
+  document.getElementById('profile-modal-onlyone').checked = true;
+  document.getElementById('profile-modal-comment').value = '';
+  document.getElementById('ppp-profile-modal').classList.remove('hidden');
+}
+
+function closePPPProfileModal() {
+  document.getElementById('ppp-profile-modal').classList.add('hidden');
+}
+
+async function submitSavePPPProfile(e) {
+  e.preventDefault();
+  const name = document.getElementById('profile-modal-name').value.trim();
+  const local = document.getElementById('profile-modal-local').value.trim();
+  const remote = document.getElementById('profile-modal-remote').value.trim();
+  const dns = document.getElementById('profile-modal-dns').value.trim();
+  const enc = document.getElementById('profile-modal-encryption').value;
+  const onlyone = document.getElementById('profile-modal-onlyone').checked;
+  const comment = document.getElementById('profile-modal-comment').value.trim();
+
+  try {
+    const res = await fetch(`${API_BASE}/vpn/ppp/profiles`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        id: name,
+        name,
+        local_address: local,
+        remote_address: remote,
+        dns_servers: dns,
+        use_encryption: enc,
+        only_one: onlyone,
+        comment
+      })
+    });
+    if (res.ok) {
+      showToast('Profile Saved', `PPP profile ${name} created`, 'success');
+      closePPPProfileModal();
+      loadPPPProfiles();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.message, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function deletePPPProfile(name) {
+  if (!confirm(`Delete PPP profile ${name}?`)) return;
+  try {
+    const res = await fetch(`${API_BASE}/vpn/ppp/profiles/${encodeURIComponent(name)}`, { method: 'DELETE' });
+    if (res.ok) {
+      showToast('Deleted', 'Profile removed', 'success');
+      loadPPPProfiles();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.message, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+// Server Config submissions
+async function savePPPoEServerConfig(e) {
+  e.preventDefault();
+  const enabled = document.getElementById('pppoe-server-enabled').checked;
+  const name = document.getElementById('pppoe-server-name').value.trim();
+  const iface = document.getElementById('pppoe-server-iface').value;
+  const profile = document.getElementById('pppoe-server-profile').value;
+  const mtu = parseInt(document.getElementById('pppoe-server-mtu').value) || 1480;
+  const onesession = document.getElementById('pppoe-server-onesession').checked;
+
+  try {
+    const res = await fetch(`${API_BASE}/vpn/servers/pppoe`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        enabled,
+        service_name: name,
+        interface: iface,
+        default_profile: profile,
+        max_mtu: mtu,
+        max_mru: mtu,
+        one_session_per_host: onesession
+      })
+    });
+    if (res.ok) {
+      showToast('PPPoE Server', enabled ? 'PPPoE Server started' : 'PPPoE Server stopped', 'success');
+      loadPPPServers();
+      fetchData(false);
+    } else {
+      const err = await res.json();
+      showToast('Error', err.message, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function savePPTPServerConfig(e) {
+  e.preventDefault();
+  const enabled = document.getElementById('pptp-server-enabled').checked;
+  const port = parseInt(document.getElementById('pptp-server-port').value) || 1723;
+  const profile = document.getElementById('pptp-server-profile').value;
+  const mppe = document.getElementById('pptp-server-mppe').checked;
+
+  try {
+    const res = await fetch(`${API_BASE}/vpn/servers/pptp`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        enabled,
+        port,
+        default_profile: profile,
+        require_mppe: mppe
+      })
+    });
+    if (res.ok) {
+      showToast('PPTP Server', enabled ? 'PPTP Server started' : 'PPTP Server stopped', 'success');
+      loadPPPServers();
+      fetchData(false);
+    } else {
+      const err = await res.json();
+      showToast('Error', err.message, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function saveL2TPServerConfig(e) {
+  e.preventDefault();
+  const enabled = document.getElementById('l2tp-server-enabled').checked;
+  const port = parseInt(document.getElementById('l2tp-server-port').value) || 1701;
+  const profile = document.getElementById('l2tp-server-profile').value;
+  const useipsec = document.getElementById('l2tp-server-useipsec').value;
+  const psk = document.getElementById('l2tp-server-ipsecsecret').value.trim();
+
+  try {
+    const res = await fetch(`${API_BASE}/vpn/servers/l2tp`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        enabled,
+        port,
+        default_profile: profile,
+        use_ipsec: useipsec,
+        ipsec_secret: psk
+      })
+    });
+    if (res.ok) {
+      showToast('L2TP Server', enabled ? 'L2TP Server started' : 'L2TP Server stopped', 'success');
+      loadPPPServers();
+      fetchData(false);
+    } else {
+      const err = await res.json();
+      showToast('Error', err.message, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+// IPSec Server Modal
+function openIPSecServerModal() {
+  loadPPPServers();
+  document.getElementById('ipsec-server-modal').classList.remove('hidden');
+}
+
+function closeIPSecServerModal() {
+  document.getElementById('ipsec-server-modal').classList.add('hidden');
+}
+
+async function submitSaveIPSecServer(e) {
+  e.preventDefault();
+  const enabled = document.getElementById('modal-ipsec-server-enabled').checked;
+  const psk = document.getElementById('modal-ipsec-server-psk').value.trim();
+  const pool = document.getElementById('modal-ipsec-server-pool').value.trim();
+  const dns = document.getElementById('modal-ipsec-server-dns').value.trim();
+  const subnet = document.getElementById('modal-ipsec-server-subnet').value.trim();
+
+  try {
+    const res = await fetch(`${API_BASE}/vpn/servers/ipsec`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        enabled,
+        preshared_key: psk,
+        pool_address: pool,
+        dns_servers: dns,
+        local_subnet: subnet
+      })
+    });
+    if (res.ok) {
+      showToast('IPSec Server', enabled ? 'IKEv2 Roadwarrior responder active' : 'IPSec Server stopped', 'success');
+      closeIPSecServerModal();
+      loadPPPServers();
+      fetchData(false);
+    } else {
+      const err = await res.json();
+      showToast('Error', err.message, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+// WireGuard Client Config Viewer
+let currentWGClientConf = '';
+
+async function openWGClientConfModal(iface, clientIP) {
+  try {
+    const res = await fetch(`${API_BASE}/vpn/wireguard/client-config`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        interface: iface,
+        address: clientIP || '10.10.0.2/32',
+        dns: '1.1.1.1, 8.8.8.8'
+      })
+    });
+    if (res.ok) {
+      const data = await res.json();
+      currentWGClientConf = data.config;
+      document.getElementById('wg-client-conf-text').value = currentWGClientConf;
+      document.getElementById('wg-client-conf-modal').classList.remove('hidden');
+    } else {
+      const err = await res.json();
+      showToast('Error', err.message, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+function closeWGClientConfModal() {
+  document.getElementById('wg-client-conf-modal').classList.add('hidden');
+}
+
+function copyWGClientConf() {
+  if (!currentWGClientConf) return;
+  copyToClipboard(currentWGClientConf, 'WireGuard Client Profile');
+}
+
+function downloadWGClientConf() {
+  if (!currentWGClientConf) return;
+  const blob = new Blob([currentWGClientConf], { type: 'text/plain;charset=utf-8' });
+  const a = document.createElement('a');
+  a.href = URL.createObjectURL(blob);
+  a.download = 'wireguard-client.conf';
+  document.body.appendChild(a);
+  a.click();
+  document.body.removeChild(a);
+  showToast('Downloaded', 'wireguard-client.conf saved', 'success');
+}

+ 638 - 13
internal/web/static/index.html

@@ -196,6 +196,28 @@
           <span id="badge-pptp-status" class="w-2 h-2 rounded-full bg-slate-600"></span>
         </a>
 
+        <a href="#ppp" onclick="navigate('ppp')" id="nav-ppp"
+           class="nav-link flex items-center justify-between px-3 py-2 rounded-lg text-xs font-medium transition-all group text-slate-300 hover:bg-slate-800/80 hover:text-white">
+          <div class="flex items-center space-x-2.5">
+            <svg class="w-4 h-4 text-violet-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
+            </svg>
+            <span>PPP (Servers & Users)</span>
+          </div>
+          <span id="badge-ppp-sessions" class="text-[10px] bg-violet-950 text-violet-300 border border-violet-800/40 px-1.5 py-0.2 rounded-full font-mono">0</span>
+        </a>
+
+        <a href="#eoip" onclick="navigate('eoip')" id="nav-eoip"
+           class="nav-link flex items-center justify-between px-3 py-2 rounded-lg text-xs font-medium transition-all group text-slate-300 hover:bg-slate-800/80 hover:text-white">
+          <div class="flex items-center space-x-2.5">
+            <svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
+            </svg>
+            <span>EoIP Tunnels</span>
+          </div>
+          <span id="badge-eoip-count" class="text-[10px] bg-slate-800 text-slate-300 px-1.5 py-0.2 rounded-full font-mono">0</span>
+        </a>
+
         <!-- CATEGORY: SYSTEM -->
         <div class="px-3 pt-3 pb-1 text-[10px] font-bold text-slate-500 uppercase tracking-wider">System</div>
 
@@ -525,9 +547,14 @@
               </div>
               <p class="text-xs text-slate-400">Manage multiple Point-to-Point Protocol over Ethernet WAN connections (MikroTik RouterOS style).</p>
             </div>
-            <button onclick="openAddPPPoEModal()" class="text-xs font-semibold px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-500 text-white shadow-lg shadow-blue-500/20 transition flex items-center gap-1.5">
-              <span>+ Add PPPoE Client</span>
-            </button>
+            <div class="flex items-center gap-2">
+              <button onclick="navigate('ppp'); switchPPPSubTab('pppoe')" class="text-xs font-semibold px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-blue-300 border border-blue-900/40 transition flex items-center gap-1.5" title="Configure Router as PPPoE Access Concentrator Server">
+                <span>⚙ Server Settings</span>
+              </button>
+              <button onclick="openAddPPPoEModal()" class="text-xs font-semibold px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-500 text-white shadow-lg shadow-blue-500/20 transition flex items-center gap-1.5">
+                <span>+ Add PPPoE Client</span>
+              </button>
+            </div>
           </div>
 
           <div class="bg-slate-900/90 border border-slate-800 rounded-xl overflow-hidden">
@@ -673,9 +700,14 @@
               </div>
               <p class="text-xs text-slate-400">Enterprise strongSwan 6 Site-to-Site and Remote Gateway connections.</p>
             </div>
-            <button onclick="openAddIPSecModal()" class="text-xs font-semibold px-4 py-2 rounded-lg bg-amber-600 hover:bg-amber-500 text-white shadow-lg shadow-amber-500/20 transition">
-              + Add IPSec Tunnel
-            </button>
+            <div class="flex items-center gap-2">
+              <button onclick="openIPSecServerModal()" class="text-xs font-semibold px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-amber-300 border border-amber-900/40 transition flex items-center gap-1.5" title="Configure Router as IPSec IKEv2 Roadwarrior Mobile Client Server">
+                <span>⚙ Roadwarrior Server</span>
+              </button>
+              <button onclick="openAddIPSecModal()" class="text-xs font-semibold px-4 py-2 rounded-lg bg-amber-600 hover:bg-amber-500 text-white shadow-lg shadow-amber-500/20 transition">
+                + Add IPSec Tunnel
+              </button>
+            </div>
           </div>
 
           <div class="bg-slate-900/90 border border-slate-800 rounded-xl overflow-hidden">
@@ -717,9 +749,14 @@
               </div>
               <p class="text-xs text-slate-400">Multiple Layer 2 Tunneling Protocol clients with optional IPSec PSK encryption.</p>
             </div>
-            <button onclick="openAddL2TPModal()" class="text-xs font-semibold px-4 py-2 rounded-lg bg-cyan-600 hover:bg-cyan-500 text-white shadow-lg shadow-cyan-500/20 transition">
-              + Add L2TP Client
-            </button>
+            <div class="flex items-center gap-2">
+              <button onclick="navigate('ppp'); switchPPPSubTab('l2tp')" class="text-xs font-semibold px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-cyan-300 border border-cyan-900/40 transition flex items-center gap-1.5" title="Configure Router as L2TP Server">
+                <span>⚙ Server Settings</span>
+              </button>
+              <button onclick="openAddL2TPModal()" class="text-xs font-semibold px-4 py-2 rounded-lg bg-cyan-600 hover:bg-cyan-500 text-white shadow-lg shadow-cyan-500/20 transition">
+                + Add L2TP Client
+              </button>
+            </div>
           </div>
 
           <div class="bg-slate-900/90 border border-slate-800 rounded-xl overflow-hidden">
@@ -762,9 +799,14 @@
               </div>
               <p class="text-xs text-slate-400">Multiple Point-to-Point Tunneling Protocol clients with MPPE-128 encryption.</p>
             </div>
-            <button onclick="openAddPPTPModal()" class="text-xs font-semibold px-4 py-2 rounded-lg bg-purple-600 hover:bg-purple-500 text-white shadow-lg shadow-purple-500/20 transition">
-              + Add PPTP Client
-            </button>
+            <div class="flex items-center gap-2">
+              <button onclick="navigate('ppp'); switchPPPSubTab('pptp')" class="text-xs font-semibold px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-purple-300 border border-purple-900/40 transition flex items-center gap-1.5" title="Configure Router as PPTP Server">
+                <span>⚙ Server Settings</span>
+              </button>
+              <button onclick="openAddPPTPModal()" class="text-xs font-semibold px-4 py-2 rounded-lg bg-purple-600 hover:bg-purple-500 text-white shadow-lg shadow-purple-500/20 transition">
+                + Add PPTP Client
+              </button>
+            </div>
           </div>
 
           <div class="bg-slate-900/90 border border-slate-800 rounded-xl overflow-hidden">
@@ -797,6 +839,310 @@
           </div>
         </section>
 
+        <!-- ================= SECTION: PPP SERVERS & SECRETS (/ppp) ================= -->
+        <section id="section-ppp" class="hidden space-y-6">
+          <div class="bg-gradient-to-r from-violet-950/40 via-slate-900 to-slate-900 border border-violet-900/30 rounded-xl p-5 flex flex-wrap items-center justify-between gap-4">
+            <div>
+              <div class="flex items-center gap-2 mb-1">
+                <span class="px-2 py-0.5 rounded text-[10px] font-mono bg-violet-500/20 text-violet-300 border border-violet-500/30 font-semibold">/ppp</span>
+                <h2 class="text-base font-bold text-white">PPP Servers, Secrets & Profiles</h2>
+              </div>
+              <p class="text-xs text-slate-400">MikroTik RouterOS centralized user authentication database, IP pools and VPN server concentrators.</p>
+            </div>
+
+            <div class="flex items-center space-x-2.5">
+              <button id="ppp-primary-add-btn" onclick="openCurrentPPPModal()" class="text-xs font-semibold px-4 py-2 rounded-lg bg-violet-600 hover:bg-violet-500 text-white shadow-lg shadow-violet-500/20 transition flex items-center gap-1.5">
+                <span>+ Add User (Secret)</span>
+              </button>
+            </div>
+          </div>
+
+          <!-- Sub Tabs: Secrets / Profiles / PPPoE Server / PPTP Server / L2TP Server / Active Sessions -->
+          <div class="flex border-b border-slate-800 space-x-1 overflow-x-auto">
+            <button id="ppp-subtab-btn-secrets" onclick="switchPPPSubTab('secrets')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-violet-500 text-white transition flex items-center gap-2 whitespace-nowrap">
+              <span>Secrets (Users)</span>
+              <span id="ppp-badge-secrets" class="px-1.5 py-0.2 rounded text-[10px] bg-slate-800 text-slate-300">0</span>
+            </button>
+            <button id="ppp-subtab-btn-profiles" onclick="switchPPPSubTab('profiles')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
+              <span>Profiles (Pools)</span>
+              <span id="ppp-badge-profiles" class="px-1.5 py-0.2 rounded text-[10px] bg-slate-800 text-slate-300">0</span>
+            </button>
+            <button id="ppp-subtab-btn-pppoe" onclick="switchPPPSubTab('pppoe')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
+              <span>PPPoE Server</span>
+              <span id="ppp-badge-pppoe-state" class="w-2 h-2 rounded-full bg-slate-600"></span>
+            </button>
+            <button id="ppp-subtab-btn-pptp" onclick="switchPPPSubTab('pptp')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
+              <span>PPTP Server</span>
+              <span id="ppp-badge-pptp-state" class="w-2 h-2 rounded-full bg-slate-600"></span>
+            </button>
+            <button id="ppp-subtab-btn-l2tp" onclick="switchPPPSubTab('l2tp')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
+              <span>L2TP Server</span>
+              <span id="ppp-badge-l2tp-state" class="w-2 h-2 rounded-full bg-slate-600"></span>
+            </button>
+            <button id="ppp-subtab-btn-active" onclick="switchPPPSubTab('active')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
+              <span>Active Sessions</span>
+              <span id="ppp-badge-active" class="px-1.5 py-0.2 rounded text-[10px] bg-emerald-950 text-emerald-400 border border-emerald-800/40">0</span>
+            </button>
+          </div>
+
+          <!-- TAB 1: SECRETS (USERS) -->
+          <div id="ppp-subtab-secrets" class="bg-slate-900/90 border border-slate-800 rounded-xl overflow-hidden">
+            <div class="p-4 border-b border-slate-800 flex items-center justify-between">
+              <h3 class="text-sm font-bold text-white">PPP Secrets (/ppp secret)</h3>
+              <span id="secrets-count-badge" class="text-xs font-mono text-slate-400">0 Users</span>
+            </div>
+            <div class="overflow-x-auto">
+              <table class="w-full text-left text-xs font-mono">
+                <thead class="text-slate-400 bg-slate-950/60 border-b border-slate-800 uppercase tracking-wider text-[10px] font-sans">
+                  <tr>
+                    <th class="py-2.5 px-3 w-12 text-center">Flag</th>
+                    <th class="py-2.5 px-3">Name (Username)</th>
+                    <th class="py-2.5 px-3">Service</th>
+                    <th class="py-2.5 px-3">Profile</th>
+                    <th class="py-2.5 px-3">Local Address</th>
+                    <th class="py-2.5 px-3">Remote Address</th>
+                    <th class="py-2.5 px-3">Comment</th>
+                    <th class="py-2.5 px-3 text-right font-sans">Actions</th>
+                  </tr>
+                </thead>
+                <tbody id="secrets-table-body" class="divide-y divide-slate-800/80"></tbody>
+              </table>
+            </div>
+            <div id="secrets-empty" class="hidden p-10 text-center text-slate-500 text-xs font-sans">
+              No PPP secrets configured. Click "+ Add User" to define an account.
+            </div>
+          </div>
+
+          <!-- TAB 2: PROFILES (POOLS) -->
+          <div id="ppp-subtab-profiles" class="hidden bg-slate-900/90 border border-slate-800 rounded-xl overflow-hidden">
+            <div class="p-4 border-b border-slate-800 flex items-center justify-between">
+              <h3 class="text-sm font-bold text-white">PPP Profiles (/ppp profile)</h3>
+              <span id="profiles-count-badge" class="text-xs font-mono text-slate-400">0 Profiles</span>
+            </div>
+            <div class="overflow-x-auto">
+              <table class="w-full text-left text-xs font-mono">
+                <thead class="text-slate-400 bg-slate-950/60 border-b border-slate-800 uppercase tracking-wider text-[10px] font-sans">
+                  <tr>
+                    <th class="py-2.5 px-3">Profile Name</th>
+                    <th class="py-2.5 px-3">Local Address</th>
+                    <th class="py-2.5 px-3">Remote Address (Pool)</th>
+                    <th class="py-2.5 px-3">DNS Servers</th>
+                    <th class="py-2.5 px-3">Encryption</th>
+                    <th class="py-2.5 px-3 text-center">Only One</th>
+                    <th class="py-2.5 px-3">Comment</th>
+                    <th class="py-2.5 px-3 text-right font-sans">Actions</th>
+                  </tr>
+                </thead>
+                <tbody id="profiles-table-body" class="divide-y divide-slate-800/80"></tbody>
+              </table>
+            </div>
+          </div>
+
+          <!-- TAB 3: PPPoE SERVER -->
+          <div id="ppp-subtab-pppoe" class="hidden bg-slate-900/90 border border-slate-800 rounded-xl p-6">
+            <form onsubmit="savePPPoEServerConfig(event)" class="max-w-2xl space-y-5">
+              <div class="flex items-center justify-between border-b border-slate-800 pb-4">
+                <div>
+                  <h3 class="text-sm font-bold text-white">PPPoE Access Concentrator Server</h3>
+                  <p class="text-xs text-slate-400">Listens on physical interface or VLAN for PPPoE Discovery (PADI) packets</p>
+                </div>
+                <div class="flex items-center gap-3">
+                  <span id="pppoe-server-status-pill" class="px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-slate-800 text-slate-400">STOPPED</span>
+                  <label class="relative inline-flex items-center cursor-pointer">
+                    <input type="checkbox" id="pppoe-server-enabled" class="sr-only peer">
+                    <div class="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
+                  </label>
+                </div>
+              </div>
+              <div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs font-sans">
+                <div>
+                  <label class="block text-slate-300 font-medium mb-1">Service Name</label>
+                  <input type="text" id="pppoe-server-name" placeholder="proxmox-pppoe" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+                </div>
+                <div>
+                  <label class="block text-slate-300 font-medium mb-1">Listen Interface</label>
+                  <select id="pppoe-server-iface" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono"></select>
+                </div>
+                <div>
+                  <label class="block text-slate-300 font-medium mb-1">Default Profile</label>
+                  <select id="pppoe-server-profile" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono"></select>
+                </div>
+                <div>
+                  <label class="block text-slate-300 font-medium mb-1">Max MTU / MRU</label>
+                  <input type="number" id="pppoe-server-mtu" value="1480" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+                </div>
+              </div>
+              <div class="flex items-center space-x-2 pt-2">
+                <input type="checkbox" id="pppoe-server-onesession" class="rounded bg-slate-800 border-slate-700 text-blue-600">
+                <label for="pppoe-server-onesession" class="text-xs text-slate-300">One Session Per Host (limit 1 PPPoE session per client MAC)</label>
+              </div>
+              <div class="pt-4 border-t border-slate-800 flex justify-end">
+                <button type="submit" class="px-5 py-2 rounded-lg text-xs font-semibold bg-blue-600 hover:bg-blue-500 text-white shadow-lg shadow-blue-500/20 transition">
+                  Apply PPPoE Server Settings
+                </button>
+              </div>
+            </form>
+          </div>
+
+          <!-- TAB 4: PPTP SERVER -->
+          <div id="ppp-subtab-pptp" class="hidden bg-slate-900/90 border border-slate-800 rounded-xl p-6">
+            <form onsubmit="savePPTPServerConfig(event)" class="max-w-2xl space-y-5">
+              <div class="flex items-center justify-between border-b border-slate-800 pb-4">
+                <div>
+                  <h3 class="text-sm font-bold text-white">PPTP Server (/interface pptp-server server)</h3>
+                  <p class="text-xs text-slate-400">Point-to-Point Tunneling Server listening on TCP port 1723 with MPPE-128 encryption</p>
+                </div>
+                <div class="flex items-center gap-3">
+                  <span id="pptp-server-status-pill" class="px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-slate-800 text-slate-400">STOPPED</span>
+                  <label class="relative inline-flex items-center cursor-pointer">
+                    <input type="checkbox" id="pptp-server-enabled" class="sr-only peer">
+                    <div class="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
+                  </label>
+                </div>
+              </div>
+              <div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs font-sans">
+                <div>
+                  <label class="block text-slate-300 font-medium mb-1">Port</label>
+                  <input type="number" id="pptp-server-port" value="1723" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+                </div>
+                <div>
+                  <label class="block text-slate-300 font-medium mb-1">Default Profile</label>
+                  <select id="pptp-server-profile" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono"></select>
+                </div>
+              </div>
+              <div class="flex items-center space-x-2 pt-2">
+                <input type="checkbox" id="pptp-server-mppe" checked class="rounded bg-slate-800 border-slate-700 text-purple-600">
+                <label for="pptp-server-mppe" class="text-xs text-slate-300">Require 128-bit MPPE Encryption (MS-CHAPv2)</label>
+              </div>
+              <div class="pt-4 border-t border-slate-800 flex justify-end">
+                <button type="submit" class="px-5 py-2 rounded-lg text-xs font-semibold bg-purple-600 hover:bg-purple-500 text-white shadow-lg shadow-purple-500/20 transition">
+                  Apply PPTP Server Settings
+                </button>
+              </div>
+            </form>
+          </div>
+
+          <!-- TAB 5: L2TP SERVER -->
+          <div id="ppp-subtab-l2tp" class="hidden bg-slate-900/90 border border-slate-800 rounded-xl p-6">
+            <form onsubmit="saveL2TPServerConfig(event)" class="max-w-2xl space-y-5">
+              <div class="flex items-center justify-between border-b border-slate-800 pb-4">
+                <div>
+                  <h3 class="text-sm font-bold text-white">L2TP Server (/interface l2tp-server server)</h3>
+                  <p class="text-xs text-slate-400">Layer 2 Tunneling Protocol Server listening on UDP port 1701 with optional IPSec</p>
+                </div>
+                <div class="flex items-center gap-3">
+                  <span id="l2tp-server-status-pill" class="px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-slate-800 text-slate-400">STOPPED</span>
+                  <label class="relative inline-flex items-center cursor-pointer">
+                    <input type="checkbox" id="l2tp-server-enabled" class="sr-only peer">
+                    <div class="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-cyan-600"></div>
+                  </label>
+                </div>
+              </div>
+              <div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-xs font-sans">
+                <div>
+                  <label class="block text-slate-300 font-medium mb-1">Port</label>
+                  <input type="number" id="l2tp-server-port" value="1701" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+                </div>
+                <div>
+                  <label class="block text-slate-300 font-medium mb-1">Default Profile</label>
+                  <select id="l2tp-server-profile" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono"></select>
+                </div>
+                <div>
+                  <label class="block text-slate-300 font-medium mb-1">Use IPSec</label>
+                  <select id="l2tp-server-useipsec" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+                    <option value="no">no</option>
+                    <option value="yes">yes</option>
+                    <option value="required">required</option>
+                  </select>
+                </div>
+                <div>
+                  <label class="block text-slate-300 font-medium mb-1">IPSec Secret (PSK)</label>
+                  <input type="password" id="l2tp-server-ipsecsecret" placeholder="Optional Pre-Shared Key" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+                </div>
+              </div>
+              <div class="pt-4 border-t border-slate-800 flex justify-end">
+                <button type="submit" class="px-5 py-2 rounded-lg text-xs font-semibold bg-cyan-600 hover:bg-cyan-500 text-white shadow-lg shadow-cyan-500/20 transition">
+                  Apply L2TP Server Settings
+                </button>
+              </div>
+            </form>
+          </div>
+
+          <!-- TAB 6: ACTIVE SESSIONS -->
+          <div id="ppp-subtab-active" class="hidden bg-slate-900/90 border border-slate-800 rounded-xl overflow-hidden">
+            <div class="p-4 border-b border-slate-800 flex items-center justify-between">
+              <h3 class="text-sm font-bold text-white">Active PPP Connections (/ppp active)</h3>
+              <button onclick="loadPPPSessions()" class="text-xs text-slate-400 hover:text-white px-2 py-1 rounded bg-slate-800">Refresh</button>
+            </div>
+            <div class="overflow-x-auto">
+              <table class="w-full text-left text-xs font-mono">
+                <thead class="text-slate-400 bg-slate-950/60 border-b border-slate-800 uppercase tracking-wider text-[10px] font-sans">
+                  <tr>
+                    <th class="py-2.5 px-3">Interface</th>
+                    <th class="py-2.5 px-3">Username</th>
+                    <th class="py-2.5 px-3">Service</th>
+                    <th class="py-2.5 px-3">Client IP Address</th>
+                    <th class="py-2.5 px-3">Uptime</th>
+                    <th class="py-2.5 px-3">RX / TX Traffic</th>
+                    <th class="py-2.5 px-3 text-right font-sans">Actions</th>
+                  </tr>
+                </thead>
+                <tbody id="active-ppp-table-body" class="divide-y divide-slate-800/80"></tbody>
+              </table>
+            </div>
+            <div id="active-ppp-empty" class="hidden p-10 text-center text-slate-500 text-xs font-sans">
+              No active PPP sessions currently connected.
+            </div>
+          </div>
+        </section>
+
+        <!-- ================= SECTION: EoIP TUNNELS (/interface eoip) ================= -->
+        <section id="section-eoip" class="hidden space-y-6">
+          <div class="bg-gradient-to-r from-emerald-950/40 via-slate-900 to-slate-900 border border-emerald-900/30 rounded-xl p-5 flex flex-wrap items-center justify-between gap-4">
+            <div>
+              <div class="flex items-center gap-2 mb-1">
+                <span class="px-2 py-0.5 rounded text-[10px] font-mono bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 font-semibold">/interface eoip</span>
+                <h2 class="text-base font-bold text-white">EoIP Tunnels (Ethernet over IP)</h2>
+              </div>
+              <p class="text-xs text-slate-400">MikroTik RouterOS Layer 2 GRETAP tunneling over IP. Transparently bridge Ethernet subnets across the Internet.</p>
+            </div>
+            <button onclick="openAddEoIPModal()" class="text-xs font-semibold px-4 py-2 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-500/20 transition flex items-center gap-1.5">
+              <span>+ Add EoIP Tunnel</span>
+            </button>
+          </div>
+
+          <div class="bg-slate-900/90 border border-slate-800 rounded-xl overflow-hidden">
+            <div class="p-4 border-b border-slate-800 flex items-center justify-between">
+              <h3 class="text-sm font-bold text-white">Configured EoIP Interfaces</h3>
+              <span id="eoip-count-badge" class="text-xs font-mono bg-slate-800 text-slate-300 px-2.5 py-0.5 rounded-full">0 Tunnels</span>
+            </div>
+
+            <div class="overflow-x-auto">
+              <table class="w-full text-left text-xs font-mono">
+                <thead class="text-slate-400 bg-slate-950/60 border-b border-slate-800 uppercase tracking-wider text-[10px] font-sans">
+                  <tr>
+                    <th class="py-2.5 px-3 w-12 text-center" title="Flags: R=Running, X=Disabled">Flag</th>
+                    <th class="py-2.5 px-3">Name</th>
+                    <th class="py-2.5 px-3">Tunnel ID</th>
+                    <th class="py-2.5 px-3">Remote Address</th>
+                    <th class="py-2.5 px-3">Local Address</th>
+                    <th class="py-2.5 px-3">MAC Address</th>
+                    <th class="py-2.5 px-3">MTU</th>
+                    <th class="py-2.5 px-3">RX / TX Traffic</th>
+                    <th class="py-2.5 px-3">Comment</th>
+                    <th class="py-2.5 px-3 text-right font-sans">Actions</th>
+                  </tr>
+                </thead>
+                <tbody id="eoip-table-body" class="divide-y divide-slate-800/80"></tbody>
+              </table>
+            </div>
+
+            <div id="eoip-empty" class="hidden p-10 text-center text-slate-500 text-xs font-sans">
+              No EoIP tunnels configured. Click "+ Add EoIP Tunnel" to create one.
+            </div>
+          </div>
+        </section>
+
         <!-- ================= SECTION: FIREWALL (/ip firewall) ================= -->
         <section id="section-firewall" class="hidden space-y-6">
           <div class="bg-gradient-to-r from-emerald-950/40 via-slate-900 to-slate-900 border border-emerald-900/30 rounded-xl p-5 flex flex-wrap items-center justify-between gap-4">
@@ -2188,6 +2534,285 @@
     </div>
   </div>
 
-  <script src="app.js?v=2.2"></script>
+  <!-- ================= MODAL: ADD/EDIT EoIP TUNNEL ================= -->
+  <div id="eoip-modal" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
+    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in">
+      <div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/40">
+        <div>
+          <h3 id="eoip-modal-title" class="text-sm font-bold text-white">Add EoIP Tunnel</h3>
+          <p class="text-[11px] text-slate-400">MikroTik RouterOS Layer 2 GRETAP Tunnel</p>
+        </div>
+        <button onclick="closeEoIPModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+      <form onsubmit="submitSaveEoIP(event)" class="p-6 space-y-4 text-xs font-sans">
+        <input type="hidden" id="eoip-modal-id">
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Interface Name *</label>
+            <input type="text" id="eoip-modal-name" placeholder="eoip-tunnel1" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Tunnel ID (1 - 65535) *</label>
+            <input type="number" id="eoip-modal-tunnelid" min="1" max="65535" placeholder="10" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Remote Address (IPv4) *</label>
+            <input type="text" id="eoip-modal-remote" placeholder="198.51.100.2" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Local Address (Optional)</label>
+            <input type="text" id="eoip-modal-local" placeholder="e.g. 192.168.131.56" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">MAC Address (Auto if empty)</label>
+            <input type="text" id="eoip-modal-mac" placeholder="02:64:xx:xx:xx:xx" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">MTU</label>
+            <input type="number" id="eoip-modal-mtu" value="1500" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+          </div>
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Comment</label>
+          <input type="text" id="eoip-modal-comment" placeholder="Bridge to Office Branch" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white">
+        </div>
+
+        <div class="flex items-center justify-between pt-1">
+          <label class="flex items-center space-x-2 text-slate-300">
+            <input type="checkbox" id="eoip-modal-clampmss" checked class="rounded bg-slate-800 border-slate-700 text-emerald-600">
+            <span>Clamp TCP MSS (PMTUD protection)</span>
+          </label>
+          <label class="flex items-center space-x-2 text-slate-300">
+            <input type="checkbox" id="eoip-modal-disabled" class="rounded bg-slate-800 border-slate-700 text-rose-600">
+            <span>Disabled</span>
+          </label>
+        </div>
+
+        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-3">
+          <button type="button" onclick="closeEoIPModal()" class="px-4 py-2 rounded-lg text-slate-400 hover:bg-slate-800">Cancel</button>
+          <button type="submit" class="px-5 py-2 rounded-lg font-semibold bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-500/20">Save Tunnel</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
+  <!-- ================= MODAL: ADD/EDIT PPP SECRET (USER) ================= -->
+  <div id="ppp-secret-modal" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
+    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in">
+      <div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/40">
+        <div>
+          <h3 id="secret-modal-title" class="text-sm font-bold text-white">Add PPP Secret</h3>
+          <p class="text-[11px] text-slate-400">User account for PPPoE, PPTP, and L2TP authentication</p>
+        </div>
+        <button onclick="closePPPSecretModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+      <form onsubmit="submitSavePPPSecret(event)" class="p-6 space-y-4 text-xs font-sans">
+        <input type="hidden" id="secret-modal-id">
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Username (Name) *</label>
+            <input type="text" id="secret-modal-name" placeholder="client_user1" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-violet-500">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Password *</label>
+            <input type="password" id="secret-modal-password" placeholder="••••••••" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-violet-500">
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Service</label>
+            <select id="secret-modal-service" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+              <option value="any">any</option>
+              <option value="pppoe">pppoe</option>
+              <option value="pptp">pptp</option>
+              <option value="l2tp">l2tp</option>
+            </select>
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Profile</label>
+            <select id="secret-modal-profile" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono"></select>
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Local Address (Optional)</label>
+            <input type="text" id="secret-modal-local" placeholder="Override server IP" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Remote Address (Optional Static IP)</label>
+            <input type="text" id="secret-modal-remote" placeholder="Static client IP" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+          </div>
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Comment</label>
+          <input type="text" id="secret-modal-comment" placeholder="Home user / Office user" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white">
+        </div>
+
+        <div class="flex items-center space-x-2 pt-1">
+          <input type="checkbox" id="secret-modal-disabled" class="rounded bg-slate-800 border-slate-700 text-rose-600">
+          <label for="secret-modal-disabled" class="text-slate-300">Disabled</label>
+        </div>
+
+        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-3">
+          <button type="button" onclick="closePPPSecretModal()" class="px-4 py-2 rounded-lg text-slate-400 hover:bg-slate-800">Cancel</button>
+          <button type="submit" class="px-5 py-2 rounded-lg font-semibold bg-violet-600 hover:bg-violet-500 text-white shadow-lg shadow-violet-500/20">Save Secret</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
+  <!-- ================= MODAL: ADD/EDIT PPP PROFILE ================= -->
+  <div id="ppp-profile-modal" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
+    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in">
+      <div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/40">
+        <div>
+          <h3 id="profile-modal-title" class="text-sm font-bold text-white">Add PPP Profile</h3>
+          <p class="text-[11px] text-slate-400">IP pool and encryption settings for PPP servers</p>
+        </div>
+        <button onclick="closePPPProfileModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+      <form onsubmit="submitSavePPPProfile(event)" class="p-6 space-y-4 text-xs font-sans">
+        <input type="hidden" id="profile-modal-id">
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Profile Name *</label>
+          <input type="text" id="profile-modal-name" placeholder="e.g. office-pool" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-violet-500">
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Local Address *</label>
+            <input type="text" id="profile-modal-local" placeholder="10.10.10.1" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Remote Address (Pool) *</label>
+            <input type="text" id="profile-modal-remote" placeholder="10.10.10.10-10.10.10.50" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">DNS Servers</label>
+            <input type="text" id="profile-modal-dns" value="1.1.1.1,8.8.8.8" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Use Encryption</label>
+            <select id="profile-modal-encryption" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+              <option value="no">no</option>
+              <option value="yes">yes</option>
+              <option value="required" selected>required (MPPE-128)</option>
+            </select>
+          </div>
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Comment</label>
+          <input type="text" id="profile-modal-comment" placeholder="Description of this profile" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white">
+        </div>
+
+        <div class="flex items-center space-x-2 pt-1">
+          <input type="checkbox" id="profile-modal-onlyone" checked class="rounded bg-slate-800 border-slate-700 text-violet-600">
+          <label for="profile-modal-onlyone" class="text-slate-300">Only One session per user account</label>
+        </div>
+
+        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-3">
+          <button type="button" onclick="closePPPProfileModal()" class="px-4 py-2 rounded-lg text-slate-400 hover:bg-slate-800">Cancel</button>
+          <button type="submit" class="px-5 py-2 rounded-lg font-semibold bg-violet-600 hover:bg-violet-500 text-white shadow-lg shadow-violet-500/20">Save Profile</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
+  <!-- ================= MODAL: IPSEC ROADWARRIOR SERVER ================= -->
+  <div id="ipsec-server-modal" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
+    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in">
+      <div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/40">
+        <div>
+          <h3 class="text-sm font-bold text-white">IPSec (IKEv2) Roadwarrior Server</h3>
+          <p class="text-[11px] text-slate-400">Mobile VPN server for Windows, macOS, iOS, Android clients</p>
+        </div>
+        <button onclick="closeIPSecServerModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+      <form onsubmit="submitSaveIPSecServer(event)" class="p-6 space-y-4 text-xs font-sans">
+        <div class="flex items-center justify-between border-b border-slate-800 pb-3">
+          <span class="text-slate-300 font-semibold">Server State</span>
+          <div class="flex items-center gap-3">
+            <span id="modal-ipsec-server-status" class="px-2 py-0.5 rounded text-[10px] font-mono font-semibold bg-slate-800 text-slate-400">STOPPED</span>
+            <label class="relative inline-flex items-center cursor-pointer">
+              <input type="checkbox" id="modal-ipsec-server-enabled" class="sr-only peer">
+              <div class="w-11 h-6 bg-slate-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-amber-600"></div>
+            </label>
+          </div>
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Pre-Shared Key (PSK) *</label>
+          <input type="password" id="modal-ipsec-server-psk" placeholder="••••••••" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-amber-500">
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Virtual IP Pool (CIDR) *</label>
+            <input type="text" id="modal-ipsec-server-pool" value="10.10.30.0/24" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">DNS Servers</label>
+            <input type="text" id="modal-ipsec-server-dns" value="1.1.1.1,8.8.8.8" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+          </div>
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Local Traffic Selector (Subnet)</label>
+          <input type="text" id="modal-ipsec-server-subnet" value="0.0.0.0/0" placeholder="0.0.0.0/0 (Full Tunnel)" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+        </div>
+
+        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-3">
+          <button type="button" onclick="closeIPSecServerModal()" class="px-4 py-2 rounded-lg text-slate-400 hover:bg-slate-800">Cancel</button>
+          <button type="submit" class="px-5 py-2 rounded-lg font-semibold bg-amber-600 hover:bg-amber-500 text-white shadow-lg shadow-amber-500/20">Apply IPSec Server</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
+  <!-- ================= MODAL: WIREGUARD CLIENT CONFIG VIEWER ================= -->
+  <div id="wg-client-conf-modal" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
+    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in">
+      <div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/40">
+        <div>
+          <h3 class="text-sm font-bold text-white">WireGuard Client Configuration</h3>
+          <p class="text-[11px] text-slate-400">Import this .conf into WireGuard on iOS, Android, macOS or Windows</p>
+        </div>
+        <button onclick="closeWGClientConfModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+      <div class="p-6 space-y-4">
+        <div>
+          <label class="block text-xs font-semibold text-slate-300 mb-1.5">WireGuard Client Profile (.conf)</label>
+          <textarea id="wg-client-conf-text" rows="11" readonly class="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-xs font-mono text-cyan-300 select-all focus:outline-none"></textarea>
+        </div>
+        <div class="flex justify-between items-center pt-2">
+          <span class="text-[11px] text-slate-500">Ready for instant import</span>
+          <div class="space-x-2">
+            <button onclick="copyWGClientConf()" class="px-4 py-2 rounded-lg text-xs font-semibold bg-slate-800 hover:bg-slate-700 text-white border border-slate-700 transition">
+              Copy Config
+            </button>
+            <button onclick="downloadWGClientConf()" class="px-4 py-2 rounded-lg text-xs font-semibold bg-cyan-600 hover:bg-cyan-500 text-white shadow-lg shadow-cyan-500/20 transition">
+              Download .conf
+            </button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <script src="app.js?v=2.3"></script>
 </body>
 </html>