ssh_tunnel.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "os"
  6. "os/exec"
  7. "sync"
  8. "time"
  9. )
  10. // TunnelConfig contains configuration for a specific tunnel
  11. type TunnelConfig struct {
  12. Enabled bool
  13. Server string
  14. Port int
  15. User string
  16. RemotePort int
  17. KeepaliveInterval int
  18. ReconnectDelay int
  19. }
  20. // SSHTunnel manages reverse SSH tunnel to server
  21. type SSHTunnel struct {
  22. name string // "ssh" or "dashboard"
  23. localPort int // Local port to forward (22 for SSH, 8080 for dashboard)
  24. cfg *TunnelConfig
  25. client *APIClient
  26. cmd *exec.Cmd
  27. stopChan chan struct{}
  28. mu sync.Mutex
  29. }
  30. // NewSSHTunnel creates a new SSH tunnel manager
  31. func NewSSHTunnel(name string, localPort int, cfg *TunnelConfig, client *APIClient) *SSHTunnel {
  32. return &SSHTunnel{
  33. name: name,
  34. localPort: localPort,
  35. cfg: cfg,
  36. client: client,
  37. stopChan: make(chan struct{}),
  38. }
  39. }
  40. // Start initiates the SSH tunnel
  41. func (t *SSHTunnel) Start() error {
  42. if !t.cfg.Enabled {
  43. log.Printf("[%s-tunnel] Tunnel disabled", t.name)
  44. return nil
  45. }
  46. if t.cfg.RemotePort == 0 {
  47. log.Printf("[%s-tunnel] Remote port not allocated yet, waiting...", t.name)
  48. return nil
  49. }
  50. keyPath := "/opt/mybeacon/etc/ssh_tunnel_ed25519"
  51. // Verify key exists
  52. if _, err := os.Stat(keyPath); os.IsNotExist(err) {
  53. return fmt.Errorf("SSH key not found: %s", keyPath)
  54. }
  55. // Build reverse tunnel string: remote_port:localhost:local_port
  56. reverseSpec := fmt.Sprintf("%d:localhost:%d", t.cfg.RemotePort, t.localPort)
  57. args := []string{
  58. "-N", // No command execution
  59. "-R", reverseSpec, // Reverse tunnel with fixed port
  60. "-o", fmt.Sprintf("ServerAliveInterval=%d", t.cfg.KeepaliveInterval),
  61. "-o", "ServerAliveCountMax=3",
  62. "-o", "ExitOnForwardFailure=yes",
  63. "-o", "StrictHostKeyChecking=accept-new",
  64. "-i", keyPath,
  65. "-p", fmt.Sprintf("%d", t.cfg.Port),
  66. fmt.Sprintf("%s@%s", t.cfg.User, t.cfg.Server),
  67. }
  68. t.cmd = exec.Command("ssh", args...)
  69. if err := t.cmd.Start(); err != nil {
  70. return err
  71. }
  72. log.Printf("[%s-tunnel] Started: %s:%d -> localhost:%d (remote_port=%d)",
  73. t.name, t.cfg.Server, t.cfg.Port, t.localPort, t.cfg.RemotePort)
  74. // Report tunnel connection to server
  75. go func() {
  76. // Wait a bit for SSH to establish connection
  77. time.Sleep(2 * time.Second)
  78. if err := t.client.ReportTunnelPort(t.name, t.cfg.RemotePort, "connected"); err != nil {
  79. log.Printf("[%s-tunnel] Failed to report connection to server: %v", t.name, err)
  80. } else {
  81. log.Printf("[%s-tunnel] Reported connection to server (port %d)", t.name, t.cfg.RemotePort)
  82. }
  83. }()
  84. // Monitor process
  85. go t.monitor()
  86. return nil
  87. }
  88. // monitor watches SSH process and handles reconnection
  89. func (t *SSHTunnel) monitor() {
  90. err := t.cmd.Wait()
  91. if err != nil {
  92. log.Printf("[%s-tunnel] Tunnel exited with error: %v", t.name, err)
  93. } else {
  94. log.Printf("[%s-tunnel] Tunnel exited", t.name)
  95. }
  96. // Auto-reconnect after delay
  97. select {
  98. case <-t.stopChan:
  99. log.Printf("[%s-tunnel] Stopped, not reconnecting", t.name)
  100. return
  101. case <-time.After(time.Duration(t.cfg.ReconnectDelay) * time.Second):
  102. log.Printf("[%s-tunnel] Reconnecting in %ds...", t.name, t.cfg.ReconnectDelay)
  103. if err := t.Start(); err != nil {
  104. log.Printf("[%s-tunnel] Reconnect failed: %v", t.name, err)
  105. // Will retry after another delay via monitor()
  106. }
  107. }
  108. }
  109. // Stop terminates the SSH tunnel
  110. func (t *SSHTunnel) Stop() {
  111. log.Printf("[%s-tunnel] Stopping...", t.name)
  112. t.mu.Lock()
  113. defer t.mu.Unlock()
  114. select {
  115. case <-t.stopChan:
  116. // Already closed
  117. return
  118. default:
  119. close(t.stopChan)
  120. }
  121. if t.cmd != nil && t.cmd.Process != nil {
  122. t.cmd.Process.Kill()
  123. // Report disconnection to server
  124. if t.cfg.RemotePort != 0 {
  125. if err := t.client.ReportTunnelPort(t.name, 0, "disconnected"); err != nil {
  126. log.Printf("[%s-tunnel] Failed to report disconnection to server: %v", t.name, err)
  127. }
  128. }
  129. }
  130. }
  131. // Restart restarts the tunnel (useful when config changes)
  132. func (t *SSHTunnel) Restart() error {
  133. t.Stop()
  134. time.Sleep(2 * time.Second)
  135. // Recreate stop channel
  136. t.mu.Lock()
  137. t.stopChan = make(chan struct{})
  138. t.mu.Unlock()
  139. return t.Start()
  140. }
  141. // UpdateConfig updates the tunnel configuration
  142. func (t *SSHTunnel) UpdateConfig(cfg *TunnelConfig) {
  143. t.mu.Lock()
  144. defer t.mu.Unlock()
  145. t.cfg = cfg
  146. }