ssh.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. package cli
  2. import (
  3. "crypto/ed25519"
  4. "crypto/rand"
  5. "encoding/pem"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "log"
  10. "net"
  11. "os"
  12. "path/filepath"
  13. "strings"
  14. "sync"
  15. "golang.org/x/crypto/ssh"
  16. "golang.org/x/crypto/ssh/terminal"
  17. "proxmox-router/internal/auth"
  18. "proxmox-router/internal/logging"
  19. "proxmox-router/internal/system"
  20. )
  21. const (
  22. DefaultSSHHostKeyPath = "/etc/proxmox-router/ssh_host_ed25519_key"
  23. )
  24. type SSHServer struct {
  25. addr string
  26. authMgr *auth.Manager
  27. executor *Executor
  28. listener net.Listener
  29. mu sync.Mutex
  30. stopChan chan struct{}
  31. running bool
  32. }
  33. func NewSSHServer(
  34. addr string,
  35. authMgr *auth.Manager,
  36. executor *Executor,
  37. ) *SSHServer {
  38. if addr == "" {
  39. addr = ":2222"
  40. }
  41. return &SSHServer{
  42. addr: addr,
  43. authMgr: authMgr,
  44. executor: executor,
  45. stopChan: make(chan struct{}),
  46. }
  47. }
  48. func (s *SSHServer) SetAddr(addr string) {
  49. s.mu.Lock()
  50. defer s.mu.Unlock()
  51. s.addr = addr
  52. }
  53. func (s *SSHServer) IsRunning() bool {
  54. s.mu.Lock()
  55. defer s.mu.Unlock()
  56. return s.running
  57. }
  58. func (s *SSHServer) Start() error {
  59. s.mu.Lock()
  60. if s.running {
  61. s.mu.Unlock()
  62. return nil
  63. }
  64. s.stopChan = make(chan struct{})
  65. signer, err := getOrCreateHostKey()
  66. if err != nil {
  67. s.mu.Unlock()
  68. return fmt.Errorf("failed to load SSH host key: %w", err)
  69. }
  70. config := &ssh.ServerConfig{
  71. PasswordCallback: func(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
  72. user := conn.User()
  73. sess, err := s.authMgr.Authenticate(user, string(password))
  74. if err != nil {
  75. logging.Log("auth,warning", "SSH login failed for user '%s' from %s: %v", user, conn.RemoteAddr(), err)
  76. return nil, errors.New("authentication failed")
  77. }
  78. logging.Log("auth,info", "SSH login successful for user '%s' from %s (role: %s)", user, conn.RemoteAddr(), sess.Role)
  79. return &ssh.Permissions{
  80. Extensions: map[string]string{
  81. "username": user,
  82. "role": string(sess.Role),
  83. },
  84. }, nil
  85. },
  86. }
  87. config.AddHostKey(signer)
  88. l, err := net.Listen("tcp", s.addr)
  89. if err != nil {
  90. s.mu.Unlock()
  91. return fmt.Errorf("failed to bind SSH server on %s: %w", s.addr, err)
  92. }
  93. s.listener = l
  94. s.running = true
  95. stopCh := s.stopChan
  96. s.mu.Unlock()
  97. log.Printf("[CLI-SSH] RouterOS SSH server listening on %s", s.addr)
  98. for {
  99. rawConn, err := l.Accept()
  100. if err != nil {
  101. select {
  102. case <-stopCh:
  103. return nil
  104. default:
  105. continue
  106. }
  107. }
  108. go s.handleSSHConn(rawConn, config)
  109. }
  110. }
  111. func (s *SSHServer) Stop() {
  112. s.mu.Lock()
  113. defer s.mu.Unlock()
  114. if !s.running {
  115. return
  116. }
  117. s.running = false
  118. close(s.stopChan)
  119. if s.listener != nil {
  120. _ = s.listener.Close()
  121. }
  122. log.Printf("[CLI-SSH] SSH server stopped")
  123. }
  124. func (s *SSHServer) handleSSHConn(rawConn net.Conn, config *ssh.ServerConfig) {
  125. defer rawConn.Close()
  126. sshConn, chans, reqs, err := ssh.NewServerConn(rawConn, config)
  127. if err != nil {
  128. return
  129. }
  130. defer sshConn.Close()
  131. // Discard out-of-band global requests
  132. go ssh.DiscardRequests(reqs)
  133. username := sshConn.Permissions.Extensions["username"]
  134. pubUser, err := s.authMgr.GetPublicUser(username)
  135. if err != nil || pubUser == nil {
  136. return
  137. }
  138. for newChan := range chans {
  139. if newChan.ChannelType() != "session" {
  140. _ = newChan.Reject(ssh.UnknownChannelType, "only session channels supported")
  141. continue
  142. }
  143. channel, requests, err := newChan.Accept()
  144. if err != nil {
  145. continue
  146. }
  147. go s.handleSession(channel, requests, pubUser)
  148. }
  149. }
  150. func (s *SSHServer) handleSession(channel ssh.Channel, requests <-chan *ssh.Request, user *auth.PublicUser) {
  151. defer channel.Close()
  152. term := terminal.NewTerminal(channel, "")
  153. term.SetSize(120, 40)
  154. for req := range requests {
  155. switch req.Type {
  156. case "pty-req":
  157. termWidth := 120
  158. termHeight := 40
  159. if len(req.Payload) >= 8 {
  160. // uint32 cols, uint32 rows
  161. cols := int(req.Payload[3]) | int(req.Payload[2])<<8
  162. rows := int(req.Payload[7]) | int(req.Payload[6])<<8
  163. if cols > 20 && rows > 5 {
  164. termWidth = cols
  165. termHeight = rows
  166. }
  167. }
  168. term.SetSize(termWidth, termHeight)
  169. _ = req.Reply(true, nil)
  170. case "window-change":
  171. if len(req.Payload) >= 8 {
  172. cols := int(req.Payload[3]) | int(req.Payload[2])<<8
  173. rows := int(req.Payload[7]) | int(req.Payload[6])<<8
  174. if cols > 20 && rows > 5 {
  175. term.SetSize(cols, rows)
  176. }
  177. }
  178. _ = req.Reply(true, nil)
  179. case "shell":
  180. _ = req.Reply(true, nil)
  181. s.runInteractiveShell(term, user)
  182. return
  183. case "exec":
  184. _ = req.Reply(true, nil)
  185. cmdStr := ""
  186. if len(req.Payload) > 4 {
  187. cmdLen := int(req.Payload[3]) | int(req.Payload[2])<<8
  188. if 4+cmdLen <= len(req.Payload) {
  189. cmdStr = string(req.Payload[4 : 4+cmdLen])
  190. } else {
  191. cmdStr = string(req.Payload[4:])
  192. }
  193. }
  194. output, _ := s.executor.Execute(cmdStr, user)
  195. _, _ = channel.Write([]byte(output))
  196. // Send exit status 0
  197. _, _ = channel.SendRequest("exit-status", false, []byte{0, 0, 0, 0})
  198. return
  199. default:
  200. _ = req.Reply(false, nil)
  201. }
  202. }
  203. }
  204. func (s *SSHServer) runInteractiveShell(term *terminal.Terminal, user *auth.PublicUser) {
  205. st, _ := system.GetStatus()
  206. hostname := "DebTik"
  207. if st != nil && st.Hostname != "" {
  208. hostname = st.Hostname
  209. }
  210. banner := Banner("1.0", user.Username, string(user.Role), hostname)
  211. _, _ = term.Write([]byte(banner))
  212. prompt := fmt.Sprintf("[%s%s%s@%s%s%s] > ", ColorGreen+ColorBold, user.Username, ColorReset, ColorCyan+ColorBold, hostname, ColorReset)
  213. term.SetPrompt(prompt)
  214. for {
  215. line, err := term.ReadLine()
  216. if err != nil {
  217. if err == io.EOF {
  218. break
  219. }
  220. return
  221. }
  222. trimmed := strings.TrimSpace(line)
  223. if trimmed == "" {
  224. continue
  225. }
  226. if trimmed == "password" {
  227. currPass, err := term.ReadPassword("Current password: ")
  228. if err != nil {
  229. return
  230. }
  231. newPass, err := term.ReadPassword("New password: ")
  232. if err != nil {
  233. return
  234. }
  235. if len(newPass) < 4 {
  236. _, _ = term.Write([]byte(ColorRed + "Password must be at least 4 characters\n" + ColorReset))
  237. continue
  238. }
  239. confirmPass, err := term.ReadPassword("Confirm new password: ")
  240. if err != nil {
  241. return
  242. }
  243. if newPass != confirmPass {
  244. _, _ = term.Write([]byte(ColorRed + "Passwords do not match\n" + ColorReset))
  245. continue
  246. }
  247. if err := s.authMgr.ChangePassword(user.Username, currPass, newPass, false); err != nil {
  248. _, _ = term.Write([]byte(ColorRed + "Error: " + err.Error() + "\n" + ColorReset))
  249. continue
  250. }
  251. _, _ = term.Write([]byte(ColorGreen + "Password changed successfully!\n" + ColorReset))
  252. continue
  253. }
  254. output, shouldExit := s.executor.Execute(trimmed, user)
  255. if output != "" {
  256. _, _ = term.Write([]byte(output))
  257. }
  258. if shouldExit {
  259. break
  260. }
  261. }
  262. }
  263. func getOrCreateHostKey() (ssh.Signer, error) {
  264. keyPath := DefaultSSHHostKeyPath
  265. if data, err := os.ReadFile(keyPath); err == nil {
  266. signer, err := ssh.ParsePrivateKey(data)
  267. if err == nil {
  268. return signer, nil
  269. }
  270. }
  271. // Generate persistent ED25519 host key
  272. _ = os.MkdirAll(filepath.Dir(keyPath), 0755)
  273. _, priv, err := ed25519.GenerateKey(rand.Reader)
  274. if err != nil {
  275. return nil, err
  276. }
  277. privBytes, err := ssh.MarshalPrivateKey(priv, "")
  278. if err != nil {
  279. return nil, err
  280. }
  281. pemBlock := pem.EncodeToMemory(privBytes)
  282. _ = os.WriteFile(keyPath, pemBlock, 0600)
  283. return ssh.ParsePrivateKey(pemBlock)
  284. }