#Proxy io.reader and io.writer

16 messages · Page 1 of 1 (latest)

viral cypress
#

I'm creating a SSH bastion server, which essentially connects the client to multiple SSH servers. The client should be able to seamlessly switch between SSH servers without losing connection. When a client connects, I connect to the first SSH server and then use io.copy in goroutines to pipe responses to/from the server to the client.However, when I connect to the second server, I need to be able to pipe response from the second server to the client.

Here is my attempt at the proxy. It works for the first connection, but when I get to the second one, the client connection closes.

package proxyio

import (
    "io"

    "github.com/sirupsen/logrus"
)

// Reader wraps and io.reader
func Reader(reader io.Reader) ReaderProxy {
    return ReaderProxy{reader: reader}
}

type ReaderProxy struct {
    reader io.Reader
}

// Read from reader
func (r ReaderProxy) Read(p []byte) (n int, err error) {
    if n, err = r.reader.Read(p); err != nil {
        logrus.Errorf("Failed to read: %v", err)
        return 0, err
    }

    return
}

// Switch reader
func (r ReaderProxy) Switch(reader io.Reader) {
    r.reader = reader
}

// Writer wraps an io.Writer
func Writer(writer io.Writer) WriterProxy {
    return WriterProxy{writer: writer}
}

type WriterProxy struct {
    writer io.Writer
}

// Write
func (w WriterProxy) Write(p []byte) (n int, err error) {
    if n, err = w.writer.Write(p); err != nil {
        logrus.Errorf("Failed to write: %v", err)
        return 0, err
    }

    return
}

// Switch writer
func (w WriterProxy) Switch(writer io.Writer) {
    w.writer = writer
}
#

Is using a channel to buffer a better idea?

snow estuary
#

What's the error message and can you show some of the calling code

viral cypress
#

Here's a sample. I stripped out the irrelevant parts

#

In the above, I'm attempting to use channels and buffer it

#

These are the logs

INFO[0002] Connecting to Jail                            port=2222 pty=true remote="127.0.0.1:39602" user=root
INFO[0002] Creating first jail                           port=2222 pty=true remote="127.0.0.1:39602" user=root
INFO[0005] Creating Jail session                         port=2222 pty=true remote="127.0.0.1:39602" user=root
INFO[0005] Waiting 5s to kill Jail                      
INFO[0005] Client requested pty-req %!s(bool=true) xterm-256colora������

 !"#$%&'()*23456789:;<=>FGHIJKZ[\]  port=2222 pty=true remote="127.0.0.1:39602" user=root
INFO[0006] Client requested env %!s(bool=false) LANG
                                                    en_US.UTF-8  port=2222 pty=true remote="127.0.0.1:39602" user=root
INFO[0006] Client requested shell %!s(bool=true)         port=2222 pty=true remote="127.0.0.1:39602" user=root
INFO[0010] TTL expired, killing jail                    
INFO[0010] Exiting TTL goroutine                        
INFO[0010] Creating second Jail                          port=2222 pty=true remote="127.0.0.1:39602" user=root
INFO[0011] closed req channel 2                          port=2222 pty=true remote="127.0.0.1:39602" user=root
^CINFO[0012] Stopping 2 SSH servers                       
#

So there is no error returned. But when the first ssh server connection is ended the client gets disconnected

#

The first server conection works perfectly. I can send commands and see outputs

twilit haven
#

random thought: io.EOF

viral cypress
#

I check if the err is io.EOF then break out of the for loop before writing to channel

twilit haven
#

another thought: close the reader but switch the client to the next open one. in case readers drop to 0, forward io.EOF to the downstream reader (client)

#

didn't read the code, tho

mental copper
#

Hey @viral cypress, I would create a PipeProxy struct which has an io.TeeReader (https://pkg.go.dev/io#TeeReader), this handles piping for you (you can export this variable in your struct so you don't need to duplicate the functions, then call it directly). Why are you not using pointer receivers for the functions attached to your struct? I would expect it to only modify the local copy of the variable at the moment. Do you only read from one SSH connection after switching? Then you only need one goroutine which keeps a single connection open.

mental copper
#

I would attach the Jail struct to the Client struct ("the client is in jail"), then add the JailPipeProxy to the Client struct. The switch function can then be added to the Client struct to switch the pipe.
Side note but you should be checking for other errors other than io.EOF via else. Defer should ideally also check it's error via defer func() { if errClosing := x.Close(); errClosing != nil { // At least log it, but you can make your return "named parameters" and if errClosing != nil do err = errClosing to pass to caller, if using "named parameters" make sure to actually check for not nil otherwise defer will override any previous error

#

At least log your errors instead of just returning, you can add stack-traces via https://github.com/pkg/errors WithStack saves you a lot of time trying to find which error message came from which line.