package cli import ( "encoding/json" "fmt" "strconv" "strings" "proxmox-router/internal/aerotun" "proxmox-router/internal/auth" ) func (e *Executor) handleInterfaceAeroTUN(parts []string, user *auth.PublicUser) string { if e.vpnMgr == nil || e.vpnMgr.AeroTUN == nil { return ColorRed + "AeroTUN subsystem not available.\n" + ColorReset } mgr := e.vpnMgr.AeroTUN action := "print" if len(parts) > 0 { action = strings.ToLower(parts[0]) parts = parts[1:] } pos, params := parseArgs(parts) if action == "print" || action == "show" || action == "pr" { list := mgr.List() for i := range list { list[i] = list[i].PublicView() } if params["detail"] == "yes" { data, _ := json.MarshalIndent(list, "", " ") return string(data) + "\n" } tbl := NewTable("NAME", "VERSION", "STATE", "BACKEND", "LOCAL", "PEER", "ERROR") for _, inst := range list { tbl.AddRow(inst.Name, strconv.Itoa(inst.Protocol), aeroTUNState(inst), inst.Backend, inst.LocalAddress, inst.PeerAddress, inst.LastError) } return tbl.String() } if user == nil || user.Role != auth.RoleFull { return ColorRed + "Permission denied: admin role required to manage AeroTUN\n" + ColorReset } if action == "genkey" { key, err := aerotun.GenerateSecureKey() if err != nil { return cliError(err) } return key + "\n" } if action != "add" && action != "set" && action != "remove" && action != "enable" && action != "disable" && action != "start" && action != "stop" { return ColorRed + "Usage: /interface aerotun [print [detail=yes]|add|set |remove |enable |disable |start |stop |genkey]\n" + ColorReset } inst := aerotun.TunnelInstance{Protocol: 3, Mode: "server", Disabled: true, MTU: aerotun.DefaultMTU} if action != "add" { if len(pos) != 1 { return cliError(fmt.Errorf("%s requires one tunnel name or ID", action)) } found := false for _, candidate := range mgr.List() { if candidate.Name == pos[0] || candidate.ID == pos[0] { inst, found = candidate, true break } } if !found { return cliError(fmt.Errorf("AeroTUN tunnel not found: %s", pos[0])) } } var err error switch action { case "remove": err = mgr.Delete(inst.ID) case "enable": err = mgr.ToggleState(inst.ID, false) case "disable": err = mgr.ToggleState(inst.ID, true) case "start": err = mgr.StartTunnel(inst.ID) case "stop": err = mgr.StopTunnel(inst.ID) case "add", "set": for key, value := range params { switch key { case "name": inst.Name = value case "mode": inst.Mode = value case "endpoint": inst.RemoteEndpoint = value case "local-address": inst.LocalAddress = value case "peer-address": inst.PeerAddress = value case "peer-public-key": inst.PeerPublicKey = value case "psk": inst.PSK = value case "comment": inst.Comment = value case "allowed-ips": inst.AllowedIPs = nil for _, prefix := range strings.Split(value, ",") { if s := strings.TrimSpace(prefix); s != "" { inst.AllowedIPs = append(inst.AllowedIPs, s) } } case "protocol", "listen-port", "mtu": var number int number, err = strconv.Atoi(value) if err == nil { switch key { case "protocol": inst.Protocol = number case "listen-port": inst.ListenPort = number case "mtu": inst.MTU = number } } case "disabled", "compression", "default-route": var enabled bool switch value { case "yes", "true": enabled = true case "no", "false": enabled = false default: err = fmt.Errorf("%s must be yes or no", key) } if err == nil { switch key { case "disabled": inst.Disabled = enabled case "compression": inst.Compression = enabled case "default-route": inst.DefaultRoute = enabled } } default: err = fmt.Errorf("unknown AeroTUN parameter: %s", key) } if err != nil { return cliError(err) } } if action == "add" { if inst.Name == "" { return cliError(fmt.Errorf("name is required")) } if inst.PSK == "" { return cliError(fmt.Errorf("psk is required; use /interface aerotun genkey and share it with the peer")) } } inst, err = mgr.Save(inst) } if err != nil { return cliError(err) } if current, ok := mgr.Get(inst.ID); ok { return ColorGreen + fmt.Sprintf("AeroTUN %s: %s\nPublic key: %s\n", current.Name, aeroTUNState(current), current.PublicKey) + ColorReset } return ColorGreen + "AeroTUN tunnel removed.\n" + ColorReset } func cliError(err error) string { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } func aeroTUNState(inst aerotun.TunnelInstance) string { switch { case inst.LastError != "": return "failed" case inst.Connected: return "connected" case inst.Running: return "connecting" case inst.Disabled: return "saved/disabled" default: return "stopped" } }