From 8bfa76900b71fdd4c15f44fc1bdb727138d51198 Mon Sep 17 00:00:00 2001 From: Janos Bonic <86970079+janosdebugs@users.noreply.github.com> Date: Sun, 5 Jun 2022 11:14:37 +0200 Subject: [PATCH 1/3] Refactoring the agent protocol --- agentprotocol/Connection.go | 11 +++ agentprotocol/ForwardCtx.go | 31 ++++++++ agentprotocol/NewForwardCtx.go | 12 +-- agentprotocol/Protocol.go | 28 +++---- agentprotocol/README.md | 19 +++++ agentprotocol/server.go | 132 ++++++++++++++++----------------- agentprotocol/server_test.go | 23 +++--- 7 files changed, 159 insertions(+), 97 deletions(-) create mode 100644 agentprotocol/Connection.go create mode 100644 agentprotocol/ForwardCtx.go create mode 100644 agentprotocol/README.md diff --git a/agentprotocol/Connection.go b/agentprotocol/Connection.go new file mode 100644 index 00000000..1d84892f --- /dev/null +++ b/agentprotocol/Connection.go @@ -0,0 +1,11 @@ +package agentprotocol + +type Connection interface { + Read(p []byte) (n int, err error) + Write(data []byte) (n int, err error) + Close() error + CloseImmediately() error + Accept() error + Reject() error + Details() NewConnectionPayload +} diff --git a/agentprotocol/ForwardCtx.go b/agentprotocol/ForwardCtx.go new file mode 100644 index 00000000..c6518b50 --- /dev/null +++ b/agentprotocol/ForwardCtx.go @@ -0,0 +1,31 @@ +package agentprotocol + +import "io" + +type ForwardCtx interface { + NewConnectionTCP( + connectedAddress string, + connectedPort uint32, + origAddress string, + origPort uint32, + closeFunc func() error, + ) (io.ReadWriteCloser, error) + NewConnectionUnix( + path string, + closeFunc func() error, + ) (io.ReadWriteCloser, error) + + StartClient() (connectionType uint32, setupPacket SetupPacket, connChan chan Connection, err error) + StartServerForward() (chan Connection, error) + StartX11ForwardClient( + singleConnection bool, + screen string, + authProtocol string, + authCookie string, + ) (chan Connection, error) + StartReverseForwardClient(bindHost string, bindPort uint32, singleConnection bool) (chan Connection, error) + StartReverseForwardClientUnix(path string, singleConnection bool) (chan Connection, error) + NoMoreConnections() error + WaitFinish() + Kill() +} diff --git a/agentprotocol/NewForwardCtx.go b/agentprotocol/NewForwardCtx.go index e368bf9a..5320bd3c 100644 --- a/agentprotocol/NewForwardCtx.go +++ b/agentprotocol/NewForwardCtx.go @@ -3,13 +3,13 @@ package agentprotocol import ( "io" - log "go.containerssh.io/libcontainerssh/log" + log "go.containerssh.io/libcontainerssh/log" ) -func NewForwardCtx(fromBackend io.Reader, toBackend io.Writer, logger log.Logger) *ForwardCtx { - return &ForwardCtx{ +func NewForwardCtx(fromBackend io.Reader, toBackend io.Writer, logger log.Logger) ForwardCtx { + return &forwardCtx{ fromBackend: fromBackend, - toBackend: toBackend, - logger: logger, + toBackend: toBackend, + logger: logger, } -} \ No newline at end of file +} diff --git a/agentprotocol/Protocol.go b/agentprotocol/Protocol.go index 0d60035d..d73e1643 100644 --- a/agentprotocol/Protocol.go +++ b/agentprotocol/Protocol.go @@ -2,14 +2,14 @@ package agentprotocol const ( CONNECTION_TYPE_X11 = iota - CONNECTION_TYPE_PORT_FORWARD = iota - CONNECTION_TYPE_PORT_DIAL = iota - CONNECTION_TYPE_SOCKET_FORWARD = iota - CONNECTION_TYPE_SOCKET_DIAL = iota + CONNECTION_TYPE_PORT_FORWARD + CONNECTION_TYPE_PORT_DIAL + CONNECTION_TYPE_SOCKET_FORWARD + CONNECTION_TYPE_SOCKET_DIAL ) const ( - PROTOCOL_TCP string = "tcp" + PROTOCOL_TCP string = "tcp" PROTOCOL_UNIX string = "unix" ) @@ -24,10 +24,10 @@ const ( ) type SetupPacket struct { - ConnectionType uint32 - BindHost string - BindPort uint32 - Protocol string + ConnectionType uint32 + BindHost string + BindPort uint32 + Protocol string Screen string SingleConnection bool @@ -36,8 +36,8 @@ type SetupPacket struct { } type NewConnectionPayload struct { - Protocol string - + Protocol string + ConnectedAddress string ConnectedPort uint32 OriginatorAddress string @@ -45,7 +45,7 @@ type NewConnectionPayload struct { } type Packet struct { - Type int - ConnectionId uint64 - Payload []byte + Type int + ConnectionID uint64 + Payload []byte } diff --git a/agentprotocol/README.md b/agentprotocol/README.md new file mode 100644 index 00000000..edd22fbc --- /dev/null +++ b/agentprotocol/README.md @@ -0,0 +1,19 @@ +# The ContainerSSH Agent protocol + +The ContainerSSH Agent protocol allows for forwarding several types of connections within the container: X11, TCP, etc. This description describes how the protocol works and how to integrate it. + +## Concepts + +The server and the client communicate over the standard input/output using the container APIs. (Docker and Kubernetes) The agent is running just like any other program would in the container. + +### Server + +The server in this context is the ContainerSSH agent. It waits for connection requests from the client (ContainerSSH) and opens the corresponding sockkets. + +### Client + +The client in this context is ContainerSSH, opening a connection by sending requests to the agent via the standard input/output using the container API (Docker or Kubernetes). + +## Protocol + +The protocol consists of messages sent in [CBOR-encoding](https://cbor.io/) in a back-to-back fashion. \ No newline at end of file diff --git a/agentprotocol/server.go b/agentprotocol/server.go index d0924358..d5930fb6 100644 --- a/agentprotocol/server.go +++ b/agentprotocol/server.go @@ -6,9 +6,9 @@ import ( "sync" "time" - log "go.containerssh.io/libcontainerssh/log" - message "go.containerssh.io/libcontainerssh/message" "github.com/fxamacker/cbor/v2" + log "go.containerssh.io/libcontainerssh/log" + message "go.containerssh.io/libcontainerssh/message" ) const ( @@ -18,7 +18,7 @@ const ( CONNECTION_STATE_CLOSED ) -type Connection struct { +type connection struct { logger log.Logger lock sync.Mutex state int @@ -28,15 +28,15 @@ type Connection struct { details NewConnectionPayload bufferReader *io.PipeReader bufferWriter *io.PipeWriter - ctx *ForwardCtx + ctx *forwardCtx closeCallback func() error } -func (c *Connection) Read(p []byte) (n int, err error) { +func (c *connection) Read(p []byte) (n int, err error) { return c.bufferReader.Read(p) } -func (c *Connection) Write(data []byte) (n int, err error) { +func (c *connection) Write(data []byte) (n int, err error) { c.lock.Lock() defer c.lock.Unlock() L: @@ -59,7 +59,7 @@ L: packet := Packet{ Type: PACKET_DATA, - ConnectionId: c.id, + ConnectionID: c.id, Payload: data, } err = c.ctx.writePacket(&packet) @@ -74,7 +74,7 @@ L: return len(data), nil } -func (c *Connection) Close() error { +func (c *connection) Close() error { c.lock.Lock() switch c.state { @@ -86,7 +86,7 @@ func (c *Connection) Close() error { c.lock.Unlock() packet := Packet{ Type: PACKET_CLOSE_CONNECTION, - ConnectionId: c.id, + ConnectionID: c.id, } return c.ctx.writePacket(&packet) case CONNECTION_STATE_WAITCLOSE: @@ -99,7 +99,7 @@ func (c *Connection) Close() error { return fmt.Errorf("unknown state") } -func (c *Connection) CloseImm() error { +func (c *connection) CloseImmediately() error { c.lock.Lock() defer c.lock.Unlock() if c.state != CONNECTION_STATE_WAITINIT && c.state != CONNECTION_STATE_STARTED && c.state != CONNECTION_STATE_WAITCLOSE { @@ -116,7 +116,7 @@ func (c *Connection) CloseImm() error { return nil } -func (c *Connection) Accept() error { +func (c *connection) Accept() error { c.lock.Lock() defer c.lock.Unlock() if c.initiator { @@ -129,12 +129,12 @@ func (c *Connection) Accept() error { c.stateCond.Broadcast() packet := Packet{ Type: PACKET_SUCCESS, - ConnectionId: c.id, + ConnectionID: c.id, } return c.ctx.writePacket(&packet) } -func (c *Connection) Reject() error { +func (c *connection) Reject() error { c.lock.Lock() defer c.lock.Unlock() if c.initiator { @@ -147,32 +147,32 @@ func (c *Connection) Reject() error { c.stateCond.Broadcast() packet := Packet{ Type: PACKET_ERROR, - ConnectionId: c.id, + ConnectionID: c.id, } return c.ctx.writePacket(&packet) } -func (c *Connection) Details() NewConnectionPayload { +func (c *connection) Details() NewConnectionPayload { return c.details } -func (c *Connection) setState(state int) { +func (c *connection) setState(state int) { c.lock.Lock() c.state = state c.stateCond.Broadcast() c.lock.Unlock() } -type ForwardCtx struct { +type forwardCtx struct { fromBackend io.Reader toBackend io.Writer logger log.Logger - connectionChannel chan *Connection + connectionChannel chan Connection stopped bool connectionId uint64 connMapMu sync.RWMutex - connMap map[uint64]*Connection + connMap map[uint64]*connection encoderMu sync.Mutex encoder *cbor.Encoder decoder *cbor.Decoder @@ -180,23 +180,23 @@ type ForwardCtx struct { waitGroup sync.WaitGroup } -func (c *ForwardCtx) writePacket(packet *Packet) error { +func (c *forwardCtx) writePacket(packet *Packet) error { c.encoderMu.Lock() err := c.encoder.Encode(&packet) c.encoderMu.Unlock() return err } -func (c *ForwardCtx) handleData(packet *Packet) { +func (c *forwardCtx) handleData(packet *Packet) { c.connMapMu.RLock() - conn, ok := c.connMap[packet.ConnectionId] + conn, ok := c.connMap[packet.ConnectionID] c.connMapMu.RUnlock() if !ok { c.logger.Info( message.NewMessage( message.EAgentUnknownConnection, "Received data packet with unknown connection id %d", - packet.ConnectionId, + packet.ConnectionID, ), ) return @@ -232,24 +232,24 @@ func (c *ForwardCtx) handleData(packet *Packet) { } } -func (c *ForwardCtx) handleClose(packet *Packet) { +func (c *forwardCtx) handleClose(packet *Packet) { c.connMapMu.Lock() - conn, ok := c.connMap[packet.ConnectionId] + conn, ok := c.connMap[packet.ConnectionID] if !ok { c.logger.Info( message.NewMessage( message.EAgentUnknownConnection, "Received close packet with unknown connection id %d", - packet.ConnectionId, + packet.ConnectionID, ), ) return } c.connMapMu.Unlock() - err := conn.CloseImm() + err := conn.CloseImmediately() retPacket := Packet{ Type: PACKET_SUCCESS, - ConnectionId: conn.id, + ConnectionID: conn.id, } if err != nil { retPacket.Type = PACKET_ERROR @@ -257,16 +257,16 @@ func (c *ForwardCtx) handleClose(packet *Packet) { _ = c.writePacket(&retPacket) } -func (c *ForwardCtx) handleSuccess(packet *Packet) { +func (c *forwardCtx) handleSuccess(packet *Packet) { c.connMapMu.Lock() defer c.connMapMu.Unlock() - conn, ok := c.connMap[packet.ConnectionId] + conn, ok := c.connMap[packet.ConnectionID] if !ok { c.logger.Info( message.NewMessage( message.EAgentUnknownConnection, "Received success packet with unknown connection id %d", - packet.ConnectionId, + packet.ConnectionID, ), ) return @@ -276,7 +276,7 @@ func (c *ForwardCtx) handleSuccess(packet *Packet) { case CONNECTION_STATE_WAITINIT: conn.setState(CONNECTION_STATE_STARTED) case CONNECTION_STATE_WAITCLOSE: - _ = conn.CloseImm() + _ = conn.CloseImmediately() default: c.logger.Warning( message.NewMessage( @@ -287,16 +287,16 @@ func (c *ForwardCtx) handleSuccess(packet *Packet) { } } -func (c *ForwardCtx) handleError(packet *Packet) { +func (c *forwardCtx) handleError(packet *Packet) { c.connMapMu.Lock() defer c.connMapMu.Unlock() - conn, ok := c.connMap[packet.ConnectionId] + conn, ok := c.connMap[packet.ConnectionID] if !ok { c.logger.Info( message.NewMessage( message.EAgentUnknownConnection, "Received error packet with unknown connection id %d", - packet.ConnectionId, + packet.ConnectionID, ), ) return @@ -306,23 +306,23 @@ func (c *ForwardCtx) handleError(packet *Packet) { message.NewMessage( message.MAgentRemoteError, "Received error packet for connection %d from remote", - packet.ConnectionId, + packet.ConnectionID, ), ) - _ = conn.CloseImm() + _ = conn.CloseImmediately() } -func (c *ForwardCtx) handleNewConnection(packet *Packet) { +func (c *forwardCtx) handleNewConnection(packet *Packet) { newConnectionPacket, err := c.unmarshalNewConnection(packet.Payload) if err != nil { c.logger.Error("Error unmarshalling new connection payload", err) return } pipeReader, pipeWriter := io.Pipe() - connection := Connection{ + connection := connection{ state: CONNECTION_STATE_WAITINIT, - id: packet.ConnectionId, + id: packet.ConnectionID, details: newConnectionPacket, bufferReader: pipeReader, bufferWriter: pipeWriter, @@ -331,24 +331,24 @@ func (c *ForwardCtx) handleNewConnection(packet *Packet) { } connection.stateCond = sync.NewCond(&connection.lock) c.connMapMu.Lock() - if _, ok := c.connMap[packet.ConnectionId]; ok { + if _, ok := c.connMap[packet.ConnectionID]; ok { c.logger.Warning("Remote tried to open connection with re-used connectionId") // Cannot send reject here, might interfere with other connection ? c.connMapMu.Unlock() return } - if packet.ConnectionId <= c.connectionId { + if packet.ConnectionID <= c.connectionId { c.logger.Warning("Suspicious connection, id <= prev") // Can't send reject here either c.connMapMu.Unlock() return } - if packet.ConnectionId != c.connectionId+1 { + if packet.ConnectionID != c.connectionId+1 { c.logger.Warning("Suspicious connection, id not prev + 1") } - c.connectionId = packet.ConnectionId - c.connMap[packet.ConnectionId] = &connection + c.connectionId = packet.ConnectionID + c.connMap[packet.ConnectionID] = &connection c.waitGroup.Add(1) c.connMapMu.Unlock() @@ -361,7 +361,7 @@ func (c *ForwardCtx) handleNewConnection(packet *Packet) { c.connectionChannel <- &connection } -func (c *ForwardCtx) handleBackend() { +func (c *forwardCtx) handleBackend() { for { packet := Packet{} err := c.decoder.Decode(&packet) @@ -401,7 +401,7 @@ func (c *ForwardCtx) handleBackend() { } } -func (c *ForwardCtx) unmarshalSetup(payload []byte) (SetupPacket, error) { +func (c *forwardCtx) unmarshalSetup(payload []byte) (SetupPacket, error) { packet := SetupPacket{} err := cbor.Unmarshal(payload, &packet) if err != nil { @@ -410,7 +410,7 @@ func (c *ForwardCtx) unmarshalSetup(payload []byte) (SetupPacket, error) { return packet, nil } -func (c *ForwardCtx) unmarshalNewConnection(payload []byte) (NewConnectionPayload, error) { +func (c *forwardCtx) unmarshalNewConnection(payload []byte) (NewConnectionPayload, error) { packet := NewConnectionPayload{} err := cbor.Unmarshal(payload, &packet) if err != nil { @@ -419,7 +419,7 @@ func (c *ForwardCtx) unmarshalNewConnection(payload []byte) (NewConnectionPayloa return packet, nil } -func (c *ForwardCtx) NewConnectionTCP( +func (c *forwardCtx) NewConnectionTCP( connectedAddress string, connectedPort uint32, origAddress string, @@ -436,7 +436,7 @@ func (c *ForwardCtx) NewConnectionTCP( ) } -func (c *ForwardCtx) NewConnectionUnix( +func (c *forwardCtx) NewConnectionUnix( path string, closeFunc func() error, ) (io.ReadWriteCloser, error) { @@ -450,7 +450,7 @@ func (c *ForwardCtx) NewConnectionUnix( ) } -func (c *ForwardCtx) newConnection( +func (c *forwardCtx) newConnection( protocol string, connectedAddress string, connectedPort uint32, @@ -476,7 +476,7 @@ func (c *ForwardCtx) newConnection( } bufferReader, bufferWriter := io.Pipe() - conn := Connection{ + conn := connection{ state: CONNECTION_STATE_WAITINIT, initiator: true, bufferReader: bufferReader, @@ -498,7 +498,7 @@ func (c *ForwardCtx) newConnection( c.connMapMu.Unlock() err = c.writePacket(&Packet{ Type: PACKET_NEW_CONNECTION, - ConnectionId: conn.id, + ConnectionID: conn.id, Payload: marInfo, }) if err != nil { @@ -513,15 +513,15 @@ func (c *ForwardCtx) newConnection( return &conn, nil } -func (c *ForwardCtx) init() { - c.connMap = make(map[uint64]*Connection) - c.connectionChannel = make(chan *Connection) +func (c *forwardCtx) init() { + c.connMap = make(map[uint64]*connection) + c.connectionChannel = make(chan Connection) c.encoder = cbor.NewEncoder(c.toBackend) c.decoder = cbor.NewDecoder(c.fromBackend) } -func (c *ForwardCtx) StartClient() (connectionType uint32, setupPacket SetupPacket, connChan chan *Connection, err error) { +func (c *forwardCtx) StartClient() (connectionType uint32, setupPacket SetupPacket, connChan chan Connection, err error) { c.init() packet := Packet{} @@ -568,7 +568,7 @@ func (c *ForwardCtx) StartClient() (connectionType uint32, setupPacket SetupPack return setup.ConnectionType, setup, c.connectionChannel, nil } -func (c *ForwardCtx) StartServerForward() (chan *Connection, error) { +func (c *forwardCtx) StartServerForward() (chan Connection, error) { c.init() setupPacket := SetupPacket{ @@ -609,7 +609,7 @@ func (c *ForwardCtx) StartServerForward() (chan *Connection, error) { return c.connectionChannel, nil } -func (c *ForwardCtx) startReverseForwardingClient(setupPacket SetupPacket) (chan *Connection, error) { +func (c *forwardCtx) startReverseForwardingClient(setupPacket SetupPacket) (chan Connection, error) { c.init() mar, err := cbor.Marshal(&setupPacket) @@ -647,7 +647,7 @@ func (c *ForwardCtx) startReverseForwardingClient(setupPacket SetupPacket) (chan return c.connectionChannel, nil } -func (c *ForwardCtx) StartX11ForwardClient(singleConnection bool, screen string, authProtocol string, authCookie string) (chan *Connection, error) { +func (c *forwardCtx) StartX11ForwardClient(singleConnection bool, screen string, authProtocol string, authCookie string) (chan Connection, error) { setupPacket := SetupPacket{ ConnectionType: CONNECTION_TYPE_X11, Protocol: "tcp", @@ -660,7 +660,7 @@ func (c *ForwardCtx) StartX11ForwardClient(singleConnection bool, screen string, return c.startReverseForwardingClient(setupPacket) } -func (c *ForwardCtx) StartReverseForwardClient(bindHost string, bindPort uint32, singleConnection bool) (chan *Connection, error) { +func (c *forwardCtx) StartReverseForwardClient(bindHost string, bindPort uint32, singleConnection bool) (chan Connection, error) { setupPacket := SetupPacket{ ConnectionType: CONNECTION_TYPE_PORT_FORWARD, BindHost: bindHost, @@ -672,7 +672,7 @@ func (c *ForwardCtx) StartReverseForwardClient(bindHost string, bindPort uint32, return c.startReverseForwardingClient(setupPacket) } -func (c *ForwardCtx) StartReverseForwardClientUnix(path string, singleConnection bool) (chan *Connection, error) { +func (c *forwardCtx) StartReverseForwardClientUnix(path string, singleConnection bool) (chan Connection, error) { setupPacket := SetupPacket{ ConnectionType: CONNECTION_TYPE_PORT_FORWARD, BindHost: path, @@ -683,7 +683,7 @@ func (c *ForwardCtx) StartReverseForwardClientUnix(path string, singleConnection return c.startReverseForwardingClient(setupPacket) } -func (c *ForwardCtx) NoMoreConnections() error { +func (c *forwardCtx) NoMoreConnections() error { c.stopped = true close(c.connectionChannel) return c.writePacket( @@ -693,11 +693,11 @@ func (c *ForwardCtx) NoMoreConnections() error { ) } -func (c *ForwardCtx) WaitFinish() { +func (c *forwardCtx) WaitFinish() { c.waitGroup.Wait() } -func (c *ForwardCtx) Kill() { +func (c *forwardCtx) Kill() { if !c.stopped { _ = c.NoMoreConnections() } @@ -710,7 +710,7 @@ func (c *ForwardCtx) Kill() { case <-t: case <-time.After(5 * time.Second): for _, conn := range c.connMap { - _ = conn.CloseImm() + _ = conn.CloseImmediately() } } }() diff --git a/agentprotocol/server_test.go b/agentprotocol/server_test.go index b4f589ae..089cdde3 100644 --- a/agentprotocol/server_test.go +++ b/agentprotocol/server_test.go @@ -5,22 +5,21 @@ import ( "io" "testing" - proto "go.containerssh.io/libcontainerssh/agentprotocol" + proto "go.containerssh.io/libcontainerssh/agentprotocol" - log "go.containerssh.io/libcontainerssh/log" + log "go.containerssh.io/libcontainerssh/log" ) -// region Tests func TestConnectionSetup(t *testing.T) { - log := log.NewTestLogger(t) + logger := log.NewTestLogger(t) fromClientReader, fromClientWriter := io.Pipe() toClientReader, toClientWriter := io.Pipe() - clientCtx := proto.NewForwardCtx(toClientReader, fromClientWriter, log) - - serverCtx := proto.NewForwardCtx(fromClientReader, toClientWriter, log) + clientCtx := proto.NewForwardCtx(toClientReader, fromClientWriter, logger) + serverCtx := proto.NewForwardCtx(fromClientReader, toClientWriter, logger) closeChan := make(chan struct{}) + startedChan := make(chan struct{}) go func() { connChan, err := serverCtx.StartReverseForwardClient( @@ -32,19 +31,21 @@ func TestConnectionSetup(t *testing.T) { panic(err) } + close(startedChan) + testConServer := <-connChan err = testConServer.Accept() if err != nil { - log.Error("Error accept connection", err) + logger.Error("Error accept connection", err) } buf := make([]byte, 512) nBytes, err := testConServer.Read(buf) if err != nil { - log.Error("Failed to read from server") + logger.Error("Failed to read from server") } _, err = testConServer.Write(buf[:nBytes]) if err != nil { - log.Error("Failed to write to server") + logger.Error("Failed to write to server") } <-closeChan serverCtx.Kill() @@ -55,7 +56,7 @@ func TestConnectionSetup(t *testing.T) { t.Fatal("Test failed with error", err) } if conType != proto.CONNECTION_TYPE_PORT_FORWARD { - panic(fmt.Errorf("Invalid connection type %d", conType)) + panic(fmt.Errorf("invalid connection type %d", conType)) } go func() { From dac7a8eac4aa9bc8262fb8c350b552197b0a91aa Mon Sep 17 00:00:00 2001 From: Nikos Tsipinakis Date: Thu, 23 Feb 2023 20:08:33 +0100 Subject: [PATCH 2/3] Fix agentforward use of agentprotocol --- internal/agentforward/agentForwardImpl.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/agentforward/agentForwardImpl.go b/internal/agentforward/agentForwardImpl.go index 8b8445cc..b649646a 100644 --- a/internal/agentforward/agentForwardImpl.go +++ b/internal/agentforward/agentForwardImpl.go @@ -6,17 +6,17 @@ import ( "io" "sync" - protocol "go.containerssh.io/libcontainerssh/agentprotocol" - "go.containerssh.io/libcontainerssh/internal/sshserver" - "go.containerssh.io/libcontainerssh/log" + protocol "go.containerssh.io/libcontainerssh/agentprotocol" + "go.containerssh.io/libcontainerssh/internal/sshserver" + "go.containerssh.io/libcontainerssh/log" ) type agentForward struct { lock sync.Mutex - reverseForwards map[string]*protocol.ForwardCtx + reverseForwards map[string]protocol.ForwardCtx nX11Channels uint32 - x11Forward *protocol.ForwardCtx - directForward *protocol.ForwardCtx + x11Forward protocol.ForwardCtx + directForward protocol.ForwardCtx logger log.Logger } @@ -24,7 +24,7 @@ func NewAgentForward( logger log.Logger, ) AgentForward { return &agentForward{ - reverseForwards: make(map[string]*protocol.ForwardCtx), + reverseForwards: make(map[string]protocol.ForwardCtx), logger: logger, } } @@ -61,7 +61,7 @@ func serveConnection(log log.Logger, dst io.WriteCloser, src io.ReadCloser) { _ = src.Close() } -func (f *agentForward) serveX11(connChan chan *protocol.Connection, reverseHandler sshserver.ReverseForward) { +func (f *agentForward) serveX11(connChan chan protocol.Connection, reverseHandler sshserver.ReverseForward) { for { agentConn, ok := <-connChan if !ok { @@ -94,7 +94,7 @@ func (f *agentForward) serveX11(connChan chan *protocol.Connection, reverseHandl } } -func (f *agentForward) serveReverseForward(connChan chan *protocol.Connection, reverseHandler sshserver.ReverseForward) { +func (f *agentForward) serveReverseForward(connChan chan protocol.Connection, reverseHandler sshserver.ReverseForward) { for { agentConn, ok := <-connChan if !ok { @@ -120,7 +120,7 @@ func (f *agentForward) serveReverseForward(connChan chan *protocol.Connection, r } } -func (f *agentForward) serveReverseForwardUnix(connChan chan *protocol.Connection, reverseHandler sshserver.ReverseForward) { +func (f *agentForward) serveReverseForwardUnix(connChan chan protocol.Connection, reverseHandler sshserver.ReverseForward) { for { agentConn, ok := <-connChan if !ok { @@ -325,7 +325,7 @@ func (f *agentForward) setupDirectForward( return err } f.directForward = protocol.NewForwardCtx(fromAgent, toAgent, logger) - connChan, err := f.directForward.StartServerForward() + connChan, err := f.directForward.StartClientForward() if err != nil { return err } From 51648e05587f77cc9541413a00ec1f3bf09ad3ba Mon Sep 17 00:00:00 2001 From: Nikos Tsipinakis Date: Thu, 23 Feb 2023 20:07:21 +0100 Subject: [PATCH 3/3] Document agent protocol --- agentprotocol/Connection.go | 7 +++++ agentprotocol/ForwardCtx.go | 42 ++++++++++++++++++++++++++-- agentprotocol/README.md | 12 ++++++-- agentprotocol/images/cssh-agent.png | Bin 0 -> 28687 bytes agentprotocol/server.go | 4 +-- agentprotocol/server_test.go | 2 +- 6 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 agentprotocol/images/cssh-agent.png diff --git a/agentprotocol/Connection.go b/agentprotocol/Connection.go index 1d84892f..4eeb81a0 100644 --- a/agentprotocol/Connection.go +++ b/agentprotocol/Connection.go @@ -1,11 +1,18 @@ package agentprotocol type Connection interface { + // Read reads data from the connection. The blocking nature of this call depends on the underlying communication medium Read(p []byte) (n int, err error) + // Read reads data from the connection. The blocking nature of this call depends on the underlying communication medium Write(data []byte) (n int, err error) + // Close requests to close an active connection Close() error + // CloseImmediately closes an active connection without waiting for the other side to acknowledge CloseImmediately() error + // Accept accepts a pending connection request. It must be called before any Read/Write functions can be called on the connection Accept() error + // Reject rejects a pending connection request and closes the connection. Reject() error + // Details returns the details of a connection request. It can be called to gain more information about a connection before an Accept/Reject action is made Details() NewConnectionPayload } diff --git a/agentprotocol/ForwardCtx.go b/agentprotocol/ForwardCtx.go index c6518b50..47ca755b 100644 --- a/agentprotocol/ForwardCtx.go +++ b/agentprotocol/ForwardCtx.go @@ -3,6 +3,13 @@ package agentprotocol import "io" type ForwardCtx interface { + // NewConnectionTCP requests the other side to connect to a specified address/host and port combination and forward all data from the returned ReadWriteCloser to it. + // + // connectedAddress is the address that the connection requested to connect to, is it used by the receiving side to initiate the connection to the desired address. + // connectedPort is the port that the connection requested to connect to, is it used by the receiving side to initiate the connection to the desired port. + // origAddress is the originator address of the connection. It can be used by the receiving side to decide whether to accept this connection. + // origAddress is the originator port of the connection. It can be used by the receiving side to decide whether to accept this connection. + // closeFunc is a callback function that is called when the connection is called to perform cleanup of the backing connection. NewConnectionTCP( connectedAddress string, connectedPort uint32, @@ -10,22 +17,53 @@ type ForwardCtx interface { origPort uint32, closeFunc func() error, ) (io.ReadWriteCloser, error) + // NewConnectionUnix requests the other side to connect to a specified unix and forward all data from the returned ReadWriteCloser to it. + // + // path is the path of the unix socket to connect to. + // closeFunc is a callback function that is called when the connection is called to perform cleanup of the backing connection. NewConnectionUnix( path string, closeFunc func() error, ) (io.ReadWriteCloser, error) - StartClient() (connectionType uint32, setupPacket SetupPacket, connChan chan Connection, err error) - StartServerForward() (chan Connection, error) + // StartServer initializes the ForwardCtx in server mode which waits for information from the other side about the function it needs to perform. It returns the connection type the other side requests and additional information in the setupPacket. Additionally, a Connection channel, connChan, is returned that provides connection requests from the other side of the connection. + StartServer() (connectionType uint32, setupPacket SetupPacket, connChan chan Connection, err error) + + // StartClientForward initializes the ForwardCtx in client mode and informs the server that the client is going to be the connection requestor (Direct Forward). A connection channel is returned that informs the server of connection requests by the client however in this mode it is a assumed that the server sends no connection request so the sane behaviour is to reject all connections. In this mode, the client can start new connections on the server using the NewConnection* function family. + StartClientForward() (chan Connection, error) + + // StartX11ForwardClient initializes the ForwardCtx in client mode and informs the server to start an X11 server and forward all X11 connections to the client. + // + // singleConnection is the X11 singleConnection parameter that requests to only accept the first connection (X11 window) and no more. + // screen is the X11 screen number. + // authProtocol is the X11 auth protocol. + // authCookie is the X11 auth cookie. StartX11ForwardClient( singleConnection bool, screen string, authProtocol string, authCookie string, ) (chan Connection, error) + + // StartReverseForwardClient initializes the ForwardCtx in client mode and informs the server to start listening for connections on the requested host and port. Once a connection is received a new connection is created and sent through the Connection channel. + // + // bindHost is the host to listen on connections on. + // bindPort is the port to listen on connections on. + // singleConnection is a flag that requests to stop listening for new connections after the first one. StartReverseForwardClient(bindHost string, bindPort uint32, singleConnection bool) (chan Connection, error) + + // StartReverseForwardClient initializes the ForwardCtx in client mode and informs the server to start listening for connections on the requested unix socket. Once a connection is received a new connection is created and sent through the Connection channel. + // + // path is the path to the unix socket to listen on. + // singleConnection is a flag that requests to stop listening for new connections after the first one. StartReverseForwardClientUnix(path string, singleConnection bool) (chan Connection, error) + + // NoMoreConnections informs the other side that it should not accept any more connection requests from it. It is used as a forwarding security feature in cases where it's clear there will only be one connection. NoMoreConnections() error + + // WaitFinish blocks until NoMoreConnections has been received and all active connections have been closed. WaitFinish() + + // Kill closes the ForwardCtx immediately and terminates all connections. Kill() } diff --git a/agentprotocol/README.md b/agentprotocol/README.md index edd22fbc..c8c9b7e9 100644 --- a/agentprotocol/README.md +++ b/agentprotocol/README.md @@ -1,6 +1,6 @@ # The ContainerSSH Agent protocol -The ContainerSSH Agent protocol allows for forwarding several types of connections within the container: X11, TCP, etc. This description describes how the protocol works and how to integrate it. +The ContainerSSH Agent protocol allows for forwarding and reverse-forwarding several types of connections within the container: X11, TCP, etc. The protocol is designed to be symmetrical in a way that both ends can request, accept/reject, and process connections and both ends have the same capabilities during a connection. However, there is a small client/server distinction while initializing the protocol in the initial exchange: The 'Client' (ContainerSSH) sends a `SetupPacket` to the 'Server' (Agent) that specificies the mode that the agent should initialize to (e.g. forward, reverse-forward, X11 forward etc). ## Concepts @@ -14,6 +14,14 @@ The server in this context is the ContainerSSH agent. It waits for connection re The client in this context is ContainerSSH, opening a connection by sending requests to the agent via the standard input/output using the container API (Docker or Kubernetes). +### Connection + +A Connection is a bidirectional binary communication between the server and the client. Multiple number of connections can be active at any given time and both sides (client/server) have the capacity to request a new connection. Connections are identified by a ConnectionID and each packet includes the ConnectionID to associate it with a connection. The state of connections is detailed in the following flow graph where the nodes represent the valid connection states and the edges are the actions/packets that affect the connection state. + +![connection state diagram](./images/cssh-agent.png) + +When a connection is initiated it is in WAITINIT state until the other end issues either an Accept action, which results in a SUCCESS message and the connection starting or a Reject action whith results in an ERROR message and the connection closing. When a connection is in the STARTED state it can accept data and both sides can issue write() and read() calls to write and read from the connection. The blocking/non-blocking nature of these calls depends on the underlying communication medium. When a connection is closed from one end it is moved to the WAITCLOSE state until the other side acknowledges the close request. This is necessary to ensure that any leftover data sent after the close call is processed. Finally once the close request is acknowledged the connection is finally closed. + ## Protocol -The protocol consists of messages sent in [CBOR-encoding](https://cbor.io/) in a back-to-back fashion. \ No newline at end of file +The protocol consists of messages sent in [CBOR-encoding](https://cbor.io/) in a back-to-back fashion. Other than the connection control packets described above there is additionally a 'No More Connections' packet that instructs the other side to stop accepting new connections. This is handled internally in the protocol library by closing the new connection channel. \ No newline at end of file diff --git a/agentprotocol/images/cssh-agent.png b/agentprotocol/images/cssh-agent.png new file mode 100644 index 0000000000000000000000000000000000000000..8cb463e93f2cbabb9c3feea0b47878e9b6e60a8b GIT binary patch literal 28687 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 Y(VL^w-wkl>MBI^t-