From 5abff6d99908178acbb2d958a71e5a76955d17ac Mon Sep 17 00:00:00 2001 From: Jorge David de Hoz Diego Date: Fri, 26 Jul 2024 12:45:33 -0400 Subject: [PATCH] Reverse port forwarding implementation --- channel.go | 117 +++++++++++++++++++++++++++++++++++++++++++++ client/client.go | 114 +++++++++++++++++++++++++++++++++++++++++++ cmd/ssh3-server.go | 106 ++++++++++++++++++++++++++++++++++++++++ cmd/ssh3.go | 37 +++++++++++++- conversation.go | 24 ++++++++++ server.go | 13 +++++ 6 files changed, 410 insertions(+), 1 deletion(-) diff --git a/channel.go b/channel.go index 259300a..3e49631 100644 --- a/channel.go +++ b/channel.go @@ -127,6 +127,16 @@ type TCPForwardingChannelImpl struct { Channel } +type TCPReverseForwardingChannelImpl struct { + RemoteAddr *net.TCPAddr + LocalAddr *net.TCPAddr + Channel +} +type TCPOpenReverseForwardingChannelImpl struct { + RemoteAddr *net.TCPAddr + Channel +} + func buildHeader(conversationStreamID uint64, channelType string, maxPacketSize uint64, additionalBytes []byte) []byte { channelTypeBuf := make([]byte, util.SSHStringLen(channelType)) util.WriteSSHString(channelTypeBuf, channelType) @@ -159,6 +169,38 @@ func buildForwardingChannelAdditionalBytes(remoteAddr net.IP, port uint16) []byt buf = append(buf, portBuf[:]...) return buf } +func buildRequestTCPReverseChannelAdditionalBytes(localAddr net.IP, localPort uint16, remoteAddr net.IP, remotePort uint16) []byte { + var buf []byte + var portBuf [2]byte + //var portBuf2 [2]byte + + var addressFamily util.SSHForwardingAddressFamily + if len(localAddr) == 4 { + addressFamily = util.SSHAFIpv4 + } else { + addressFamily = util.SSHAFIpv6 + } + + buf = util.AppendVarInt(buf, addressFamily) + buf = append(buf, localAddr...) + binary.BigEndian.PutUint16(portBuf[:], uint16(localPort)) + buf = append(buf, portBuf[:]...) + + if len(remoteAddr) == 4 { + addressFamily = util.SSHAFIpv4 + } else { + addressFamily = util.SSHAFIpv6 + } + + buf = util.AppendVarInt(buf, addressFamily) + buf = append(buf, remoteAddr...) + binary.BigEndian.PutUint16(portBuf[:], uint16(remotePort)) + buf = append(buf, portBuf[:]...) + //TODO: If I do not duplicate this, the port does not arrive to destination + buf = append(buf, portBuf[:]...) + return buf + +} func parseHeader(channelID uint64, r util.Reader) (conversationControlStreamID ControlStreamID, channelType string, maxPacketSize uint64, err error) { conversationControlStreamID, err = util.ReadVarInt(r) @@ -206,6 +248,67 @@ func parseForwardingHeader(channelID uint64, buf util.Reader) (net.IP, uint16, e return address, port, nil } +func parseRequestReverseHeader(channelID uint64, buf util.Reader) (net.IP, uint16, net.IP, uint16, error) { + + var localaddress net.IP + var remoteaddress net.IP + var portBuf [2]byte + + //Parse local address and port where the reverse socket is proxied within the client machine + //------------------------------------------------------------------------------------------ + addressFamily, err := util.ReadVarInt(buf) + if err != nil { + return nil, 0, nil, 0, err + } + + if addressFamily == util.SSHAFIpv4 { + localaddress = make([]byte, 4) + } else if addressFamily == util.SSHAFIpv6 { + localaddress = make([]byte, 16) + } else { + return nil, 0, nil, 0, fmt.Errorf("invalid local address family: %d", addressFamily) + } + + _, err = buf.Read(localaddress) + if err != nil { + return nil, 0, nil, 0, err + } + + _, err = buf.Read(portBuf[:]) + if err != nil { + return nil, 0, nil, 0, err + } + localport := binary.BigEndian.Uint16(portBuf[:]) + + //Parse remote address and port of the remote service to be proxied within the client machine + //------------------------------------------------------------------------------------------- + addressFamily, err = util.ReadVarInt(buf) + if err != nil { + return nil, 0, nil, 0, err + } + + if addressFamily == util.SSHAFIpv4 { + remoteaddress = make([]byte, 4) + } else if addressFamily == util.SSHAFIpv6 { + remoteaddress = make([]byte, 16) + } else { + return nil, 0, nil, 0, fmt.Errorf("invalid remote address family: %d", addressFamily) + } + + _, err = buf.Read(remoteaddress) + if err != nil { + return nil, 0, nil, 0, err + } + + _, err = buf.Read(portBuf[:]) + if err != nil { + return nil, 0, nil, 0, err + } + remoteport := binary.BigEndian.Uint16(portBuf[:]) + + return localaddress, localport, remoteaddress, remoteport, nil +} + func parseUDPForwardingHeader(channelID uint64, buf util.Reader) (*net.UDPAddr, error) { address, port, err := parseForwardingHeader(channelID, buf) if err != nil { @@ -228,6 +331,20 @@ func parseTCPForwardingHeader(channelID uint64, buf util.Reader) (*net.TCPAddr, }, nil } +func parseTCPRequestReverseHeader(channelID uint64, buf util.Reader) (*net.TCPAddr, *net.TCPAddr, error) { + localaddress, localport, remoteaddress, remoteport, err := parseRequestReverseHeader(channelID, buf) + if err != nil { + return nil, nil, err + } + return &net.TCPAddr{ + IP: localaddress, + Port: int(localport), + }, &net.TCPAddr{ + IP: remoteaddress, + Port: int(remoteport), + }, nil +} + func NewChannel(conversationStreamID uint64, conversationID ConversationID, channelID uint64, channelType string, maxPacketSize uint64, recv quic.ReceiveStream, send io.WriteCloser, datagramSender util.SSH3DatagramSenderFunc, channelCloseListener channelCloseListener, sendHeader bool, confirmSent bool, confirmReceived bool, datagramsQueueSize uint64, additonalHeaderBytes []byte) Channel { diff --git a/client/client.go b/client/client.go index 54956bb..2afa158 100644 --- a/client/client.go +++ b/client/client.go @@ -207,6 +207,84 @@ func forwardTCPInBackground(ctx context.Context, channel ssh3.Channel, conn *net }() } +func forwardReverseTCPInBackground(ctx context.Context, channel ssh3.Channel, conn *net.TCPConn) { + go func() { + defer conn.CloseWrite() + for { + select { + case <-ctx.Done(): + return + default: + } + genericMessage, err := channel.NextMessage() + if err == io.EOF { + log.Info().Msgf("eof on tcp-forwarding channel %d", channel.ChannelID()) + } else if err != nil { + log.Error().Msgf("could get message from tcp forwarding channel: %s", err) + return + } + + // nothing to process + if genericMessage == nil { + return + } + + switch message := genericMessage.(type) { + case *ssh3Messages.DataOrExtendedDataMessage: + if message.DataType == ssh3Messages.SSH_EXTENDED_DATA_NONE { + _, err := conn.Write([]byte(message.Data)) + if err != nil { + log.Error().Msgf("could not write data on TCP socket: %s", err) + // signal the write error to the peer + channel.CancelRead() + return + } + } else { + log.Warn().Msgf("ignoring message data of unexpected type %d on TCP forwarding channel %d", message.DataType, channel.ChannelID()) + } + default: + log.Warn().Msgf("ignoring message of type %T on TCP forwarding channel %d", message, channel.ChannelID()) + } + } + }() + + go func() { + defer channel.Close() + defer conn.CloseRead() + buf := make([]byte, channel.MaxPacketSize()) + for { + select { + case <-ctx.Done(): + return + default: + } + n, err := conn.Read(buf) + if err != nil && err != io.EOF { + log.Error().Msgf("could read data on TCP socket: %s", err) + return + } + //log.Debug().Msgf("Reading from socket: %s", string(buf)) + _, errWrite := channel.WriteData(buf[:n], ssh3Messages.SSH_EXTENDED_DATA_NONE) + if errWrite != nil { + switch quicErr := errWrite.(type) { + case *quic.StreamError: + if quicErr.Remote && quicErr.ErrorCode == 42 { + log.Info().Msgf("writing was canceled by the remote, closing the socket: %s", errWrite) + } else { + log.Error().Msgf("unhandled quic stream error: %+v", quicErr) + } + default: + log.Error().Msgf("could send data on channel: %s", errWrite) + } + return + } + if err == io.EOF { + return + } + } + }() +} + type Client struct { qconn quic.EarlyConnection *ssh3.Conversation @@ -453,6 +531,42 @@ func (c *Client) ForwardTCP(ctx context.Context, localTCPAddr *net.TCPAddr, remo return conn.Addr().(*net.TCPAddr), nil } +func (c *Client) ReverseTCP(ctx context.Context, localTCPAddr *net.TCPAddr, remoteTCPAddr *net.TCPAddr) (*net.TCPAddr, error) { + log.Debug().Msgf("start TCP forwarding from %s to %s", localTCPAddr, remoteTCPAddr) + + forwardingChannel, err := c.RequestTCPReverseChannel(30000, 10, localTCPAddr, remoteTCPAddr) + if err != nil { + log.Error().Msgf("could open new TCP reverse forwarding channel: %s", err) + return remoteTCPAddr, nil + } + + go func() { + for { + channel, err := c.AcceptChannel(c.Context()) + if err != nil { + log.Debug().Msgf("Error accepting channel") + } + + switch channel.ChannelType() { + case "open-request-reverse-tcp": + log.Debug().Msgf("start reverse TCP forwarding from %s to %s", localTCPAddr, remoteTCPAddr) + + conn, err := net.DialTCP("tcp", nil, remoteTCPAddr) + if err != nil { + return + } + forwardReverseTCPInBackground(ctx, channel, conn) + if err != nil { + channel.Close() + return + } + } + } + }() + forwardingChannel.Close() + return remoteTCPAddr, nil +} + func (c *Client) RunSession(tty *os.File, forwardSSHAgent bool, command ...string) error { ctx := c.Context() diff --git a/cmd/ssh3-server.go b/cmd/ssh3-server.go index a7db8c7..629eddb 100644 --- a/cmd/ssh3-server.go +++ b/cmd/ssh3-server.go @@ -248,6 +248,84 @@ func forwardTCPInBackground(ctx context.Context, channel ssh3.Channel, conn *net }() } +func forwardReverseTCPInBackground(ctx context.Context, channel ssh3.Channel, conn *net.TCPConn) { + go func() { + defer channel.Close() + defer conn.CloseRead() + buf := make([]byte, channel.MaxPacketSize()) + for { + select { + case <-ctx.Done(): + return + default: + } + n, err := conn.Read(buf) + if err != nil && err != io.EOF { + log.Error().Msgf("could read data on TCP socket: %s", err) + return + } + _, errWrite := channel.WriteData(buf[:n], ssh3Messages.SSH_EXTENDED_DATA_NONE) + if errWrite != nil { + switch quicErr := errWrite.(type) { + case *quic.StreamError: + if quicErr.Remote && quicErr.ErrorCode == 42 { + log.Info().Msgf("writing was canceled by the remote, closing the socket: %s", errWrite) + } else { + log.Error().Msgf("unhandled quic stream error: %+v", quicErr) + } + default: + log.Error().Msgf("could send data on channel: %s", errWrite) + } + return + } + if err == io.EOF { + return + } + } + }() + + go func() { + defer conn.CloseWrite() + for { + select { + case <-ctx.Done(): + return + default: + } + genericMessage, err := channel.NextMessage() + if err == io.EOF { + log.Info().Msgf("eof on reverse-tcp-forwarding channel %d", channel.ChannelID()) + } else if err != nil { + log.Error().Msgf("could get message from tcp forwarding channel: %s", err) + return + } + + // nothing to process + if genericMessage == nil { + return + } + + switch message := genericMessage.(type) { + case *ssh3Messages.DataOrExtendedDataMessage: + if message.DataType == ssh3Messages.SSH_EXTENDED_DATA_NONE { + _, err := conn.Write([]byte(message.Data)) + if err != nil { + log.Error().Msgf("could not write data on TCP socket: %s", err) + // signal the write error to the peer + channel.CancelRead() + return + } + } else { + log.Warn().Msgf("ignoring message data of unexpected type %d on TCP forwarding channel %d", message.DataType, channel.ChannelID()) + } + default: + log.Warn().Msgf("ignoring message of type %T on TCP forwarding channel %d", message, channel.ChannelID()) + } + } + }() + +} + func execCmdInBackground(channel ssh3.Channel, openPty *openPty, user *unix_util.User, runningCommand *runningCommand, authAgentSocketPath string) error { setupEnv(user, runningCommand, authAgentSocketPath) if openPty != nil { @@ -551,6 +629,32 @@ func handleTCPForwardingChannel(ctx context.Context, user *unix_util.User, conv return nil } +func handleTCPReverseForwardingChannel(ctx context.Context, user *unix_util.User, conv *ssh3.Conversation, channel *ssh3.TCPReverseForwardingChannelImpl) error { + conn, err := net.ListenTCP("tcp", channel.LocalAddr) + if err != nil { + log.Error().Msgf("could listen on TCP socket: %s", err) + return nil + } + + go func() { + for { + conn, err := conn.AcceptTCP() + if err != nil { + log.Error().Msgf("could read on UDP socket: %s", err) + return + } + + forwardingChannel, err := conv.OpenTCPReverseForwardingChannel(30000, 10, channel.RemoteAddr) + if err != nil { + log.Error().Msgf("could not open new TCP reverse forwarding channel: %s", err) + return + } + forwardReverseTCPInBackground(ctx, forwardingChannel, conn) + } + }() + return nil +} + func newDataReq(user *unix_util.User, channel ssh3.Channel, request ssh3Messages.DataOrExtendedDataMessage) error { runningSession, ok := runningSessions.Get(channel) if !ok { @@ -855,6 +959,8 @@ func ServerMain() int { handleUDPForwardingChannel(conv.Context(), authenticatedUser, conv, c) case *ssh3.TCPForwardingChannelImpl: handleTCPForwardingChannel(conv.Context(), authenticatedUser, conv, c) + case *ssh3.TCPReverseForwardingChannelImpl: + handleTCPReverseForwardingChannel(conv.Context(), authenticatedUser, conv, c) default: runningSessions.Insert(channel, &runningSession{ channelState: LARVAL, diff --git a/cmd/ssh3.go b/cmd/ssh3.go index 2e2ce3f..3afd9fd 100644 --- a/cmd/ssh3.go +++ b/cmd/ssh3.go @@ -326,6 +326,7 @@ func ClientMain() int { forwardSSHAgent := flag.Bool("forward-agent", false, "if set, forwards ssh agent to be used with sshv2 connections on the remote host") forwardUDP := flag.String("forward-udp", "", "if set, take a localport/remoteip@remoteport forwarding localhost@localport towards remoteip@remoteport") forwardTCP := flag.String("forward-tcp", "", "if set, take a localport/remoteip@remoteport forwarding localhost@localport towards remoteip@remoteport") + reverseTCP := flag.String("reverse-tcp", "", "if set, take a remoteip@remoteport reverse forwarding it towards a localport/remoteip@remoteport") proxyJump := flag.String("proxy-jump", "", "if set, performs a proxy jump using the specified remote host as proxy (requires server with version >= 0.1.5)") flag.Parse() args := flag.Args() @@ -432,6 +433,31 @@ func ClientMain() int { } } + if *reverseTCP != "" { + localPort, remoteIP, remotePort, err := parseAddrPort(*reverseTCP) + if err != nil { + log.Error().Msgf("UDP reverse parsing error %s", err) + } + remoteTCPAddr = &net.TCPAddr{ + IP: remoteIP, + Port: remotePort, + } + if remoteIP.To4() != nil { + localTCPAddr = &net.TCPAddr{ + IP: net.IPv4(127, 0, 0, 1), + Port: localPort, + } + } else if remoteIP.To16() != nil { + localTCPAddr = &net.TCPAddr{ + IP: net.IPv6loopback, + Port: localPort, + } + } else { + log.Error().Msgf("Unrecognized IP length %d", len(remoteIP)) + return -1 + } + } + var sshConfig *ssh_config.Config var configBytes []byte configPath := path.Join(homedir(), ".ssh", "config") @@ -640,13 +666,22 @@ func ClientMain() int { log.Error().Msgf("could not dial %s: %s", options.CanonicalHostFormat(), err) return -1 } - if localTCPAddr != nil && remoteTCPAddr != nil { + if *forwardTCP != "" && localTCPAddr != nil && remoteTCPAddr != nil { _, err := c.ForwardTCP(ctx, localTCPAddr, remoteTCPAddr) if err != nil { log.Error().Msgf("could not forward UDP: %s", err) return -1 } } + + if *reverseTCP != "" && localTCPAddr != nil && remoteTCPAddr != nil { + _, err := c.ReverseTCP(ctx, localTCPAddr, remoteTCPAddr) + if err != nil { + log.Error().Msgf("could not reverse UDP: %s", err) + return -1 + } + } + if localUDPAddr != nil && remoteUDPAddr != nil { _, err := c.ForwardUDP(ctx, localUDPAddr, remoteUDPAddr) if err != nil { diff --git a/conversation.go b/conversation.go index 9d8364c..d6f10b3 100644 --- a/conversation.go +++ b/conversation.go @@ -307,7 +307,31 @@ func (c *Conversation) OpenTCPForwardingChannel(maxPacketSize uint64, datagramsQ c.channelsManager.addChannel(channel) return &TCPForwardingChannelImpl{Channel: channel, RemoteAddr: remoteAddr}, nil } +func (c *Conversation) RequestTCPReverseChannel(maxPacketSize uint64, datagramsQueueSize uint64, localAddr *net.TCPAddr, remoteAddr *net.TCPAddr) (Channel, error) { + str, err := c.streamCreator.OpenStream() + if err != nil { + return nil, err + } + + additionalBytes := buildRequestTCPReverseChannelAdditionalBytes(localAddr.IP, uint16(localAddr.Port), remoteAddr.IP, uint16(remoteAddr.Port)) + + channel := NewChannel(uint64(c.controlStream.StreamID()), c.conversationID, uint64(str.StreamID()), "request-reverse-tcp", maxPacketSize, &StreamByteReader{str}, str, nil, c.channelsManager, true, true, false, datagramsQueueSize, additionalBytes) + channel.maybeSendHeader() + c.channelsManager.addChannel(channel) + return &TCPForwardingChannelImpl{Channel: channel, RemoteAddr: remoteAddr}, nil +} +func (c *Conversation) OpenTCPReverseForwardingChannel(maxPacketSize uint64, datagramsQueueSize uint64, remoteAddr *net.TCPAddr) (Channel, error) { + + str, err := c.streamCreator.OpenStream() + if err != nil { + return nil, err + } + channel := NewChannel(uint64(c.controlStream.StreamID()), c.conversationID, uint64(str.StreamID()), "open-request-reverse-tcp", maxPacketSize, &StreamByteReader{str}, str, nil, c.channelsManager, true, true, false, datagramsQueueSize, nil) + channel.maybeSendHeader() + c.channelsManager.addChannel(channel) + return &TCPOpenReverseForwardingChannelImpl{Channel: channel, RemoteAddr: remoteAddr}, nil +} func (c *Conversation) AcceptChannel(ctx context.Context) (Channel, error) { for { if channel := c.channelsAcceptQueue.Next(); channel != nil { diff --git a/server.go b/server.go index 679ee3e..93265c0 100644 --- a/server.go +++ b/server.go @@ -91,6 +91,19 @@ func NewServer(maxPacketSize uint64, defaultDatagramQueueSize uint64, h3Server * return false, err } newChannel = &TCPForwardingChannelImpl{Channel: newChannel, RemoteAddr: tcpAddr} + + case "request-reverse-tcp": + tcpAddrLocal, tcpAddrRemote, err := parseTCPRequestReverseHeader(channelInfo.ChannelID, &StreamByteReader{stream}) + if err != nil { + return false, err + } + + newChannel = &TCPReverseForwardingChannelImpl{Channel: newChannel, RemoteAddr: tcpAddrRemote, LocalAddr: tcpAddrLocal} + //The channel is only used to receive the data featuring the reverse proxy and is closed afterwards + //In OpenSSH, the client does this by sendinng a GLOBAL_REQUEST "tcpip-forward", but I think in SSH3 global messages are not implemented + + //tcpAddrLocal: Local socket within the server machine where will be proxied a local service at reach of the ssh3 client + //tcpAddrRemote: The remote socket at reach of the SSH3 client to be proxied within the machine hosting the ssh3 server } conversation.channelsAcceptQueue.Add(newChannel) return true, nil