5.4 Forwarded command execution

This example application implements a pair of ssh2 clients. The first client requests a TCP port forwarding on a server and uses the forwarded port to let the second client connect to a second server. The second client executes a remote command like in the Remote command execution example.

The two client sessions instantiated in our application are referred as the fwd session and the rexec session. Similarly, the first server host that is used as a proxy is called fwd_host and the second server that executes the remote command is called rexec_host.

From the remote servers point of view, running our application is similar to invocation of the following OpenSSH commands on the localhost terminal:

localhost$ ssh -N -L 2222:rexec_host:22 fwd_host &
localhost$ ssh -p 2222 localhost command

This approach relies on a direct connection between localhost and rexec_host in order to request execution of the command and fetch its output. This means that fwd_host is only used as a TCP proxy for the rexec connection and never see the content of the rexec session in clear text.

The same goal can be achieved by running the following commands on different hosts:

localhost$ ssh bounce_host
bounce_host$ ssh rexec_host command

This second approach is more common but less secure as it does not rely on port forwarding. In this case, the second client runs on bounce_host. This lets bounce_host see what happens as it processes user data in clear text, including any password typed to connect to rexec_host. In the same way, using user keys from bounce_host with this approach requires the use of possibly unsafe ssh agent forwarding.

Instead, our example application implements the first approach as a simple tool that can be invoked like this:

localhost$ example/fwdexec forward_host rexec_host command

In this example, we rely on the ability of libassh to let the application handle the network streams as buffers. This allows to implement forwarding of the rexec session by the fwd session without piping the network streams of the first through the operating system. That's why we do not rely on system calls for this task.

This means that the application directly generates and processes the network streams of the nested ssh2 sessions. From the operating system point of view, there is a single TCP socket used to connect to fwd_host, as in the other client examples.

The main loop [link] 

The first thing to do in the main function is initialization of the two sessions. A single interactive session state machine is used because it's only needed by the rexec session.

// code from examples/fwdexec.c:402

/* initialize an assh context, register services and algorithms */
struct assh_context_s *context;

if (assh_context_create(&context, ASSH_CLIENT,
NULL, NULL, NULL, NULL) ||
assh_service_register_default(context) ||
assh_algo_register_default(context, ASSH_SAFETY_WEAK))
ERROR("Unable to create an assh context.\n");

/* initialize the 2 client sessions */
if (assh_session_create(context, &fwd_session) ||
assh_session_create(context, &rexec_session))
ERROR("Unable to create an sessions.\n");

/* initializes an interactive session state machine object for the
rexec session */

asshh_client_init_inter_session(&rexec_inter, command, NULL);

Our main loop just has to run the event loops of the two sessions. The fwd session disconnects when the rexec session terminates.

while (1)
{
/* run the event loop of the forwarding session */
if (!ssh_loop_fwd() && assh_session_closed(rexec_session))
break;

/* run the event loop of the rexec session */
if (!ssh_loop_rexec())
assh_session_disconnect(fwd_session, SSH_DISCONNECT_BY_APPLICATION, NULL);
}

The main function ends with cleanup of the sessions and context.

fprintf(stderr, "Connection closed\n");

assh_session_release(rexec_session);
assh_session_release(fwd_session);
assh_context_release(context);

return 0;
}

The forwarder event loop [link] 

We will now focus on the implementation of the event loop of the fwd session.

The port forwarding mechanism in ssh2 is provided by the connection protocol and uses a channel in order to transport the stream of the forwarded TCP connection. This means that our fwd session will need to open such a channel in order to let the application exchange data with rexec_host.

There are many programming constructs that could be used to move data between the channel of the fwd session and the I/O events of the rexec session. In this application, we choose to copy the data directly between libassh buffers in one direction but we use a software fifo in the other direction.

Here is the API of our simple fifo. The fifo_read and fifo_write functions return the actual amount of data transferred.

struct fifo_s
{
uint8_t buf[128];
size_t ptr;
size_t size;
};

static size_t
fifo_read(struct fifo_s *f, uint8_t *data, size_t size);

static size_t
fifo_write(struct fifo_s *f, const uint8_t *data, size_t size);

Exchanging data requires sharing the channel and fifo objects between the two sessions. These are declared here:

static struct assh_channel_s *fwd_channel = NULL;
static struct fifo_s fwd_to_rexec = { };

Then comes the ssh_loop_fwd function with the usual event loop code construct:

static assh_bool_t
ssh_loop_fwd(void)
{
struct assh_event_s fwd_event;

while (assh_event_get(fwd_session, &fwd_event, time(NULL)))
{
switch (fwd_event.id)
{

User authentication and key-exchange related events are handled using helper functions as detailed in the previous examples and are not shown here.

Because this session talks to fwd_host through a TCP socket, we just have to rely on the usual helper function in order to handle the network I/O events.

case ASSH_EVENT_READ:
case ASSH_EVENT_WRITE:
/* use helpers to read/write the ssh stream from/to our
socket file descriptor */

asshh_fd_event(fwd_session, &fwd_event, fwd_sock);
continue;

We setup the TCP port forwarding as soon as the connection protocol starts. The asshh_portfwd_open_direct_tcpip function serializes our fwd_rq parameters and calls the assh_channel_open function. This makes the fwd_channel variable point to the opening channel object.

case ASSH_EVENT_SERVICE_START: {
const struct assh_service_s *srv = fwd_event.service.start.srv;

assh_event_done(fwd_session, &fwd_event, ASSH_OK);

/* setup a TCP port forwarding as soon as the ssh-connection
service has started. */

if (srv == &assh_service_connection)
{
struct asshh_portfwd_direct_tcpip_s fwd_rq;

assh_buffer_strset(&fwd_rq.conn_addr, rexec_hostname);
fwd_rq.conn_port = 22;
assh_buffer_strset(&fwd_rq.orig_addr, "127.0.0.1");
fwd_rq.orig_port = 22;

if (asshh_portfwd_open_direct_tcpip(fwd_session,
&fwd_channel, &fwd_rq))
goto disconnect;
}
continue;
}

We then need to wait for the channel open reply from fwd_host in order to know if the port forwarding is accepted.

case ASSH_EVENT_CHANNEL_CONFIRMATION:
fprintf(stderr, "SSH port forwarding ok\n");
assh_event_done(fwd_session, &fwd_event, ASSH_OK);
continue;

case ASSH_EVENT_CHANNEL_FAILURE:
fprintf(stderr, "SSH port forwarding denied\n");
assh_event_done(fwd_session, &fwd_event, ASSH_OK);
goto disconnect;

At this point, fwd_host has initiated a TCP connection to rexec_host on behalf of our application. It will start to forward any data transmitted by rexec_host to our fwd session. The library will report this as incoming channel data. When this occurs, we just have to copy as many data as we can to our software fifo, then yield to the event loop of the rexec_session.

case ASSH_EVENT_CHANNEL_DATA: {
struct assh_event_channel_data_s *ev =
&fwd_event.connection.channel_data;

/* write incoming forwarded ssh stream to our software fifo */
ev->transferred =
fifo_write(&fwd_to_rexec, ev->data.data, ev->data.size);

assh_event_done(fwd_session, &fwd_event, ASSH_OK);
return 1;
}

We are almost done with the code of the fwd session event loop. We still have to handle the channel close event that will be reported if the connection to rexec_host gets broken.

case ASSH_EVENT_CHANNEL_CLOSE:
fwd_channel = NULL;
assh_event_done(fwd_session, &fwd_event, ASSH_OK);
/* initiate a disconnect when the port forwarding terminates */
goto disconnect;

The code of the event loop ends as usual, with a default event handler. The ssh_loop_fwd function returns 0 when no more events will be reported. The disconnection request code is factored at the end of the function.

default:
/* acknowledge any unhandled event */
assh_event_done(fwd_session, &fwd_event, ASSH_OK);
}
}

return 0; /* session terminated, no more events */

disconnect:
assh_session_disconnect(fwd_session, SSH_DISCONNECT_BY_APPLICATION, NULL);
return 1;
}

The remote execution event loop [link] 

We can now focus on the event loop of the rexec session located in the ssh_loop_rexec function.

static assh_bool_t
ssh_loop_rexec(void)
{
struct assh_event_s rexec_event;

while (assh_event_get(rexec_session, &rexec_event, time(NULL)))
{
switch (rexec_event.id)
{

Reading data from the fifo in order to feed the rexec session with the ssh stream from rexec_host is the easier part. We just take care of yielding to the fwd session event loop when the fifo is empty.

case ASSH_EVENT_READ: {
struct assh_event_transport_read_s *ev =
&rexec_event.transport.read;

/* read ssh stream from our software fifo */
size_t s = fifo_read(&fwd_to_rexec, ev->buf.data, ev->buf.size);
ev->transferred = s;

assh_event_done(rexec_session, &rexec_event, ASSH_OK);

if (s == 0)
return 1; /* yield to forwarder event loop */
continue;
}

Because we do not use a fifo in the other direction, there is more work to do in order to send the rexec network stream through the port forwarding channel. We first have to make sure that the channel exists and has reached the ASSH_CHANNEL_ST_OPEN state. When this is the case, an outgoing packet is allocated and filled with some of the network stream. We also yield to the fwd session event loop if we were not able to send some data yet.

case ASSH_EVENT_WRITE: {
struct assh_event_transport_write_s *ev =
&rexec_event.transport.write;

size_t s = ev->buf.size;
uint8_t *d;

if (fwd_channel != NULL &&
assh_channel_state(fwd_channel) >= ASSH_CHANNEL_ST_OPEN &&
!assh_channel_data_alloc(fwd_channel, &d, &s, 0))
{
/* write our ssh stream to the port forwarding channel of
the other session */

memcpy(d, ev->buf.data, s);
assh_channel_data_send(fwd_channel, s);
ev->transferred = s;
}

assh_event_done(rexec_session, &rexec_event, ASSH_OK);

if (ev->transferred == 0)
return 1; /* yield to forwarder event loop */
continue;
}

Other events of the rexec session are handled in the exact same way as in the Remote command execution example application and are not detailed here.

As in the event loop of the fwd session, the function returns 0 when there are no more event to report.

default:
/* acknowledge any unhandled event */
assh_event_done(rexec_session, &rexec_event, ASSH_OK);
}
}

return 0; /* session terminated, no more events */
}

In a real world ssh2 client based on libassh, such a system independent code construct would allow forwarding a session through an arbitrary number hosts with a single user command, replacing unsafe SSH agent forwarding altogether.

Valid XHTML 1.0 StrictGenerated by diaxen on Sun Oct 25 23:30:45 2020 using MkDoc