| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452 |
- package vpn
- import (
- "encoding/json"
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "sync"
- )
- const (
- SwanctlConfPath = "/etc/swanctl/conf.d/tunnels.conf"
- IPSecStorePath = "/etc/debtik/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.IKEVersion == "" {
- conn.IKEVersion = "ikev2"
- }
- if conn.IKEProposals == "" {
- conn.IKEProposals = "aes256-sha256-modp2048,default"
- }
- if conn.ESPProposals == "" {
- conn.ESPProposals = "aes256-sha256-modp2048,default"
- }
- if conn.LocalAddrs == "" {
- conn.LocalAddrs = "%any"
- }
- if conn.IKEKeylife <= 0 {
- conn.IKEKeylife = 86400
- }
- if conn.ESPKeylife <= 0 {
- conn.ESPKeylife = 28800
- }
- if conn.Phase2Name == "" {
- conn.Phase2Name = conn.Name + "_p2"
- }
- // Normalize subnets
- if len(conn.LocalSubnets) == 0 && conn.LocalSubnet != "" {
- conn.LocalSubnets = []string{conn.LocalSubnet}
- }
- if len(conn.LocalSubnets) > 0 {
- conn.LocalSubnet = conn.LocalSubnets[0]
- }
- if len(conn.RemoteSubnets) == 0 && conn.RemoteSubnet != "" {
- conn.RemoteSubnets = []string{conn.RemoteSubnet}
- }
- if len(conn.RemoteSubnets) > 0 {
- conn.RemoteSubnet = conn.RemoteSubnets[0]
- }
- 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 DebTik - strongSwan IPSec Configuration (FortiGate Phase 1 & 2 Engine)\n\n")
- sb.WriteString("connections {\n")
- for _, c := range m.config.Connections {
- if c.Disabled {
- continue
- }
- connName := sanitizeConnName(c.Name)
- sb.WriteString(fmt.Sprintf(" %s {\n", connName))
- // IKE Version
- if strings.EqualFold(c.IKEVersion, "ikev1") {
- sb.WriteString(" version = 1\n")
- } else {
- sb.WriteString(" version = 2\n")
- }
- localAddrs := strings.TrimSpace(c.LocalAddrs)
- if localAddrs == "" {
- localAddrs = "%any"
- }
- sb.WriteString(fmt.Sprintf(" local_addrs = %s\n", localAddrs))
- sb.WriteString(fmt.Sprintf(" remote_addrs = %s\n", strings.TrimSpace(c.RemoteAddrs)))
- // Local auth
- sb.WriteString(" local {\n")
- sb.WriteString(" auth = psk\n")
- if strings.TrimSpace(c.LocalID) != "" {
- sb.WriteString(fmt.Sprintf(" id = %s\n", strings.TrimSpace(c.LocalID)))
- }
- sb.WriteString(" }\n")
- // Remote auth
- sb.WriteString(" remote {\n")
- sb.WriteString(" auth = psk\n")
- if strings.TrimSpace(c.RemoteID) != "" {
- sb.WriteString(fmt.Sprintf(" id = %s\n", strings.TrimSpace(c.RemoteID)))
- }
- sb.WriteString(" }\n")
- // DPD
- dpd := strings.ToLower(strings.TrimSpace(c.DPD))
- if dpd == "disabled" {
- // DPD disabled
- } else if dpd == "periodic" {
- sb.WriteString(" dpd_delay = 20s\n")
- sb.WriteString(" dpd_timeout = 60s\n")
- } else {
- // default on-demand
- sb.WriteString(" dpd_delay = 30s\n")
- sb.WriteString(" dpd_timeout = 120s\n")
- }
- // IKE Proposals & Rekey
- ikeProps := strings.TrimSpace(c.IKEProposals)
- if ikeProps == "" {
- ikeProps = "aes256-sha256-modp2048,aes128-sha256-modp2048,default"
- }
- sb.WriteString(fmt.Sprintf(" proposals = %s\n", ikeProps))
- ikeKeylife := 86400
- if c.IKEKeylife > 0 {
- ikeKeylife = c.IKEKeylife
- }
- sb.WriteString(fmt.Sprintf(" rekey_time = %ds\n", ikeKeylife))
- // Children (Phase 2)
- sb.WriteString(" children {\n")
- p2Name := strings.TrimSpace(c.Phase2Name)
- if p2Name == "" {
- p2Name = connName + "_p2"
- } else {
- p2Name = sanitizeConnName(p2Name)
- }
- sb.WriteString(fmt.Sprintf(" %s {\n", p2Name))
- // Traffic Selectors: Collect all local and remote subnets!
- var localNets []string
- for _, s := range c.LocalSubnets {
- s = strings.TrimSpace(s)
- if s != "" {
- localNets = append(localNets, s)
- }
- }
- if len(localNets) == 0 && strings.TrimSpace(c.LocalSubnet) != "" {
- localNets = append(localNets, strings.TrimSpace(c.LocalSubnet))
- }
- if len(localNets) == 0 {
- localNets = []string{"0.0.0.0/0"}
- }
- var remoteNets []string
- for _, s := range c.RemoteSubnets {
- s = strings.TrimSpace(s)
- if s != "" {
- remoteNets = append(remoteNets, s)
- }
- }
- if len(remoteNets) == 0 && strings.TrimSpace(c.RemoteSubnet) != "" {
- remoteNets = append(remoteNets, strings.TrimSpace(c.RemoteSubnet))
- }
- if len(remoteNets) == 0 {
- remoteNets = []string{"0.0.0.0/0"}
- }
- sb.WriteString(fmt.Sprintf(" local_ts = %s\n", strings.Join(localNets, ",")))
- sb.WriteString(fmt.Sprintf(" remote_ts = %s\n", strings.Join(remoteNets, ",")))
- // ESP Proposals
- espProps := strings.TrimSpace(c.ESPProposals)
- if espProps == "" {
- espProps = "aes256-sha256-modp2048,aes128-sha256,default"
- }
- sb.WriteString(fmt.Sprintf(" esp_proposals = %s\n", espProps))
- espKeylife := 28800
- if c.ESPKeylife > 0 {
- espKeylife = c.ESPKeylife
- }
- sb.WriteString(fmt.Sprintf(" rekey_time = %ds\n", espKeylife))
- if c.AutoStart {
- sb.WriteString(" start_action = start\n")
- sb.WriteString(" dpd_action = restart\n")
- } else {
- sb.WriteString(" start_action = none\n")
- }
- sb.WriteString(" }\n")
- sb.WriteString(" }\n")
- sb.WriteString(" }\n\n")
- }
- sb.WriteString("}\n\n")
- // Secrets
- sb.WriteString("secrets {\n")
- for _, c := range m.config.Connections {
- if c.Disabled {
- continue
- }
- if c.PresharedKey != "" {
- connName := sanitizeConnName(c.Name)
- remoteAddrs := strings.TrimSpace(c.RemoteAddrs)
- sb.WriteString(fmt.Sprintf(" ike-%s {\n", connName))
- if remoteAddrs != "" && remoteAddrs != "%any" {
- sb.WriteString(fmt.Sprintf(" id = %s\n", remoteAddrs))
- }
- sb.WriteString(fmt.Sprintf(" secret = %q\n", c.PresharedKey))
- sb.WriteString(" }\n")
- }
- }
- sb.WriteString("}\n")
- if err := os.WriteFile(SwanctlConfPath, []byte(sb.String()), 0600); err != nil {
- return err
- }
- if err := exec.Command("systemctl", "start", "strongswan").Run(); err != nil {
- return err
- }
- if out, err := exec.Command("swanctl", "--load-all").CombinedOutput(); err != nil {
- return fmt.Errorf("swanctl load failed: %s: %w", out, err)
- }
- return nil
- }
- func sanitizeConnName(raw string) string {
- raw = strings.TrimSpace(raw)
- var b strings.Builder
- for _, r := range raw {
- if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
- b.WriteRune(r)
- } else {
- b.WriteRune('_')
- }
- }
- res := b.String()
- if res == "" {
- res = "tunnel1"
- }
- return res
- }
- func (m *IPSecManager) StartTunnel(name string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- return m.startTunnelLocked(name)
- }
- func (m *IPSecManager) startTunnelLocked(name string) error {
- if err := exec.Command("systemctl", "start", "strongswan").Run(); err != nil {
- return err
- }
- if out, err := exec.Command("swanctl", "--load-all").CombinedOutput(); err != nil {
- return fmt.Errorf("swanctl load failed: %s: %w", out, err)
- }
- var p2Name string
- for _, c := range m.config.Connections {
- if c.Name == name {
- p2Name = c.Phase2Name
- break
- }
- }
- if p2Name == "" {
- p2Name = name + "_p2"
- }
- cmd := exec.Command("swanctl", "--initiate", "--child", p2Name)
- if out, err := cmd.CombinedOutput(); err != nil {
- cmd2 := exec.Command("swanctl", "--initiate", "--child", name)
- if out2, err2 := cmd2.CombinedOutput(); err2 != nil {
- return fmt.Errorf("failed to initiate IPSec tunnel %s: %s / %s", name, string(out), string(out2))
- }
- }
- return nil
- }
- func (m *IPSecManager) StopTunnel(name string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- return m.stopTunnelLocked(name)
- }
- func (m *IPSecManager) stopTunnelLocked(name string) error {
- var p2Name string
- for _, c := range m.config.Connections {
- if c.Name == name {
- p2Name = c.Phase2Name
- break
- }
- }
- if p2Name == "" {
- p2Name = name + "_p2"
- }
- _ = exec.Command("swanctl", "--terminate", "--child", p2Name).Run()
- _ = exec.Command("swanctl", "--terminate", "--child", name).Run()
- _ = exec.Command("swanctl", "--terminate", "--ike", name).Run()
- 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
- if err := m.save(); err != nil {
- return err
- }
- if enabled {
- return m.startTunnelLocked(name)
- } else {
- return m.stopTunnelLocked(name)
- }
- }
- }
- return fmt.Errorf("connection %q not found", name)
- }
- func (m *IPSecManager) List() []IPSecConnection {
- st := m.GetStatus()
- return st.Connections
- }
- 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,
- }
- }
|