// Copyright 2026 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package ssh import ( "encoding/binary" "errors" "fmt" "io" "golang.org/x/crypto/cryptobyte" ) const ( muxProtocolVersion = 4 muxMsgHello = 0x00000001 muxCProxy = 0x1000000f muxSProxy = 0x8000000f ) const controlProxyRequestID = 0 // handshakeControlProxy attempts to establish a transport connection with an // OpenSSH ControlMaster socket in proxy mode. For details see: // https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.mux func handshakeControlProxy(rw io.ReadWriteCloser) (connTransport, error) { if err := controlProxyWritePacket(rw, func(b *cryptobyte.Builder) { b.AddUint32(muxMsgHello) b.AddUint32(muxProtocolVersion) }); err != nil { return nil, fmt.Errorf("mux hello write failed: %w", err) } if err := controlProxyWritePacket(rw, func(b *cryptobyte.Builder) { b.AddUint32(muxCProxy) b.AddUint32(controlProxyRequestID) }); err != nil { return nil, fmt.Errorf("mux client proxy write failed: %w", err) } messageType, body, err := controlProxyReadMessage(rw) if err != nil { return nil, fmt.Errorf("mux hello read failed: %w", err) } if messageType != muxMsgHello { return nil, fmt.Errorf("expected hello response, got %v", messageType) } var v uint32 if !body.ReadUint32(&v) { return nil, errors.New("EOF reading mux protocol version") } if v != muxProtocolVersion { return nil, fmt.Errorf("mux server has unsupported version %v", v) } messageType, body, err = controlProxyReadMessage(rw) if err != nil { return nil, fmt.Errorf("mux server proxy read failed: %w", err) } if messageType != muxSProxy { return nil, fmt.Errorf("expected server proxy response, got %v", messageType) } var reqID uint32 if !body.ReadUint32(&reqID) { return nil, errors.New("EOF reading request id") } if reqID != controlProxyRequestID { return nil, fmt.Errorf("expected request id %v, got %v", controlProxyRequestID, reqID) } return &controlProxyTransport{rw}, nil } // controlProxyTransport implements the connTransport interface for // ControlMaster connections. Each controlMessage has zero length padding and // no MAC. type controlProxyTransport struct { rw io.ReadWriteCloser } func (p *controlProxyTransport) Close() error { return p.rw.Close() } func (p *controlProxyTransport) writePacket(controlMessage []byte) error { return controlProxyWritePacket(p.rw, func(b *cryptobyte.Builder) { b.AddUint8(0) // Padding length. b.AddBytes(controlMessage) }) } func (p *controlProxyTransport) readPacket() ([]byte, error) { buf, err := controlProxyReadPacket(p.rw) if err != nil { return nil, fmt.Errorf("ssh: error reading control message: %w", err) } // Discard the padding length. if len(buf) < 1 { return nil, errors.New("ssh: EOF reading padding length") } if buf[0] != 0 { return nil, errors.New("ssh: unexpected non-zero padding in control message") } return buf[1:], nil } func (p *controlProxyTransport) getAlgorithms() NegotiatedAlgorithms { return NegotiatedAlgorithms{} } func (p *controlProxyTransport) getSessionID() []byte { return nil } func (p *controlProxyTransport) waitSession() error { return nil } func controlProxyWritePacket(w io.Writer, f cryptobyte.BuilderContinuation) error { var buf []byte b := cryptobyte.NewBuilder(buf) b.AddUint32LengthPrefixed(f) out, err := b.Bytes() if err != nil { return err } _, err = w.Write(out) return err } func controlProxyReadPacket(r io.Reader) (cryptobyte.String, error) { var l uint32 if err := binary.Read(r, binary.BigEndian, &l); err != nil { return nil, err } if l > maxPacket { return nil, fmt.Errorf("message length %v exceeds maximum %v", l, maxPacket) } buf := make([]byte, l) if _, err := io.ReadFull(r, buf); err != nil { return nil, err } return buf, nil } func controlProxyReadMessage(r io.Reader) (messageType uint32, body cryptobyte.String, err error) { body, err = controlProxyReadPacket(r) if err != nil { return 0, nil, fmt.Errorf("error reading message body: %w", err) } if !body.ReadUint32(&messageType) { return 0, nil, errors.New("EOF reading message type") } return messageType, body, nil }