ipsec.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. package vpn
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "os"
  6. "os/exec"
  7. "path/filepath"
  8. "strings"
  9. "sync"
  10. )
  11. const (
  12. SwanctlConfPath = "/etc/swanctl/conf.d/tunnels.conf"
  13. IPSecStorePath = "/etc/debtik/vpn/ipsec.json"
  14. )
  15. type IPSecManager struct {
  16. mu sync.Mutex
  17. config IPSecConfig
  18. }
  19. func NewIPSecManager() *IPSecManager {
  20. m := &IPSecManager{
  21. config: IPSecConfig{
  22. Connections: make([]IPSecConnection, 0),
  23. },
  24. }
  25. m.load()
  26. return m
  27. }
  28. func (m *IPSecManager) load() {
  29. data, err := os.ReadFile(IPSecStorePath)
  30. if err == nil {
  31. _ = json.Unmarshal(data, &m.config)
  32. if m.config.Connections == nil {
  33. m.config.Connections = make([]IPSecConnection, 0)
  34. }
  35. }
  36. }
  37. func (m *IPSecManager) save() error {
  38. _ = os.MkdirAll(filepath.Dir(IPSecStorePath), 0755)
  39. data, err := json.MarshalIndent(m.config, "", " ")
  40. if err != nil {
  41. return err
  42. }
  43. return os.WriteFile(IPSecStorePath, data, 0600)
  44. }
  45. func (m *IPSecManager) GetConfig() IPSecConfig {
  46. m.mu.Lock()
  47. defer m.mu.Unlock()
  48. return m.config
  49. }
  50. func (m *IPSecManager) SetConfig(cfg IPSecConfig) error {
  51. m.mu.Lock()
  52. defer m.mu.Unlock()
  53. if cfg.Connections == nil {
  54. cfg.Connections = make([]IPSecConnection, 0)
  55. }
  56. m.config = cfg
  57. if err := m.save(); err != nil {
  58. return err
  59. }
  60. return m.generateSwanctlConfig()
  61. }
  62. func (m *IPSecManager) SaveConnection(conn IPSecConnection) error {
  63. m.mu.Lock()
  64. defer m.mu.Unlock()
  65. if conn.Name == "" {
  66. return fmt.Errorf("connection name is required")
  67. }
  68. if conn.IKEVersion == "" {
  69. conn.IKEVersion = "ikev2"
  70. }
  71. if conn.IKEProposals == "" {
  72. conn.IKEProposals = "aes256-sha256-modp2048,default"
  73. }
  74. if conn.ESPProposals == "" {
  75. conn.ESPProposals = "aes256-sha256-modp2048,default"
  76. }
  77. if conn.LocalAddrs == "" {
  78. conn.LocalAddrs = "%any"
  79. }
  80. if conn.IKEKeylife <= 0 {
  81. conn.IKEKeylife = 86400
  82. }
  83. if conn.ESPKeylife <= 0 {
  84. conn.ESPKeylife = 28800
  85. }
  86. if conn.Phase2Name == "" {
  87. conn.Phase2Name = conn.Name + "_p2"
  88. }
  89. // Normalize subnets
  90. if len(conn.LocalSubnets) == 0 && conn.LocalSubnet != "" {
  91. conn.LocalSubnets = []string{conn.LocalSubnet}
  92. }
  93. if len(conn.LocalSubnets) > 0 {
  94. conn.LocalSubnet = conn.LocalSubnets[0]
  95. }
  96. if len(conn.RemoteSubnets) == 0 && conn.RemoteSubnet != "" {
  97. conn.RemoteSubnets = []string{conn.RemoteSubnet}
  98. }
  99. if len(conn.RemoteSubnets) > 0 {
  100. conn.RemoteSubnet = conn.RemoteSubnets[0]
  101. }
  102. found := false
  103. for i, c := range m.config.Connections {
  104. if c.Name == conn.Name {
  105. m.config.Connections[i] = conn
  106. found = true
  107. break
  108. }
  109. }
  110. if !found {
  111. m.config.Connections = append(m.config.Connections, conn)
  112. }
  113. if err := m.save(); err != nil {
  114. return err
  115. }
  116. return m.generateSwanctlConfig()
  117. }
  118. func (m *IPSecManager) DeleteConnection(name string) error {
  119. m.mu.Lock()
  120. defer m.mu.Unlock()
  121. var updated []IPSecConnection
  122. for _, c := range m.config.Connections {
  123. if c.Name != name {
  124. updated = append(updated, c)
  125. }
  126. }
  127. m.config.Connections = updated
  128. if err := m.save(); err != nil {
  129. return err
  130. }
  131. return m.generateSwanctlConfig()
  132. }
  133. func (m *IPSecManager) generateSwanctlConfig() error {
  134. _ = os.MkdirAll("/etc/swanctl/conf.d", 0755)
  135. var sb strings.Builder
  136. sb.WriteString("# Generated by DebTik - strongSwan IPSec Configuration (FortiGate Phase 1 & 2 Engine)\n\n")
  137. sb.WriteString("connections {\n")
  138. for _, c := range m.config.Connections {
  139. if c.Disabled {
  140. continue
  141. }
  142. connName := sanitizeConnName(c.Name)
  143. sb.WriteString(fmt.Sprintf(" %s {\n", connName))
  144. // IKE Version
  145. if strings.EqualFold(c.IKEVersion, "ikev1") {
  146. sb.WriteString(" version = 1\n")
  147. } else {
  148. sb.WriteString(" version = 2\n")
  149. }
  150. localAddrs := strings.TrimSpace(c.LocalAddrs)
  151. if localAddrs == "" {
  152. localAddrs = "%any"
  153. }
  154. sb.WriteString(fmt.Sprintf(" local_addrs = %s\n", localAddrs))
  155. sb.WriteString(fmt.Sprintf(" remote_addrs = %s\n", strings.TrimSpace(c.RemoteAddrs)))
  156. // Local auth
  157. sb.WriteString(" local {\n")
  158. sb.WriteString(" auth = psk\n")
  159. if strings.TrimSpace(c.LocalID) != "" {
  160. sb.WriteString(fmt.Sprintf(" id = %s\n", strings.TrimSpace(c.LocalID)))
  161. }
  162. sb.WriteString(" }\n")
  163. // Remote auth
  164. sb.WriteString(" remote {\n")
  165. sb.WriteString(" auth = psk\n")
  166. if strings.TrimSpace(c.RemoteID) != "" {
  167. sb.WriteString(fmt.Sprintf(" id = %s\n", strings.TrimSpace(c.RemoteID)))
  168. }
  169. sb.WriteString(" }\n")
  170. // DPD
  171. dpd := strings.ToLower(strings.TrimSpace(c.DPD))
  172. if dpd == "disabled" {
  173. // DPD disabled
  174. } else if dpd == "periodic" {
  175. sb.WriteString(" dpd_delay = 20s\n")
  176. sb.WriteString(" dpd_timeout = 60s\n")
  177. } else {
  178. // default on-demand
  179. sb.WriteString(" dpd_delay = 30s\n")
  180. sb.WriteString(" dpd_timeout = 120s\n")
  181. }
  182. // IKE Proposals & Rekey
  183. ikeProps := strings.TrimSpace(c.IKEProposals)
  184. if ikeProps == "" {
  185. ikeProps = "aes256-sha256-modp2048,aes128-sha256-modp2048,default"
  186. }
  187. sb.WriteString(fmt.Sprintf(" proposals = %s\n", ikeProps))
  188. ikeKeylife := 86400
  189. if c.IKEKeylife > 0 {
  190. ikeKeylife = c.IKEKeylife
  191. }
  192. sb.WriteString(fmt.Sprintf(" rekey_time = %ds\n", ikeKeylife))
  193. // Children (Phase 2)
  194. sb.WriteString(" children {\n")
  195. p2Name := strings.TrimSpace(c.Phase2Name)
  196. if p2Name == "" {
  197. p2Name = connName + "_p2"
  198. } else {
  199. p2Name = sanitizeConnName(p2Name)
  200. }
  201. sb.WriteString(fmt.Sprintf(" %s {\n", p2Name))
  202. // Traffic Selectors: Collect all local and remote subnets!
  203. var localNets []string
  204. for _, s := range c.LocalSubnets {
  205. s = strings.TrimSpace(s)
  206. if s != "" {
  207. localNets = append(localNets, s)
  208. }
  209. }
  210. if len(localNets) == 0 && strings.TrimSpace(c.LocalSubnet) != "" {
  211. localNets = append(localNets, strings.TrimSpace(c.LocalSubnet))
  212. }
  213. if len(localNets) == 0 {
  214. localNets = []string{"0.0.0.0/0"}
  215. }
  216. var remoteNets []string
  217. for _, s := range c.RemoteSubnets {
  218. s = strings.TrimSpace(s)
  219. if s != "" {
  220. remoteNets = append(remoteNets, s)
  221. }
  222. }
  223. if len(remoteNets) == 0 && strings.TrimSpace(c.RemoteSubnet) != "" {
  224. remoteNets = append(remoteNets, strings.TrimSpace(c.RemoteSubnet))
  225. }
  226. if len(remoteNets) == 0 {
  227. remoteNets = []string{"0.0.0.0/0"}
  228. }
  229. sb.WriteString(fmt.Sprintf(" local_ts = %s\n", strings.Join(localNets, ",")))
  230. sb.WriteString(fmt.Sprintf(" remote_ts = %s\n", strings.Join(remoteNets, ",")))
  231. // ESP Proposals
  232. espProps := strings.TrimSpace(c.ESPProposals)
  233. if espProps == "" {
  234. espProps = "aes256-sha256-modp2048,aes128-sha256,default"
  235. }
  236. sb.WriteString(fmt.Sprintf(" esp_proposals = %s\n", espProps))
  237. espKeylife := 28800
  238. if c.ESPKeylife > 0 {
  239. espKeylife = c.ESPKeylife
  240. }
  241. sb.WriteString(fmt.Sprintf(" rekey_time = %ds\n", espKeylife))
  242. if c.AutoStart {
  243. sb.WriteString(" start_action = start\n")
  244. sb.WriteString(" dpd_action = restart\n")
  245. } else {
  246. sb.WriteString(" start_action = none\n")
  247. }
  248. sb.WriteString(" }\n")
  249. sb.WriteString(" }\n")
  250. sb.WriteString(" }\n\n")
  251. }
  252. sb.WriteString("}\n\n")
  253. // Secrets
  254. sb.WriteString("secrets {\n")
  255. for _, c := range m.config.Connections {
  256. if c.Disabled {
  257. continue
  258. }
  259. if c.PresharedKey != "" {
  260. connName := sanitizeConnName(c.Name)
  261. remoteAddrs := strings.TrimSpace(c.RemoteAddrs)
  262. sb.WriteString(fmt.Sprintf(" ike-%s {\n", connName))
  263. if remoteAddrs != "" && remoteAddrs != "%any" {
  264. sb.WriteString(fmt.Sprintf(" id = %s\n", remoteAddrs))
  265. }
  266. sb.WriteString(fmt.Sprintf(" secret = %q\n", c.PresharedKey))
  267. sb.WriteString(" }\n")
  268. }
  269. }
  270. sb.WriteString("}\n")
  271. if err := os.WriteFile(SwanctlConfPath, []byte(sb.String()), 0600); err != nil {
  272. return err
  273. }
  274. if err := exec.Command("systemctl", "start", "strongswan").Run(); err != nil {
  275. return err
  276. }
  277. if out, err := exec.Command("swanctl", "--load-all").CombinedOutput(); err != nil {
  278. return fmt.Errorf("swanctl load failed: %s: %w", out, err)
  279. }
  280. return nil
  281. }
  282. func sanitizeConnName(raw string) string {
  283. raw = strings.TrimSpace(raw)
  284. var b strings.Builder
  285. for _, r := range raw {
  286. if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {
  287. b.WriteRune(r)
  288. } else {
  289. b.WriteRune('_')
  290. }
  291. }
  292. res := b.String()
  293. if res == "" {
  294. res = "tunnel1"
  295. }
  296. return res
  297. }
  298. func (m *IPSecManager) StartTunnel(name string) error {
  299. m.mu.Lock()
  300. defer m.mu.Unlock()
  301. return m.startTunnelLocked(name)
  302. }
  303. func (m *IPSecManager) startTunnelLocked(name string) error {
  304. if err := exec.Command("systemctl", "start", "strongswan").Run(); err != nil {
  305. return err
  306. }
  307. if out, err := exec.Command("swanctl", "--load-all").CombinedOutput(); err != nil {
  308. return fmt.Errorf("swanctl load failed: %s: %w", out, err)
  309. }
  310. var p2Name string
  311. for _, c := range m.config.Connections {
  312. if c.Name == name {
  313. p2Name = c.Phase2Name
  314. break
  315. }
  316. }
  317. if p2Name == "" {
  318. p2Name = name + "_p2"
  319. }
  320. cmd := exec.Command("swanctl", "--initiate", "--child", p2Name)
  321. if out, err := cmd.CombinedOutput(); err != nil {
  322. cmd2 := exec.Command("swanctl", "--initiate", "--child", name)
  323. if out2, err2 := cmd2.CombinedOutput(); err2 != nil {
  324. return fmt.Errorf("failed to initiate IPSec tunnel %s: %s / %s", name, string(out), string(out2))
  325. }
  326. }
  327. return nil
  328. }
  329. func (m *IPSecManager) StopTunnel(name string) error {
  330. m.mu.Lock()
  331. defer m.mu.Unlock()
  332. return m.stopTunnelLocked(name)
  333. }
  334. func (m *IPSecManager) stopTunnelLocked(name string) error {
  335. var p2Name string
  336. for _, c := range m.config.Connections {
  337. if c.Name == name {
  338. p2Name = c.Phase2Name
  339. break
  340. }
  341. }
  342. if p2Name == "" {
  343. p2Name = name + "_p2"
  344. }
  345. _ = exec.Command("swanctl", "--terminate", "--child", p2Name).Run()
  346. _ = exec.Command("swanctl", "--terminate", "--child", name).Run()
  347. _ = exec.Command("swanctl", "--terminate", "--ike", name).Run()
  348. return nil
  349. }
  350. func (m *IPSecManager) SetEnabled(name string, enabled bool) error {
  351. m.mu.Lock()
  352. defer m.mu.Unlock()
  353. for i, c := range m.config.Connections {
  354. if c.Name == name {
  355. m.config.Connections[i].Disabled = !enabled
  356. if err := m.save(); err != nil {
  357. return err
  358. }
  359. if enabled {
  360. return m.startTunnelLocked(name)
  361. } else {
  362. return m.stopTunnelLocked(name)
  363. }
  364. }
  365. }
  366. return fmt.Errorf("connection %q not found", name)
  367. }
  368. func (m *IPSecManager) List() []IPSecConnection {
  369. st := m.GetStatus()
  370. return st.Connections
  371. }
  372. func (m *IPSecManager) GetStatus() IPSecStatus {
  373. m.mu.Lock()
  374. defer m.mu.Unlock()
  375. out, err := exec.Command("swanctl", "--list-sas").CombinedOutput()
  376. rawOutput := string(out)
  377. running := err == nil
  378. activeSAs := 0
  379. conns := make([]IPSecConnection, len(m.config.Connections))
  380. copy(conns, m.config.Connections)
  381. for i := range conns {
  382. if strings.Contains(rawOutput, conns[i].Name) && strings.Contains(rawOutput, "INSTALLED") {
  383. conns[i].Status = "ESTABLISHED"
  384. conns[i].Running = true
  385. activeSAs++
  386. } else {
  387. conns[i].Status = "DISCONNECTED"
  388. conns[i].Running = false
  389. }
  390. }
  391. return IPSecStatus{
  392. Running: running,
  393. ActiveSAs: activeSAs,
  394. Connections: conns,
  395. RawStatus: rawOutput,
  396. }
  397. }