Melang

Logo

A script language of time-sharing scheduling coroutine in single thread

View the Project on GitHub Water-Melon/Melang

TLS

This document introduces a set of functions for non-blocking TLS in Melang.

The TLS module wraps the TLS support that the underlying Melon C library exposes through mln_tcp_conn, so Melang scripts can do TLS exactly the way they already do plain TCP through the net module: every call looks synchronous, but I/O that would block yields the current coroutine to the scheduler so other coroutines keep running.

Requirements:

Import

tls = Import('tls');

Concurrency model

A TLS fd returned by tls.accept or tls.connect lives in the per-mln_lang_t connection table. Once accept / connect has returned, the fd is not owned by the originating coroutine; any coroutine that holds the integer can call tls.send / recv / handshake / close on it (a common pattern is to spawn a worker via Eval('handler.m', cfd, false, ...) and hand the fd off).

While one coroutine has a send, recv, or handshake in flight on a given fd, a call from a second coroutine on the same fd returns immediately:

The second coroutine is not queued or blocked. If you want the second coroutine to wait, gate the call with mq or another synchronisation primitive at the script level. Most code will not need this, because the natural pattern is “one coroutine owns one connection”.

Under melang -t=N the connection table is shared across all worker threads. All accesses to it happen under the lang mutex, and individual SSL objects are serialised by the same busy-flag check above, so OpenSSL’s “one thread per SSL at a time” requirement is upheld. Configuration objects (SSL_CTX wrappers built by tls.conf_new) are internally thread-safe in OpenSSL >= 1.1.0 and may be shared across coroutines and across threads.

conf_new

Build a reusable TLS configuration object. An mln_tcp_tls_conf_t in C terms — wraps an SSL_CTX plus the certificate / key / CA file paths. Cheap to share across many connections; expensive to build, so callers are expected to allocate once and reuse.

tls.conf_new(role, cert, key, ca, ciphers, verify);

Input:

Return value:

conf_free

Release a configuration object built by tls.conf_new.

tls.conf_free(conf);

Input:

Return value:

After this call, any connection still using the conf will keep working (the underlying SSL_CTX is reference-counted by OpenSSL), but the handle itself becomes invalid. Do not free a conf while new accept / connect calls might still race to use the handle.

listen

Set up a plain TCP listen socket. The same call as net.tcp_listen — TLS is layered on later, in tls.accept.

tls.listen(host, service);

Input:

Return value:

accept

Accept a single incoming TCP connection on a listen fd and wrap it in a TLS server-side connection using the given conf. The handshake is not driven by this call — call tls.handshake next, or just issue tls.recv / tls.send and the handshake will run on demand.

tls.accept(fd, conf, timeout);

Input:

Return value:

connect

Open a TCP connection to a remote peer and wrap it in a TLS client connection using the given conf. Like accept, the TLS handshake is not driven here; use tls.handshake (or just tls.send / tls.recv) afterwards.

tls.connect(host, service, conf, timeout);

Input:

Return value:

set_sni

Set the SNI (Server Name Indication) hostname on a client-side TLS fd before the handshake starts. Required by many virtual-hosted servers.

tls.set_sni(fd, hostname);

Input:

Return value:

set_verify_host

Enable RFC 6125 hostname verification against the peer certificate. Call before the handshake.

tls.set_verify_host(fd, hostname);

Input:

Return value:

Verification only kicks in when the conf was built with verify=true and a CA bundle was provided.

handshake

Drive the TLS handshake explicitly. Optional: the first send or recv call will trigger it automatically. Calling it explicitly lets the script surface a handshake-failure before the first I/O, which is often more readable in test code.

tls.handshake(fd, timeout);

Input:

Return value:

send

Encrypt the buffer and push it to the peer. Yields the coroutine while waiting on the socket; other coroutines keep running.

tls.send(fd, data, timeout);

Input:

Return value:

Small payloads typically complete inline without an event-loop trip; only when the socket buffer is full does the script suspend.

recv

Receive and decrypt a chunk of plaintext from the peer. Coalesces whatever plaintext is currently pending (one or more TLS records) into a single Melang string.

tls.recv(fd, timeout);

Input:

Return value:

For framed protocols on top of TLS the caller must loop until they have accumulated enough bytes; recv is purely a chunk delivery primitive.

close

Send a TLS close_notify alert (best-effort, non-blocking) and close the underlying socket.

tls.close(fd);

Input:

Return value:

Scripts that need a strict bidirectional close_notify should call tls.recv until it returns true before calling tls.close.

Example

A minimal echo over TLS using two coroutines on one event loop.

Generate a self-signed cert/key once:

openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout /tmp/melang_tls_test.key \
  -out    /tmp/melang_tls_test.crt \
  -subj   "/CN=localhost" \
  -days   1
//tls_server.m
sys = Import('sys');
tls = Import('tls');

conf = tls.conf_new('server',
                    '/tmp/melang_tls_test.crt',
                    '/tmp/melang_tls_test.key',
                    nil, nil, false);
lfd  = tls.listen('localhost', '18443');
cfd  = tls.accept(lfd, conf, 10000);
tls.handshake(cfd, 10000);

while (1) {
    msg = tls.recv(cfd, 10000);
    if (msg == false || msg == nil || msg == true) { break; } fi
    tls.send(cfd, msg, 10000);
}

tls.close(cfd);
tls.close(lfd);
tls.conf_free(conf);
//tls_client.m
sys = Import('sys');
tls = Import('tls');

sys.msleep(300);

conf = tls.conf_new('client', nil, nil, nil, nil, false);
fd   = tls.connect('localhost', '18443', conf, 10000);
tls.set_sni(fd, 'localhost');
tls.handshake(fd, 10000);

tls.send(fd, 'hello-from-melang', 10000);
sys.print(tls.recv(fd, 10000));

tls.close(fd);
tls.conf_free(conf);

Run both as siblings on a single Melang scheduler:

$ melang tls_server.m tls_client.m

The output is:

hello-from-melang