ssh.go 7.2 KB

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