To the extent possible under law, Antti-Juhani Kaijanaho has waived all copyright and related or neighboring rights to
Asynchronous transput and gnutls. This work is published from Finland.
GnuTLS is a wonderful thing. It even has a thick manual but nevertheless its documentation is severely lacking from the programmer s point of view (and there doesn t even seem to be independent howtos floating on the net). My hope is to remedy with this post, in small part, that problem.
I spent the weekend adding
STARTTLS support to the NNTP (reading) server component of Alue. Since Alue is written in C++ and uses the
Boost ASIO library as its primary concurrency framework, it seemed prudent to use ASIO s SSL sublibrary. However, the result wasn t stable and debugging it looked unappetizing. So, I wrote my own TLS layer on top of ASIO, based on gnutls.
Now, the gnutls API looks like it works only with synchronous transput: all TLS network operations are of the form do this and return when done ; for example
gnutls_handshake
returns once the handshake is finished. So how does one adapt this to asynchronous transput? Fortunately, there are (badly documented) hooks for this purpose.
An application can tell gnutls to call application-supplied functions instead of the
read(2)
and
write(2)
system calls. Thus, when setting up a TLS session but before the handshake, I do the following:
gnutls_transport_set_ptr(gs, this);
gnutls_transport_set_push_function(gs, push_static);
gnutls_transport_set_pull_function(gs, pull_static);
gnutls_transport_set_lowat(gs, 0);
Here,
gs
is my private copy of the gnutls session structure, and the
push_static
and
pull_static
are static member functions in my sesssion wrapper class. The first line tells gnutls to give the current
this
pointer (a pointer to the current session wrapper) as the first argument to them. The last line tells gnutls not to try treating the
this
pointer as a Berkeley socket.
The
pull_static
static member function just passes control on to a non-static member, for convenience:
ssize_t session::pull_static(void * th, void *b, size_t n)
return static_cast<session *>(th)->pull(b, n);
The basic idea of the
pull
function is to try to return immediately with data from a buffer, and if the buffer is empty, to fail with an error code signalling the absence of data with the possibility that data may become available later (the POSIX
EAGAIN
code):
class session
[...]
std::vector<unsigned char> ins;
size_t ins_low, ins_high;
[...]
;
ssize_t session::pull(void *b, size_t n_wanted)
unsigned char *cs = static_cast<unsigned char *>(b);
if (ins_high - ins_low > 0)
errno = EAGAIN;
return -1;
size_t n = ins_high - ins_low < n_wanted
? ins_high - ins_low
: n_wanted;
for (size_t i = 0; i < n; i++)
cs[i] = ins[ins_low+i];
ins_low += n;
return n;
Here,
ins_low
is an index to the
ins
vector specifying the first byte which has not already been passed on to gnutls, while
ins_high
is an index to the
ins
vector specifying the first byte that does not contain data read from the network. The assertions
0 <= ins_low
,
ins_low <= ins_high
and
ins_high <= ins.size()
are obvious invariants in this buffering scheme.
The push case is simpler: all one needs to do is buffer the data that gnutls wants to send, for later transmission:
class session
[...]
std::vector<unsigned char> outs;
size_t outs_low;
[...]
;
ssize_t session::push(const void *b, size_t n)
const unsigned char *cs = static_cast<const unsigned char *>(b);
for (size_t i = 0; i < n; i++)
outs.push_back(cs[i]);
return n;
The low water mark
outs_low
(indicating the first byte that has not yet been sent to the network) is not needed in the push function. It would be possible for the push callback to signal
EAGAIN
, but it is not necessary in this scheme (assuming that one does not need to establish hard buffer limits).
Once gnutls receives an
EAGAIN
condition from the pull callback, it suspends the current operation and returns to its caller with the gnutls condition
GNUTLS_E_AGAIN
. The caller must arrange for more data to become available to the pull callback (in this case by scheduling an asynchronous write of the data in the
outs
buffer scheme and scheduling an asynchronous read to the
ins
buffer scheme) and then call the operation again, allowing the operation to resume.
The code so far does not actually perform any network transput. For this, I have written two auxiliary methods:
class session
[...]
bool read_active, write_active;
[...]
;
void session::post_write()
if (write_active) return;
if (outs_low > 0 && outs_low == outs.size())
outs.clear();
outs_low = 0;
else if (outs_low > 4096)
outs.erase(outs.begin(), outs.begin() + outs_low);
outs_low = 0;
if (outs_low < outs.size())
stream.async_write_some
(boost::asio::buffer(outs.data()+outs_low,
outs.size()-outs_low),
boost::bind(&session::sent_some,
this, _1, _2));
write_active = true;
void session::post_read()
if (read_active) return;
if (ins_low > 0 && ins_low == ins.size())
ins.clear();
ins_low = 0;
ins_high = 0;
else if (ins_low > 4096)
ins.erase(ins.begin(), ins.begin() + ins_low);
ins_high -= ins_low;
ins_low = 0;
if (ins_high + 4096 >= ins.size()) ins.resize(ins_high + 4096);
stream.async_read_some(boost::asio::buffer(ins.data()+ins_high,
ins.size()-ins_high),
boost::bind(&session::received_some,
this, _1, _2));
read_active = true;
Both helpers prune the buffers when necessary. (I should really remove those magic 4096s and make them a symbolic constant.)
The data members
read_active
and
write_active
ensure that at most one asynchronous read and at most one asynchronous write is pending at any given time. My first version did not have this safeguard (instead trying to rely on the ASIO stream
reset
method to cancel any outstanding asynchronous transput at need), and the code sent some TLS records twice which is not good: sending the ServerHello twice is guaranteed to confuse the client.
Once ASIO completes an asynchronous transput request, it calls the corresponding handler:
void session::received_some(boost::system::error_code ec, size_t n)
read_active = false;
if (ec) pending_error = ec; return;
ins_high += n;
post_pending_actions();
void session::sent_some(boost::system::error_code ec, size_t n)
write_active = false;
if (ec) pending_error = ec; return;
outs_low += n;
post_pending_actions();
Their job is to update the bookkeeping and to trigger the resumption of suspended gnutls operations (which is done by
post_pending_actions
).
Now we have all the main pieces of the puzzle. The remaining pieces are obvious but rather messy, and I d rather not repeat them here (not even in a cleaned-up form). But their essential idea goes as follows:
When called by the application code or when resumed by
post_pending_actions
, an asynchronous wrapper of a gnutls operation first examines the session state for a saved error code. If one is found, it is propagated to the application using the usual ASIO techniques, and the operation is cancelled. Otherwise, the wrapper calls the actual gnutls operation. When it returns, the wrapper examines the return value. If successful completion is indicated, the handler given by the application is posted in the ASIO
io_service
for later execution. If
GNUTLS_E_AGAIN
is indicated,
post_read
and
post_write
are called to schedule actual network transput, and the wrapper is suspended (by pushing it into a queue of pending actions). If any other kind of failure is indicated, it is propagated to the application using the usual ASIO techniques.
The
post_pending_actions
merely empties the queue of pending actions and schedules the actions that it found in the queue for resumption.
The code snippets above are not my actual working code. I have mainly removed from them some irrelevant details (mostly certain template parameters, debug logging and mutex handling). I don t expect the snippets to compile. I expect I will be able to post my actual git repository to the web in a couple of days.
Please note that my (actual) code has received only rudimentary testing. I believe it is correct, but I won t be surprised to find it contains bugs in the edge cases. I hope this is, still, of some use to somebody