- client.go: track last output oldNum, replace instead of append when same base - transport.go: reject diffs that do not chain from latest acknowledged state (oldNum != ackNum), preventing stale ANSI sequences from corrupting output
492 lines
12 KiB
Go
492 lines
12 KiB
Go
package mosh
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/zlib"
|
|
"encoding/binary"
|
|
"io"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Transport implements the mosh State Synchronization Protocol (SSP).
|
|
//
|
|
// It manages sequence numbering, acknowledgements, retransmission timing,
|
|
// and the fragment/encrypt/decrypt pipeline. Both client and server use
|
|
// the same Transport with different direction bits.
|
|
//
|
|
// The caller provides state diffs (server: terminal output, client: keystrokes)
|
|
// and receives remote state updates.
|
|
type Transport struct {
|
|
mu sync.Mutex
|
|
|
|
ocb *OCB
|
|
toRemote uint64 // direction bit for outgoing (dirToServer or dirToClient)
|
|
toLocal uint64 // direction bit for incoming
|
|
|
|
// Outgoing state (SSP §3).
|
|
sentNum uint64 // newest state we've sent (new_num)
|
|
ackedByRemote uint64 // newest state the remote has acknowledged
|
|
pendingDiff []byte // diff payload waiting to be sent
|
|
diffSent bool // true = pendingDiff has been sent at least once
|
|
diffOldNum uint64 // locked oldNum for all diffs until base advances
|
|
hasPendingBase bool // true = diffOldNum is locked
|
|
pendingDataAck bool // true = send ack ASAP (received data, not just ack)
|
|
|
|
// Incoming state — list of received state nums for old_num validation.
|
|
receivedNums []uint64 // ordered list of state nums we have
|
|
ackNum uint64 // latest received state num
|
|
sentAckNum uint64 // last ackNum we actually sent on wire
|
|
throwawayNum uint64 // oldest state we still hold
|
|
lastRecvOldNum uint64 // oldNum from most recently received diff
|
|
lastRecvNewNum uint64 // newNum from most recently received diff
|
|
|
|
// Sequence counter for the crypto layer (independent of SSP state numbering).
|
|
seqOut uint64
|
|
seqInMax uint64
|
|
seqInMaxSet bool // false until first datagram received
|
|
|
|
// Timestamps.
|
|
lastSend time.Time
|
|
lastRecv time.Time
|
|
lastTS uint16 // last remote timestamp for echo
|
|
|
|
// RTT estimation (Jacobson/Karels).
|
|
srtt time.Duration
|
|
rttvar time.Duration
|
|
rto time.Duration
|
|
rttInit bool
|
|
|
|
// Fragment assembler for incoming.
|
|
assembler FragmentAssembler
|
|
|
|
// Latch capability negotiation.
|
|
localCaps []byte
|
|
remoteCaps []byte
|
|
}
|
|
|
|
const (
|
|
initialRTO = 1000 * time.Millisecond
|
|
minRTO = 250 * time.Millisecond
|
|
maxRTO = 10 * time.Second
|
|
)
|
|
|
|
// NewTransport creates a transport. isServer determines direction bits.
|
|
func NewTransport(ocb *OCB, isServer bool) *Transport {
|
|
t := &Transport{
|
|
ocb: ocb,
|
|
rto: initialRTO,
|
|
lastSend: time.Now(),
|
|
lastRecv: time.Now(),
|
|
receivedNums: []uint64{0}, // start with state 0
|
|
}
|
|
if isServer {
|
|
t.toRemote = dirToClient
|
|
t.toLocal = dirToServer
|
|
} else {
|
|
t.toRemote = dirToServer
|
|
t.toLocal = dirToClient
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (t *Transport) SetCaps(caps []byte) {
|
|
t.mu.Lock()
|
|
t.localCaps = caps
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
func (t *Transport) RemoteCaps() []byte {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.remoteCaps
|
|
}
|
|
|
|
func (t *Transport) HasCap(bit byte) bool {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if len(t.localCaps) == 0 || len(t.remoteCaps) == 0 {
|
|
return false
|
|
}
|
|
idx := 0
|
|
if idx >= len(t.localCaps) || idx >= len(t.remoteCaps) {
|
|
return false
|
|
}
|
|
return (t.localCaps[idx] & t.remoteCaps[idx] & bit) != 0
|
|
}
|
|
|
|
// ForceNextSend forces the next tick to send, even with no pending diff.
|
|
func (t *Transport) ForceNextSend() {
|
|
t.mu.Lock()
|
|
t.lastSend = time.Time{} // zero time, always expired
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
// SetPending sets the diff payload to send on the next tick.
|
|
func (t *Transport) SetPending(diff []byte) {
|
|
t.mu.Lock()
|
|
if len(diff) > 0 {
|
|
t.diffSent = false
|
|
}
|
|
t.pendingDiff = diff
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
// Tick produces outgoing wire datagrams if it's time to send.
|
|
// Returns nil if nothing to send.
|
|
func (t *Transport) Tick() [][]byte {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
now := time.Now()
|
|
|
|
// Decide if we should send.
|
|
haveDiff := len(t.pendingDiff) > 0
|
|
haveNewDiff := haveDiff && !t.diffSent
|
|
needAck := t.ackNum > t.sentAckNum
|
|
sinceLastSend := now.Sub(t.lastSend)
|
|
expired := sinceLastSend >= t.rto
|
|
urgentAck := t.pendingDataAck
|
|
|
|
shouldSend := haveNewDiff || needAck || expired || urgentAck
|
|
if !shouldSend {
|
|
return nil
|
|
}
|
|
|
|
// Build TransportInstruction.
|
|
if haveNewDiff {
|
|
t.sentNum++
|
|
t.diffSent = true
|
|
if !t.hasPendingBase {
|
|
t.diffOldNum = t.ackedByRemote
|
|
t.hasPendingBase = true
|
|
}
|
|
}
|
|
t.pendingDataAck = false
|
|
|
|
oldNum := t.ackedByRemote
|
|
if haveDiff {
|
|
oldNum = t.diffOldNum // use locked oldNum for diff retransmissions
|
|
}
|
|
|
|
ti := TransportInstruction{
|
|
ProtocolVersion: 2,
|
|
OldNum: oldNum,
|
|
NewNum: t.sentNum,
|
|
AckNum: t.ackNum,
|
|
ThrowawayNum: 0, // client doesn't tell server to throw away states
|
|
Diff: t.pendingDiff,
|
|
LatchCaps: t.localCaps,
|
|
}
|
|
t.sentAckNum = t.ackNum
|
|
// Do NOT nil pendingDiff — keep for retransmission until server acks.
|
|
|
|
// Marshal → compress → fragment → encrypt.
|
|
pbData := ti.Marshal()
|
|
compressed := zlibCompress(pbData)
|
|
frags := Fragmentize(t.sentNum, compressed)
|
|
|
|
var datagrams [][]byte
|
|
for i := range frags {
|
|
wire := t.encryptFragment(&frags[i], now)
|
|
datagrams = append(datagrams, wire)
|
|
}
|
|
|
|
t.lastSend = now
|
|
return datagrams
|
|
}
|
|
|
|
// Recv processes an incoming wire datagram.
|
|
// Returns the diff payload if a complete message was reassembled, or nil.
|
|
func (t *Transport) Recv(wire []byte) []byte {
|
|
if len(wire) < minDatagram {
|
|
return nil
|
|
}
|
|
|
|
dirSeq := binary.BigEndian.Uint64(wire[:8])
|
|
|
|
// Verify direction.
|
|
if dirSeq&dirToClient != t.toLocal&dirToClient {
|
|
return nil
|
|
}
|
|
|
|
seq := dirSeq & seqMask
|
|
|
|
t.mu.Lock()
|
|
if t.seqInMaxSet && seq <= t.seqInMax {
|
|
t.mu.Unlock()
|
|
return nil // replay
|
|
}
|
|
t.mu.Unlock()
|
|
|
|
// Decrypt.
|
|
var nonce [12]byte
|
|
copy(nonce[4:], wire[:8])
|
|
plaintext := t.ocb.Decrypt(nonce[:], wire[8:])
|
|
if plaintext == nil {
|
|
return nil
|
|
}
|
|
|
|
// Parse timestamp header (4 bytes).
|
|
if len(plaintext) < 4 {
|
|
return nil
|
|
}
|
|
remoteTS := binary.BigEndian.Uint16(plaintext[0:])
|
|
// plaintext[2:4] is timestamp_reply — used for RTT.
|
|
tsReply := binary.BigEndian.Uint16(plaintext[2:])
|
|
payload := plaintext[4:]
|
|
|
|
// Update crypto sequence.
|
|
t.mu.Lock()
|
|
t.seqInMax = seq
|
|
t.seqInMaxSet = true
|
|
t.lastRecv = time.Now()
|
|
t.lastTS = remoteTS
|
|
t.mu.Unlock()
|
|
|
|
// RTT estimation from timestamp echo.
|
|
if tsReply != 0 {
|
|
t.updateRTT(tsReply)
|
|
}
|
|
|
|
// Parse fragment.
|
|
if len(payload) < fragmentHeaderSize {
|
|
// Heartbeat with no fragment — that's fine.
|
|
return nil
|
|
}
|
|
frag, err := UnmarshalFragment(payload)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
// Reassemble.
|
|
t.mu.Lock()
|
|
msg := t.assembler.Add(frag)
|
|
t.mu.Unlock()
|
|
if msg == nil {
|
|
return nil
|
|
}
|
|
|
|
// Decompress → parse TransportInstruction.
|
|
decompressed := zlibDecompress(msg)
|
|
if decompressed == nil {
|
|
return nil
|
|
}
|
|
var ti TransportInstruction
|
|
if err := ti.Unmarshal(decompressed); err != nil {
|
|
return nil
|
|
}
|
|
|
|
if len(ti.LatchCaps) > 0 {
|
|
t.mu.Lock()
|
|
t.remoteCaps = ti.LatchCaps
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
// Process SSP fields (matching upstream mosh recv logic).
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
// Process ack from remote.
|
|
if ti.AckNum > t.ackedByRemote {
|
|
t.ackedByRemote = ti.AckNum
|
|
if t.ackedByRemote >= t.sentNum && t.pendingDiff != nil {
|
|
t.pendingDiff = nil
|
|
t.diffSent = false
|
|
t.hasPendingBase = false
|
|
}
|
|
}
|
|
|
|
// Check if we already have new_num (dedup).
|
|
for _, n := range t.receivedNums {
|
|
if n == ti.NewNum {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Reject diffs that do not chain from our latest acknowledged state.
|
|
// Mosh diffs MUST be sequential: each diff's oldNum must equal the
|
|
// newNum of the most recently accepted diff. Accepting diffs from
|
|
// stale bases (oldNum != ackNum) causes overlapping ANSI sequences
|
|
// that produce character ghosting and garbled output.
|
|
if ti.OldNum != t.ackNum {
|
|
return nil
|
|
}
|
|
|
|
// Process throwaway.
|
|
if ti.ThrowawayNum > t.throwawayNum {
|
|
t.throwawayNum = ti.ThrowawayNum
|
|
filtered := t.receivedNums[:0]
|
|
for _, n := range t.receivedNums {
|
|
if n >= t.throwawayNum {
|
|
filtered = append(filtered, n)
|
|
}
|
|
}
|
|
t.receivedNums = filtered
|
|
}
|
|
|
|
// Track oldNum/newNum for state management.
|
|
t.lastRecvOldNum = ti.OldNum
|
|
t.lastRecvNewNum = ti.NewNum
|
|
|
|
// Add new state.
|
|
t.receivedNums = append(t.receivedNums, ti.NewNum)
|
|
|
|
// Bound the list.
|
|
if len(t.receivedNums) > 128 {
|
|
t.receivedNums = t.receivedNums[1:]
|
|
}
|
|
|
|
// Update ack num to latest received state.
|
|
t.ackNum = ti.NewNum
|
|
|
|
// Trigger immediate ack when we receive data.
|
|
if len(ti.Diff) > 0 {
|
|
t.pendingDataAck = true
|
|
}
|
|
|
|
return ti.Diff
|
|
}
|
|
|
|
// AckedByRemote returns the highest state number the remote has acked.
|
|
func (t *Transport) AckedByRemote() uint64 {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.ackedByRemote
|
|
}
|
|
|
|
// SentNum returns the current sent state number.
|
|
func (t *Transport) SentNum() uint64 {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.sentNum
|
|
}
|
|
|
|
// LastRecvOldNum returns the oldNum from the most recently received diff.
|
|
func (t *Transport) LastRecvOldNum() uint64 {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.lastRecvOldNum
|
|
}
|
|
|
|
// LastRecvNewNum returns the newNum from the most recently received diff.
|
|
func (t *Transport) LastRecvNewNum() uint64 {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.lastRecvNewNum
|
|
}
|
|
|
|
// ThrowawayNum returns the server's throwaway number — states below this
|
|
// are no longer referenced by the server and can be safely pruned.
|
|
func (t *Transport) ThrowawayNum() uint64 {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.throwawayNum
|
|
}
|
|
|
|
// LastRecv returns the time of the last received datagram.
|
|
func (t *Transport) LastRecv() time.Time {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.lastRecv
|
|
}
|
|
|
|
// RTO returns the current retransmission timeout.
|
|
func (t *Transport) RTO() time.Duration {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.rto
|
|
}
|
|
|
|
// encryptFragment encrypts a fragment and wraps it in the mosh wire format.
|
|
// Caller holds t.mu.
|
|
func (t *Transport) encryptFragment(f *Fragment, now time.Time) []byte {
|
|
t.seqOut++
|
|
seq := t.seqOut
|
|
|
|
dirSeq := t.toRemote | (seq & seqMask)
|
|
var dirSeqBytes [8]byte
|
|
binary.BigEndian.PutUint64(dirSeqBytes[:], dirSeq)
|
|
|
|
var nonce [12]byte
|
|
copy(nonce[4:], dirSeqBytes[:])
|
|
|
|
// Plaintext: [timestamp:2][timestamp_reply:2][fragment]
|
|
fragWire := f.Marshal()
|
|
ts := uint16(now.UnixMilli() & 0xffff)
|
|
plaintext := make([]byte, 4+len(fragWire))
|
|
binary.BigEndian.PutUint16(plaintext[0:], ts)
|
|
binary.BigEndian.PutUint16(plaintext[2:], t.lastTS)
|
|
copy(plaintext[4:], fragWire)
|
|
|
|
tagAndCT := t.ocb.Encrypt(nonce[:], plaintext)
|
|
|
|
wire := make([]byte, 8+len(tagAndCT))
|
|
copy(wire[:8], dirSeqBytes[:])
|
|
copy(wire[8:], tagAndCT)
|
|
return wire
|
|
}
|
|
|
|
// updateRTT updates the RTT estimate from a timestamp echo.
|
|
func (t *Transport) updateRTT(tsReply uint16) {
|
|
now16 := uint16(time.Now().UnixMilli() & 0xffff)
|
|
// Compute RTT in milliseconds, handling 16-bit wraparound.
|
|
rttMS := int(now16) - int(tsReply)
|
|
if rttMS < 0 {
|
|
rttMS += 65536
|
|
}
|
|
if rttMS > 30000 {
|
|
return // implausible
|
|
}
|
|
rtt := time.Duration(rttMS) * time.Millisecond
|
|
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
|
|
if !t.rttInit {
|
|
t.srtt = rtt
|
|
t.rttvar = rtt / 2
|
|
t.rttInit = true
|
|
} else {
|
|
// RFC 6298 Jacobson/Karels.
|
|
delta := t.srtt - rtt
|
|
if delta < 0 {
|
|
delta = -delta
|
|
}
|
|
t.rttvar = (3*t.rttvar + delta) / 4
|
|
t.srtt = (7*t.srtt + rtt) / 8
|
|
}
|
|
|
|
t.rto = t.srtt + 4*t.rttvar
|
|
if t.rto < minRTO {
|
|
t.rto = minRTO
|
|
}
|
|
if t.rto > maxRTO {
|
|
t.rto = maxRTO
|
|
}
|
|
}
|
|
|
|
// zlibCompress compresses data with zlib (default level).
|
|
func zlibCompress(data []byte) []byte {
|
|
var buf bytes.Buffer
|
|
w := zlib.NewWriter(&buf)
|
|
w.Write(data)
|
|
w.Close()
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// zlibDecompress decompresses zlib data. Returns nil on error.
|
|
// Limits output to 1 MiB to prevent decompression bombs.
|
|
func zlibDecompress(data []byte) []byte {
|
|
r, err := zlib.NewReader(bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer r.Close()
|
|
out, err := io.ReadAll(io.LimitReader(r, 1<<20))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|