5.2 Simple loopback server

This simple server application accepts all interactive session open messages from a connecting client and sends back any data received over open channels. When connecting using a standard client, this has the effect of echoing any character typed.

Unlike code of the previous example that handles connection protocol related events using helper functions, those events are handled directly by the application here. This example also shows how to take control of the underlying channel flow control mechanism provided by the ssh2 protocol.

In this server application, we use a single struct assh_context_s object and different struct assh_session_s objects for connecting clients. A single client can connect at the same time. The context is initialized as shown previously:

// code from examples/loopback.c:101

/* init an assh server context */
struct assh_context_s *context;

if (assh_context_create(&context, ASSH_SERVER,
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");

A server context needs to have some host keys registered. We try to load host keys from standard locations using an helper function. This may however require root privileges. That's why we fallback to creating an ephemeral host key by calling the assh_key_create function:

/* load or create host key(s) */
if (asshh_server_load_hk(context)
#ifdef CONFIG_ASSH_KEY_CREATE
&& asshh_key_create(context, assh_context_keys(context),
0, "ssh-ed25519", ASSH_ALGO_SIGN)
&& asshh_key_create(context, assh_context_keys(context),
0, "ssh-rsa", ASSH_ALGO_SIGN)
#endif
)
ERROR("Unable to load or create a host key.\n");

We can then start waiting for a client connection and initialize an struct assh_session_s object when this occurs:

while (1)
{
struct sockaddr_in con_addr;
socklen_t addr_size = sizeof(con_addr);

/** wait for client connection */
int conn = accept(sock, (struct sockaddr*)&con_addr, &addr_size);
if (conn < 0)
continue;

printf("Incoming connection\n");

/** init a session for the incoming connection */
struct assh_session_s *session;

if (assh_session_create(context, &session))
ERROR("Unable to create an assh session.\n");

Then comes the event loop:

struct assh_event_s event;

/** get events from the core. */
while (assh_event_get(session, &event, time(NULL)))
{
switch (event.id)
{

Some events are handled using helper functions, as detailed in the previous example:

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(session, &event, conn);
break;

case ASSH_EVENT_SESSION_ERROR:
/* report any error to the terminal */
fprintf(stderr, "SSH error: %s\n",
assh_error_str(event.session.error.code));
assh_event_done(session, &event, ASSH_OK);
break;

case ASSH_EVENT_USERAUTH_SERVER_USERKEY:
case ASSH_EVENT_USERAUTH_SERVER_PASSWORD:
/* let some helpers handle user authentication */
asshh_server_event_auth(session, &event);
break;

When the client sends an SSH_MSG_CHANNEL_OPEN packet to our server, this ends up reporting the ASSH_EVENT_CHANNEL_OPEN event to the application. In our case, we want to allow the client to open channels of the session type. Other types of channel will be rejected by our server:

case ASSH_EVENT_CHANNEL_OPEN: {
struct assh_event_channel_open_s *ev =
&event.connection.channel_open;

/* make our server accept interactive sessions from the client */
if (!assh_buffer_strcmp(&ev->type, "session"))
{
ev->reply = ASSH_CONNECTION_REPLY_SUCCESS;

The ssh2 connection protocol provides a flow control mechanism for channels. This allows limiting the amount of data that the remote host is allowed to send through a channel. This amount is the window size. When the remote host has consumed a part of the window by sending data, the local side has to grant more window space again. This mechanism is implemented separately in both directions.

Instead of letting the library manage the window size for us, we chose here to take control of the window size in the application. This is a wise choice in the case of a loopback server because we have to make sure that we will be able to send back any received data immediately. We do not want to be limited by the window size with some data that we would have to store until we are granted more window space. We need to have the window sizes synchronized between the two directions. That's why we grant the same initial window size and packet size that is granted by the remote host in the opposite direction:

/* disable automatic window management for the channel */
ev->win_size = ev->rwin_size;
ev->pkt_size = ev->rpkt_size;
}

assh_event_done(session, &event, ASSH_OK);
break;
}

When a channel open is acknowledged, the struct assh_channel_s object pointed by ev->ch lives until a close event is reported. In our simple example, we have no private context to attach and we do not need to keep track of it because further events will provide the pointer to the struct assh_channel_s instance again.

Setting the initial window size is not enough, we have to keep the window sizes of both directions in sync when data is exchanged over a channel. Because we disabled automatic window adjustment, we need to handle the ASSH_EVENT_CHANNEL_WINDOW event and call the assh_channel_window_adjust function. We want to increase our window size by the same amount that was granted by the remote side. This occurs when the event is reported:

case ASSH_EVENT_CHANNEL_WINDOW: {
struct assh_event_channel_window_s *ev = &event.connection.channel_window;

/* find the extra amount of bytes we are allowed to send */
size_t diff = ev->new_size - ev->old_size;

/* allow the remote host to send more bytes */
assh_status_t err = assh_channel_window_adjust(ev->ch, diff);

assh_event_done(session, &event, err);
break;
}

Note that we have to acknowledge the event before increasing the window size because most functions of the library can not be called between the pair of calls to assh_event_get and assh_event_done.

A connecting client will request execution of a shell process on the server. Even if we only want to implement a loopback server, we need to fake acceptance of this request or the client may disconnect. This requires replying to channel related requests of the shell type:

case ASSH_EVENT_REQUEST: {
struct assh_event_request_s *ev = &event.connection.request;

/* accept a shell request on any open channel,
but do not actually execute a shell process */

if (ev->ch != NULL)
if (!assh_buffer_strcmp(&ev->type, "shell"))
ev->reply = ASSH_CONNECTION_REPLY_SUCCESS;

assh_event_done(session, &event, ASSH_OK);
break;
}

Then comes handling of the data transferred over the channels. When incoming data is reported by the library, we need to allocate a buffer in order to send the same amount of data back to the client. However, the current window size of the channel may not allow sending all the data, that's why the assh_channel_data_alloc function is able adjust the size. In our case though, the function is guaranteed to let us allocate the requested size because of the control we have over the window size.

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

/* size of incoming channel data */
size_t size = ev->data.size;

/* allocate output data packet */
uint8_t *data;
assh_status_t err = assh_channel_data_alloc(ev->ch, &data, &size, 1);

We must then update the event with the amount of incoming data we were able to consume. Again, we have to acknowledge the event before sending the data back.

/* copy input data to the output buffer */
if (ASSH_STATUS(err) == ASSH_OK)
{
memcpy(data, ev->data.data, size);
ev->transferred = size;
}

/* acknowledge input data event before sending */
assh_event_done(session, &event, ASSH_OK);

if (ASSH_STATUS(err) == ASSH_OK) /* send data */
assh_channel_data_send(ev->ch, size);

break;
}

We are almost done with our loopback server. We still need to acknowledge other events:

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

When the connection closes, we cleanup the session and we are ready to wait for an other connecting client:

}

fprintf(stderr, "Connection closed\n");
assh_session_release(session);
}

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