| // 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 | |
| } |