Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: x/crypto/ssh: export a transport interface #32958

Open
y3llowcake opened this issue Jul 5, 2019 · 7 comments · May be fixed by golang/crypto#100
Open

proposal: x/crypto/ssh: export a transport interface #32958

y3llowcake opened this issue Jul 5, 2019 · 7 comments · May be fixed by golang/crypto#100

Comments

@y3llowcake
Copy link

@y3llowcake y3llowcake commented Jul 5, 2019

What version of Go are you using (go version)?

$ go version
go version go1.12.6 linux/amd64

Does this issue reproduce with the latest release?

Yes

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GOARCH="amd64"                         
GOBIN=""                               
GOCACHE="/tmp/bazel_shared/gocache"    
GOEXE=""                               
GOFLAGS="-mod=readonly"                
GOHOSTARCH="amd64"                     
GOHOSTOS="linux"                       
GOOS="linux"                           
GOPATH="/tmp/bazel_shared/gopath"      
GOPROXY="direct"                       
GORACE=""                              
GOROOT="/home/cy/.cache/bazel/_bazel_cy/80b33c882b53d18473a4d1179c663f81/external/go_sdk"                                                                     
GOTMPDIR=""                            
GOTOOLDIR="/home/cy/.cache/bazel/_bazel_cy/80b33c882b53d18473a4d1179c663f81/external/go_sdk/pkg/tool/linux_amd64"                                             
GCCGO="gccgo"                          
CC="gcc"                               
CXX="g++"                              
CGO_ENABLED="0"                        
GOMOD="/home/cy/.cache/bazel/_bazel_cy/80b33c882b53d18473a4d1179c663f81/execroot/goslackgo/bazel-out/k8-fastbuild/bin/go.runfiles/goslackgo/go_gocmd_execroot/go.mod"
CGO_CFLAGS="-g -O2"                    
CGO_CPPFLAGS=""                        
CGO_CXXFLAGS="-g -O2"                  
CGO_FFLAGS="-g -O2"                    
CGO_LDFLAGS="-g -O2"                   
PKG_CONFIG="pkg-config"                
GOGCCFLAGS="-fPIC -m64 -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build807263343=/tmp/go-build -gno-record-gcc-switches" 

Proposal: Feature Request

I propose a modification to the x/crypto/ssh library that would allow an application to use this library's implementation of the SSH Connection Protocol (RFC 4254), but provide a separate implementation of the SSH Transport Protocol (RFC 4253).

Concretely this would require adapting the existing packetConn interface and exporting it, and then adding a Client constructor that operated on this interface:

type Transport interface {
  WritePacket([]byte) error
  ReadPacket() ([]byte, error)
  Close() error
}

func NewClientFromTransport(t Transport) (Conn, <-chan NewChannel, <-chan *Request, error) 

My immediate use case for this is to implement an application that uses an OpenSSH ControlMaster socket as the transport. However, I can imagine other use cases for this. This proposal is an alternative to #31874.

@gopherbot
Copy link

@gopherbot gopherbot commented Sep 30, 2019

Change https://golang.org/cl/193117 mentions this issue: ssh: export a transport interface

@y3llowcake
Copy link
Author

@y3llowcake y3llowcake commented Mar 5, 2020

@FiloSottile, if you have some time to investigate this proposal it would be much appreciated.

@dcow
Copy link

@dcow dcow commented Jun 8, 2020

The change looks good to me, and I would appreciate this interface.

@ianlancetaylor ianlancetaylor added this to Incoming in Proposals Feb 17, 2021
@y3llowcake
Copy link
Author

@y3llowcake y3llowcake commented Mar 14, 2021

Hello, just checking to see where this is at in the proposal process. I am still maintaining a fork of this library for an environment where openssh control master sockets are in use.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Mar 16, 2021

This was accidentally left off the list when it was created, and now it is waiting to get onto the list of proposals under active consideration. Sorry for the delay.

@marvinthepa
Copy link

@marvinthepa marvinthepa commented Mar 30, 2021

My immediate use case for this is to implement an application that uses an OpenSSH ControlMaster socket as the transport.

Any code on how this would look like?

@y3llowcake
Copy link
Author

@y3llowcake y3llowcake commented Mar 30, 2021

Example usage:

package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"net"
	xssh "golang.org/x/crypto/ssh"
)

func check(err error) {
	if err != nil {
		panic(err)
	}
}

func main() {
	unixAddr := "/tmp/ssh-control-cy@ssh-bastion.acme-corp.com:22"
	c, err := net.Dial("unix", unixAddr)

	if err != nil {
		check(fmt.Errorf("dial failed: %v", err))
	}

	trans, err := handshakeControlProxy(c)
	check(err)

	conn, nchan, reqs, err := xssh.NewClientConnFromTransport(trans)

	cli := xssh.NewClient(conn, nchan, reqs)
	sess, err := cli.NewSession()
	check(err)

	// Print out the hostname on the other end.
	b, err := sess.CombinedOutput("hostname")
	check(err)
	fmt.Println(string(b))
}

const (
	MUX_MSG_HELLO = 0x00000001
	MUX_C_PROXY   = 0x1000000f
	MUX_S_PROXY   = 0x8000000f
	MUX_S_FAILURE = 0x80000003
)

// handshakeControlProxy attempts to establish a transport connection with an
// ssh ControlMaster socket in proxy mode. For details see:
// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.mux
func handshakeControlProxy(rw io.ReadWriteCloser) (*controlProxyTransport, error) {
	b := &controlBuffer{}
	b.WriteUint32(MUX_MSG_HELLO)
	b.WriteUint32(4) // Protocol Version
	if _, err := rw.Write(b.LengthPrefixedBytes()); err != nil {
		return nil, fmt.Errorf("mux hello write failed: %v", err)
	}

	b.Reset()
	b.WriteUint32(MUX_C_PROXY)
	b.WriteUint32(0) // Request ID
	if _, err := rw.Write(b.LengthPrefixedBytes()); err != nil {
		return nil, fmt.Errorf("mux client proxy write failed: %v", err)
	}

	r := controlReader{rw}
	m, err := r.Next()
	if err != nil {
		return nil, fmt.Errorf("mux hello read failed: %v", err)
	}
	if m.messageType != MUX_MSG_HELLO {
		return nil, fmt.Errorf("mux reply not hello")
	}
	if v, err := m.ReadUint32(); err != nil || v != 4 {
		return nil, fmt.Errorf("mux reply hello has bad protocol version")
	}
	m, err = r.Next()
	if err != nil {
		return nil, fmt.Errorf("error reading mux server proxy: %v", err)
	}
	if m.messageType != MUX_S_PROXY {
		return nil, fmt.Errorf("expected server proxy response got %d", m.messageType)
	}
	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.Close()
}

func (p *controlProxyTransport) GetSessionID() []byte {
	return nil
}

func (p *controlProxyTransport) ReadPacket() ([]byte, error) {
	var l uint32
	err := binary.Read(p.rw, binary.BigEndian, &l)
	if err == nil {
		buf := &bytes.Buffer{}
		_, err = io.CopyN(buf, p.rw, int64(l))
		if err == nil {
			// Discard the padding byte.
			buf.ReadByte()
			return buf.Bytes(), nil
		}
	}
	return nil, err
}

func (p *controlProxyTransport) WritePacket(controlMessage []byte) error {
	l := uint32(len(controlMessage)) + 1
	b := &bytes.Buffer{}
	binary.Write(b, binary.BigEndian, &l) // controlMessage Length.
	b.WriteByte(0)                        // Padding Length.
	b.Write(controlMessage)
	_, err := p.rw.Write(b.Bytes())
	return err
}

func (p *controlProxyTransport) WaitSession() error {
	return nil
}

type controlBuffer struct {
	bytes.Buffer
}

func (b *controlBuffer) WriteUint32(i uint32) {
	binary.Write(b, binary.BigEndian, i)
}

func (b *controlBuffer) LengthPrefixedBytes() []byte {
	b2 := &bytes.Buffer{}
	binary.Write(b2, binary.BigEndian, uint32(b.Len()))
	b2.Write(b.Bytes())
	return b2.Bytes()
}

type controlMessage struct {
	body        bytes.Buffer
	messageType uint32
}

func (p controlMessage) ReadUint32() (uint32, error) {
	var u uint32
	err := binary.Read(&p.body, binary.BigEndian, &u)
	return u, err
}

func (p controlMessage) ReadString() (string, error) {
	var l uint32
	err := binary.Read(&p.body, binary.BigEndian, &l)
	if err != nil {
		return "", fmt.Errorf("error reading string length: %v", err)
	}
	b := p.body.Next(int(l))
	if len(b) != int(l) {
		return string(b), fmt.Errorf("EOF on string read")
	}
	return string(b), nil
}

type controlReader struct {
	r io.Reader
}

func (r controlReader) Next() (*controlMessage, error) {
	p := &controlMessage{}
	var len uint32
	err := binary.Read(r.r, binary.BigEndian, &len)
	if err != nil {
		return nil, fmt.Errorf("error reading message length: %v", err)
	}
	_, err = io.CopyN(&p.body, r.r, int64(len))
	if err != nil {
		return nil, fmt.Errorf("error reading message payload: %v", err)
	}
	err = binary.Read(&p.body, binary.BigEndian, &p.messageType)
	if err != nil {
		return nil, fmt.Errorf("error reading message type: %v", err)
	}
	if p.messageType == MUX_S_FAILURE {
		reason, _ := p.ReadString()
		return nil, fmt.Errorf("server failure: '%s'", reason)
	}
	return p, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Proposals
Incoming
Linked pull requests

Successfully merging a pull request may close this issue.

6 participants