//go:build !js package mosh import ( "crypto/rand" "encoding/base64" "encoding/binary" "fmt" "io" "net" "os" "os/exec" "sync" "sync/atomic" "time" "github.com/unixshells/vt-go" "github.com/creack/pty" ) const ( // Server gives up waiting for client after this. associationTimeout = 60 * time.Second // Network timeout for idle sessions. defaultNetworkTimeout = 24 * time.Hour // Default terminal size. defaultCols = 80 defaultRows = 24 ) // Server is a native mosh server. It listens on UDP, runs a shell in a PTY, // bridges data through the SSP transport, and diffs terminal framebuffers // to produce HostMessage updates for the client. type Server struct { key []byte ocb *OCB port int conn *net.UDPConn ptmx *os.File cmd *exec.Cmd shell string cols int rows int // Transport handles SSP sequencing, fragmentation, and crypto. transport *Transport // VT emulator and framebuffer state for CUP-based diffing. emu *vt.Emulator baseFB *Framebuffer // what client has (last-acked) sentFB *Framebuffer // what we last sent (pending ack) curVisible atomic.Bool // Remote client address — set on first received datagram. mu sync.Mutex clientAddr *net.UDPAddr started chan struct{} // closed when PTY is running done chan struct{} } // GenerateKey creates a random 128-bit mosh key and returns it as base64. func GenerateKey() ([]byte, string, error) { key := make([]byte, 16) if _, err := rand.Read(key); err != nil { return nil, "", err } return key, base64.StdEncoding.EncodeToString(key), nil } // NewServer creates a native mosh server. // It binds a UDP port, generates a key, and is ready to serve. func NewServer(shell string, portLow, portHigh int) (*Server, error) { key, _, err := GenerateKey() if err != nil { return nil, err } ocb, err := NewOCB(key) if err != nil { return nil, err } if shell == "" { shell = os.Getenv("SHELL") if shell == "" { shell = "/bin/sh" } } conn, port, err := BindUDP(portLow, portHigh) if err != nil { return nil, err } return &Server{ key: key, ocb: ocb, port: port, conn: conn, shell: shell, cols: defaultCols, rows: defaultRows, transport: NewTransport(ocb, true), started: make(chan struct{}), done: make(chan struct{}), }, nil } // Port returns the UDP port the server is listening on. func (s *Server) Port() int { return s.port } // KeyBase64 returns the mosh key as a base64 string (22 chars, no padding). func (s *Server) KeyBase64() string { encoded := base64.StdEncoding.EncodeToString(s.key) for len(encoded) > 0 && encoded[len(encoded)-1] == '=' { encoded = encoded[:len(encoded)-1] } return encoded } // ConnectLine returns the MOSH CONNECT line that clients parse. func (s *Server) ConnectLine() string { return fmt.Sprintf("MOSH CONNECT %d %s", s.port, s.KeyBase64()) } // Serve starts the shell and event loop. Blocks until the session ends. func (s *Server) Serve() error { s.cmd = exec.Command(s.shell) s.cmd.Env = append(os.Environ(), "TERM=xterm-256color", "MOSH_SERVER_NETWORK_TMOUT=86400", ) var err error s.ptmx, err = pty.Start(s.cmd) if err != nil { s.conn.Close() return fmt.Errorf("pty: %w", err) } close(s.started) pty.Setsize(s.ptmx, &pty.Winsize{ Rows: uint16(s.rows), Cols: uint16(s.cols), }) s.emu = vt.NewEmulator(s.cols, s.rows) s.curVisible.Store(true) s.emu.SetCallbacks(vt.Callbacks{ CursorVisibility: func(visible bool) { s.curVisible.Store(visible) }, }) s.baseFB = NewFramebuffer(s.cols, s.rows) var wg sync.WaitGroup wg.Add(3) // PTY reader: reads shell output into pending buffer. ptyOutput := make(chan []byte, 64) go func() { defer wg.Done() s.readPTY(ptyOutput) }() // UDP receiver: decrypts datagrams, feeds to transport. userInput := make(chan UserInstruction, 64) go func() { defer wg.Done() s.recvUDP(userInput) }() // Main loop: process input, diff framebuffer, send via transport. go func() { defer wg.Done() s.mainLoop(ptyOutput, userInput) }() err = s.cmd.Wait() close(s.done) s.ptmx.Close() s.conn.Close() wg.Wait() return err } // Done returns a channel that is closed when the server shuts down. func (s *Server) Done() <-chan struct{} { return s.done } // ServeRW runs the event loop using an external io.ReadWriteCloser instead // of spawning a shell in a PTY. The caller provides terminal I/O through rw // and a resize callback. When rw is closed or reaches EOF, the server shuts down. func (s *Server) ServeRW(rw io.ReadWriteCloser, resize func(cols, rows uint16)) error { close(s.started) s.emu = vt.NewEmulator(s.cols, s.rows) s.curVisible.Store(true) s.emu.SetCallbacks(vt.Callbacks{ CursorVisibility: func(visible bool) { s.curVisible.Store(visible) }, }) s.baseFB = NewFramebuffer(s.cols, s.rows) var wg sync.WaitGroup wg.Add(3) ioOutput := make(chan []byte, 64) go func() { defer wg.Done() s.readIO(rw, ioOutput) }() userInput := make(chan UserInstruction, 64) go func() { defer wg.Done() s.recvUDP(userInput) }() go func() { defer wg.Done() s.mainLoopRW(rw, resize, ioOutput, userInput) }() <-s.done rw.Close() s.conn.Close() wg.Wait() return nil } func (s *Server) readIO(r io.Reader, out chan<- []byte) { buf := make([]byte, 8192) for { n, err := r.Read(buf) if n > 0 { data := make([]byte, n) copy(data, buf[:n]) select { case out <- data: case <-s.done: return } } if err != nil { select { case <-s.done: default: close(s.done) } return } } } func (s *Server) mainLoopRW(rw io.Writer, resize func(cols, rows uint16), ioOutput <-chan []byte, userInput <-chan UserInstruction) { // Wait for the first client datagram. If no client connects within // 60 seconds, this was a failed setup — shut down to release resources. deadline := time.After(associationTimeout) for { s.mu.Lock() addr := s.clientAddr s.mu.Unlock() if addr != nil { break } select { case <-s.done: return case <-deadline: close(s.done) return case <-time.After(100 * time.Millisecond): } } ticker := time.NewTicker(tickInterval) defer ticker.Stop() dirty := false for { select { case <-s.done: return case data, ok := <-ioOutput: if !ok { return } s.emu.Write(data) dirty = true case ui := <-userInput: if len(ui.Keys) > 0 { rw.Write(ui.Keys) } if ui.Width > 0 && ui.Height > 0 { s.mu.Lock() s.cols = int(ui.Width) s.rows = int(ui.Height) s.mu.Unlock() if resize != nil { resize(uint16(ui.Width), uint16(ui.Height)) } s.emu.Resize(int(ui.Width), int(ui.Height)) s.baseFB = NewFramebuffer(int(ui.Width), int(ui.Height)) s.sentFB = nil dirty = true } case <-ticker.C: if s.sentFB != nil && s.transport.AckedByRemote() >= s.transport.SentNum() { s.baseFB = s.sentFB s.sentFB = nil } if dirty { currentFB := SnapshotEmulator(s.emu, s.curVisible.Load()) diffBytes := currentFB.Diff(s.baseFB) if len(diffBytes) > 0 { hi := HostInstruction{Hoststring: diffBytes, EchoAckNum: -1} s.transport.SetPending(marshalHostMessage([]HostInstruction{hi})) s.sentFB = currentFB } dirty = false } datagrams := s.transport.Tick() s.sendDatagrams(datagrams) } } } // Close shuts down the server. func (s *Server) Close() { // If Serve() was never called, just close the socket. select { case <-s.done: return case <-s.started: // PTY is running — kill it. default: // Serve() never called. Close socket and unblock any future Serve(). s.conn.Close() return } if s.cmd.Process != nil { s.cmd.Process.Kill() } s.conn.Close() if s.ptmx != nil { s.ptmx.Close() } } // mainLoop is the SSP event loop. It feeds PTY output to a VT emulator, // snapshots the framebuffer, diffs against the last-acked state, and // sends CUP-based updates via the transport. func (s *Server) mainLoop(ptyOutput <-chan []byte, userInput <-chan UserInstruction) { // Wait for the first client datagram. If no client connects within // 60 seconds, this was a failed setup — shut down to release resources. deadline := time.After(associationTimeout) for { s.mu.Lock() addr := s.clientAddr s.mu.Unlock() if addr != nil { break } select { case <-s.done: return case <-deadline: close(s.done) return case <-time.After(100 * time.Millisecond): } } ticker := time.NewTicker(tickInterval) defer ticker.Stop() dirty := false for { select { case <-s.done: return case data, ok := <-ptyOutput: if !ok { return } s.emu.Write(data) dirty = true case ui := <-userInput: if len(ui.Keys) > 0 { s.ptmx.Write(ui.Keys) } if ui.Width > 0 && ui.Height > 0 { s.mu.Lock() s.cols = int(ui.Width) s.rows = int(ui.Height) s.mu.Unlock() pty.Setsize(s.ptmx, &pty.Winsize{ Cols: uint16(ui.Width), Rows: uint16(ui.Height), }) s.emu.Resize(int(ui.Width), int(ui.Height)) s.baseFB = NewFramebuffer(int(ui.Width), int(ui.Height)) s.sentFB = nil dirty = true } case <-ticker.C: // Advance base when client acks all pending. if s.sentFB != nil && s.transport.AckedByRemote() >= s.transport.SentNum() { s.baseFB = s.sentFB s.sentFB = nil } if dirty { currentFB := SnapshotEmulator(s.emu, s.curVisible.Load()) diffBytes := currentFB.Diff(s.baseFB) if len(diffBytes) > 0 { hi := HostInstruction{Hoststring: diffBytes, EchoAckNum: -1} s.transport.SetPending(marshalHostMessage([]HostInstruction{hi})) s.sentFB = currentFB } dirty = false } datagrams := s.transport.Tick() s.sendDatagrams(datagrams) } } } // readPTY reads from the PTY and sends data to the channel. func (s *Server) readPTY(out chan<- []byte) { buf := make([]byte, 8192) for { select { case <-s.done: return default: } s.ptmx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) n, err := s.ptmx.Read(buf) if n > 0 { data := make([]byte, n) copy(data, buf[:n]) select { case out <- data: case <-s.done: return } } if err != nil && !os.IsTimeout(err) { return } } } // recvUDP reads encrypted datagrams, decrypts via transport, parses UserMessage. func (s *Server) recvUDP(out chan<- UserInstruction) { buf := make([]byte, maxPayload+64) for { select { case <-s.done: return default: } s.conn.SetReadDeadline(time.Now().Add(1 * time.Second)) n, addr, err := s.conn.ReadFromUDP(buf) if err != nil { if os.IsTimeout(err) { if time.Since(s.transport.LastRecv()) > defaultNetworkTimeout { return } continue } return } if n < minDatagram { continue } data := make([]byte, n) copy(data, buf[:n]) // Feed to transport — only update clientAddr on successful decrypt. diff := s.transport.Recv(data) s.mu.Lock() s.clientAddr = addr s.mu.Unlock() if diff == nil { continue } // Parse UserMessage protobuf. instrs, err := unmarshalUserMessage(diff) if err != nil { continue } for _, ui := range instrs { select { case out <- ui: case <-s.done: return } } } } // sendDatagrams sends wire datagrams to the client. func (s *Server) sendDatagrams(datagrams [][]byte) { s.mu.Lock() addr := s.clientAddr s.mu.Unlock() if addr == nil { return } for _, dg := range datagrams { s.conn.WriteToUDP(dg, addr) } } // BindUDP binds a UDP socket on the first available port in [low, high]. func BindUDP(low, high int) (*net.UDPConn, int, error) { if low == 0 && high == 0 { conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero}) if err != nil { return nil, 0, err } return conn, conn.LocalAddr().(*net.UDPAddr).Port, nil } if high < low { high = low } for port := low; port <= high; port++ { conn, err := net.ListenUDP("udp4", &net.UDPAddr{ IP: net.IPv4zero, Port: port, }) if err != nil { continue } return conn, port, nil } return nil, 0, fmt.Errorf("no available UDP port in range %d-%d", low, high) } // WriteTo implements io.WriterTo for streaming output to an SSH channel. func (s *Server) WriteTo(w io.Writer) (int64, error) { line := s.ConnectLine() + "\n" n, err := w.Write([]byte(line)) return int64(n), err } // sendToClient encrypts and sends a raw payload datagram to the client. // Used by legacy tests that don't go through the Transport. func (s *Server) sendToClient(payload []byte) { s.mu.Lock() addr := s.clientAddr if addr == nil { s.mu.Unlock() return } s.transport.mu.Lock() s.transport.seqOut++ seq := s.transport.seqOut lastTS := s.transport.lastTS s.transport.mu.Unlock() s.mu.Unlock() dirSeq := dirToClient | (seq & seqMask) var dirSeqBytes [8]byte binary.BigEndian.PutUint64(dirSeqBytes[:], dirSeq) var nonce [12]byte copy(nonce[4:], dirSeqBytes[:]) ts := uint16(time.Now().UnixMilli() & 0xffff) plaintext := make([]byte, 4+len(payload)) binary.BigEndian.PutUint16(plaintext[0:], ts) binary.BigEndian.PutUint16(plaintext[2:], lastTS) if len(payload) > 0 { copy(plaintext[4:], payload) } tagAndCT := s.ocb.Encrypt(nonce[:], plaintext) wire := make([]byte, 8+len(tagAndCT)) copy(wire[:8], dirSeqBytes[:]) copy(wire[8:], tagAndCT) s.conn.WriteToUDP(wire, addr) }