| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- package vpn
- import (
- "encoding/json"
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "sync"
- )
- const (
- SwanctlConfPath = "/etc/swanctl/conf.d/tunnels.conf"
- IPSecStorePath = "/etc/proxmox-router/vpn/ipsec.json"
- )
- type IPSecManager struct {
- mu sync.Mutex
- config IPSecConfig
- }
- func NewIPSecManager() *IPSecManager {
- m := &IPSecManager{
- config: IPSecConfig{
- Connections: make([]IPSecConnection, 0),
- },
- }
- m.load()
- return m
- }
- func (m *IPSecManager) load() {
- data, err := os.ReadFile(IPSecStorePath)
- if err == nil {
- _ = json.Unmarshal(data, &m.config)
- if m.config.Connections == nil {
- m.config.Connections = make([]IPSecConnection, 0)
- }
- }
- }
- func (m *IPSecManager) save() error {
- _ = os.MkdirAll(filepath.Dir(IPSecStorePath), 0755)
- data, err := json.MarshalIndent(m.config, "", " ")
- if err != nil {
- return err
- }
- return os.WriteFile(IPSecStorePath, data, 0600)
- }
- func (m *IPSecManager) GetConfig() IPSecConfig {
- m.mu.Lock()
- defer m.mu.Unlock()
- return m.config
- }
- func (m *IPSecManager) SetConfig(cfg IPSecConfig) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- if cfg.Connections == nil {
- cfg.Connections = make([]IPSecConnection, 0)
- }
- m.config = cfg
- if err := m.save(); err != nil {
- return err
- }
- return m.generateSwanctlConfig()
- }
- func (m *IPSecManager) SaveConnection(conn IPSecConnection) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- if conn.Name == "" {
- return fmt.Errorf("connection name is required")
- }
- if conn.IKEProposals == "" {
- conn.IKEProposals = "aes256-sha256-modp2048,default"
- }
- if conn.ESPProposals == "" {
- conn.ESPProposals = "aes256-sha256-modp2048,default"
- }
- if conn.LocalAddrs == "" {
- conn.LocalAddrs = "%any"
- }
- found := false
- for i, c := range m.config.Connections {
- if c.Name == conn.Name {
- m.config.Connections[i] = conn
- found = true
- break
- }
- }
- if !found {
- m.config.Connections = append(m.config.Connections, conn)
- }
- if err := m.save(); err != nil {
- return err
- }
- return m.generateSwanctlConfig()
- }
- func (m *IPSecManager) DeleteConnection(name string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- var updated []IPSecConnection
- for _, c := range m.config.Connections {
- if c.Name != name {
- updated = append(updated, c)
- }
- }
- m.config.Connections = updated
- if err := m.save(); err != nil {
- return err
- }
- return m.generateSwanctlConfig()
- }
- func (m *IPSecManager) generateSwanctlConfig() error {
- _ = os.MkdirAll("/etc/swanctl/conf.d", 0755)
- var sb strings.Builder
- sb.WriteString("# Generated by Proxmox RouterOS - strongSwan IPSec Configuration\n\n")
- sb.WriteString("connections {\n")
- for _, c := range m.config.Connections {
- sb.WriteString(fmt.Sprintf(" %s {\n", c.Name))
- sb.WriteString(fmt.Sprintf(" local_addrs = %s\n", c.LocalAddrs))
- sb.WriteString(fmt.Sprintf(" remote_addrs = %s\n", c.RemoteAddrs))
- sb.WriteString(" local {\n")
- sb.WriteString(" auth = psk\n")
- sb.WriteString(" }\n")
- sb.WriteString(" remote {\n")
- sb.WriteString(" auth = psk\n")
- sb.WriteString(" }\n")
- sb.WriteString(" children {\n")
- sb.WriteString(fmt.Sprintf(" %s {\n", c.Name))
- if c.LocalSubnet != "" {
- sb.WriteString(fmt.Sprintf(" local_ts = %s\n", c.LocalSubnet))
- }
- if c.RemoteSubnet != "" {
- sb.WriteString(fmt.Sprintf(" remote_ts = %s\n", c.RemoteSubnet))
- }
- sb.WriteString(fmt.Sprintf(" esp_proposals = %s\n", c.ESPProposals))
- if c.AutoStart {
- sb.WriteString(" start_action = start\n")
- }
- sb.WriteString(" }\n")
- sb.WriteString(" }\n")
- sb.WriteString(fmt.Sprintf(" proposals = %s\n", c.IKEProposals))
- sb.WriteString(" }\n\n")
- }
- sb.WriteString("}\n\n")
- // Secrets
- sb.WriteString("secrets {\n")
- for _, c := range m.config.Connections {
- if c.PresharedKey != "" {
- sb.WriteString(fmt.Sprintf(" ike-%s {\n", c.Name))
- sb.WriteString(fmt.Sprintf(" id = %s\n", c.RemoteAddrs))
- sb.WriteString(fmt.Sprintf(" secret = %q\n", c.PresharedKey))
- sb.WriteString(" }\n")
- }
- }
- sb.WriteString("}\n")
- return os.WriteFile(SwanctlConfPath, []byte(sb.String()), 0600)
- }
- func (m *IPSecManager) StartTunnel(name string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- _ = exec.Command("systemctl", "start", "strongswan").Run()
- _ = exec.Command("swanctl", "--load-all").Run()
- cmd := exec.Command("swanctl", "--initiate", "--child", name)
- if out, err := cmd.CombinedOutput(); err != nil {
- return fmt.Errorf("failed to initiate IPSec tunnel %s: %s: %w", name, string(out), err)
- }
- return nil
- }
- func (m *IPSecManager) SetEnabled(name string, enabled bool) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- for i, c := range m.config.Connections {
- if c.Name == name {
- m.config.Connections[i].Disabled = !enabled
- _ = m.save()
- if enabled {
- go m.StartTunnel(name)
- } else {
- go m.StopTunnel(name)
- }
- return nil
- }
- }
- return fmt.Errorf("connection %q not found", name)
- }
- func (m *IPSecManager) List() []IPSecConnection {
- st := m.GetStatus()
- return st.Connections
- }
- func (m *IPSecManager) StopTunnel(name string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- cmd := exec.Command("swanctl", "--terminate", "--child", name)
- out, err := cmd.CombinedOutput()
- if err != nil {
- return fmt.Errorf("failed to terminate tunnel %s: %s: %w", name, string(out), err)
- }
- return nil
- }
- func (m *IPSecManager) GetStatus() IPSecStatus {
- m.mu.Lock()
- defer m.mu.Unlock()
- out, err := exec.Command("swanctl", "--list-sas").CombinedOutput()
- rawOutput := string(out)
- running := err == nil
- activeSAs := 0
- conns := make([]IPSecConnection, len(m.config.Connections))
- copy(conns, m.config.Connections)
- for i := range conns {
- if strings.Contains(rawOutput, conns[i].Name) && strings.Contains(rawOutput, "INSTALLED") {
- conns[i].Status = "ESTABLISHED"
- conns[i].Running = true
- activeSAs++
- } else {
- conns[i].Status = "DISCONNECTED"
- conns[i].Running = false
- }
- }
- return IPSecStatus{
- Running: running,
- ActiveSAs: activeSAs,
- Connections: conns,
- RawStatus: rawOutput,
- }
- }
|