It’s not unusual for your printer, or your fridge to have a small web server running. The embedded web server provides important control interfaces of these devices. It’s not just the fridge and the printer, but increasingly a large number of additional devices too, from milling machines on a shop floor, to smart pumps and electrical meters.

The web server, while necessary also provides also opens up a huge attack surface. Any network connection you can reach is a network connection that will be attacked. It would therefore be awesome if we could somehow sandbox the web server, so that if it get’s compromised at all the attacker can’t penetrate further into our IoT device.

Traditionally we’d do this with hypervisor or a container, but usually our IoT devices are too small to host these technologies. How about instead, we sandbox the server by running the entire server in WebAssembly?

This blog post is a bit of a developer diary, a record of the journey to get to a working web server in WebAssembly. It details the changes and modifications necessary to get Mongoose working with WAMR.

Compiling Mongoose to WebAssembly

So my goal was to take Mongoose the embedded web server and networking library and port this to WebAssembly, ensuring that Mongoose enabled applications can run within the WebAssembly runtimes.

Note: If yo would like to skip this blog post and simply get a working HTTP server in WebAssembly, jump over to the GitHub Repository with all the information you need.

Dealing with WASI (WebAssembly System Interface) Fragmentation

At the time of writing most runtimes are based on WASI 0.1, the initial version of WASI. As you may now the WebAssembly world essential splits into two standardisation projects. The first, is often called “core-wasm” this is the byte code format, the best analogy is to think of this as a CPU, core-wasm defines the opcodes for WebAssembly. Basically every runtime supports core-wasm. The second project is called “WASI”, or the WebAssembly System Interface. WASI’s goal is to provide a common standard access to the underlying host platform upon which the core-wasm executes. This is a rather complex task.

The initial version of WASI (aka WASI 0.1) defined a common set of POSIX like functions that provided access to the underlying host system. However WASI 0.1 is not a complete POSIX implementation. It provides file system and standard in / out but it doesn’t provide networking support and it doesn’t provide threading support. Two features which would be really handy for … you guest it, a web server.

Lots of software requires networking support. To address this the WASI 0.1 runtimes started to introduce their own networking APIs, some provide a HTTP interface, and some provide socket interfaces, but they are not binary compatible. So a WebAssembly module which uses a network interface developed for one runtime, say Lunatic, will not run on another runtime, say Wasm-Edge.

One of the benefits of WebAssembly, and the reason why I enjoy using it, is that it allows existing code, usually ‘c’, to recompiled into a portable format. Most ‘c’ based network applications use a BSD Socket like API. Luckily the most popular runtime for running WebAssembly in embedded systems is WAMR and WAMR supports a pretty good BSD Socket library. It should therefore be pretty straight forward to port Mongoose to WAMR.

In short, until we have a common socket implementation available across multiple runtimes, I can’t say that this is a port of Mongoose for WebAssembly, I can only say that this is a port of Mongoose to WAMR.

It doesn’t work out of the box

So I’ve a code base in Mongoose that wants a BSD Socket API, and I’ve a runtime that has a BSD Socket API. This should all compile out of the, right?…. Errr (kinda predictably), erm, NOoooo.

Step 1 - Understanding Mongoose

The Mongoose embedded server is awesomely small, it is simply a header file and a single ‘c’ file. It’s super easy to use, just include the header file, add the .c file to your project and Bob’s your uncle you have HTTP features.

To make this work Mongoose uses a lot of pre-processor directives to detect the platform it is being compiled for and to include the correct header files and system interfaces.

Example Application

Mongoose provides a set of sample applications and until a recent refactoring contained an example http-server application. In a recent update the Mongoose folks replaced this with a code snippet on their web site. This snippet is awesome, and I’ve reproduced it below for our reading convenience. If you’d like the original you can still find it on the Mongoose GitHub repository.

#include "mongoose.h"   // To build, run: cc main.c mongoose.c

// HTTP server event handler function
void ev_handler(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_HTTP_MSG) {
    struct mg_http_message *hm = (struct mg_http_message *) ev_data;
    struct mg_http_serve_opts opts = { .root_dir = "./web_root/" };
    mg_http_serve_dir(c, hm, &opts);
  }
}

int main(void) {
  struct mg_mgr mgr;  // Declare event manager
  mg_mgr_init(&mgr);  // Initialise event manager
  mg_http_listen(&mgr, "http://0.0.0.0:8000", ev_handler, NULL);  // Setup listener
  for (;;) {          // Run an infinite event loop
    mg_mgr_poll(&mgr, 1000);
  }
  return 0;
}

This code snippet will create a simple http server and will serve the contents of the ./web_root/ folder (relative to the current path).

This won’t compile directly to WebAssembly, at least not yet. If you do try to compile it you see a bunch of error messages, like this:

The mongoose.h file contains pre-processor directives for a whole bunch of operating systems and platforms. However it doesn’t have support for Wasm. When the header file determines that it doesn’t know the target platform it automatically includes the mongoose_config.h header file.

To add support for WAMR and Wasm we can just supply a correctly configured mongoose_congfig.h file.

A Side Note: Contributor Agreements and Confusion between WebAssembly and the Various Runtimes

I could have taken the more complex approach of adding all the supporting mechanics to the mongoose.h file, if I’d have done this then every mongoose application would be able to build for WAMR. There were two issues that prevented me from doing this:

  1. Due to the WASI fragmentation, discussed above I couldn’t provide support for all Wasm runtimes, so this would only work for WAMR.
  2. Add contributions directly to mongoose.h entailed signing a contributor agreement, something I’d like to avoid, as my full time job would require me to additional approval, and I’d rather avoid this.

How Socket Support works in WAMR

Creating a mongoose_config.h for WAMR requires understanding how WAMR provides socket support. WAMR does this by providing two key elements:

  1. A c based socket API that applications can use
  2. A set of runtime specific functions Let’s initially focus on enabling the additional functions inside the runtime.

WAMR doesn’t include this functionality by default, instead we need to build the WAMR runtime and enable this functionality.

Runtime Support

Doing this is rather straight forward as the WAMR project already includes a set of socket sample applications, and a cmake file which will rebuild iwasm with all the additional functionality we need. You can find the example applications and the cmake file we need here - Socket API Sample. Simply follow the build instructions and you will end up with a socket enabled iwasm runtime.

Source Compatibility

Now we’ve got a runtime with socket capability, we need to ensure Mongoose can use it. The WAMR project provides this functionality in a lib-socket extension which applications can compile against. This is basically a static library we can either link against, or since it’s so small, we can simply add the single c file to our compilation list and include in our build process. The entire lib-socket library consists of just two files:

  • wasi_socket_ext.h
  • wasi_socket_ext.c

The wasi_socket_ext.h replaces the traditional posix like socket.h header file. This gives us a working API, and with a small modification we can add an additional support block in Mongoose and get it compiling for WAMR. Together these two files provide a WebAssembly application facing interface, behind the scenes this code will invoke function calls in the WAMR runtime.

Building our mongoose_custom.h

Now we know how everything works, we can start to build our mongoose_custom.h . We’re lucky that the vast majority of WASI APIs are libc compatible so we can take the existing linux header block as a starting point, there are a few small changes we need to make:

  • Remove the socket.h and replace it with our wasi_socket_ext.h .
  • Remove netdb.h it provides host name information, this isn’t provided for in our WAMR socket implementation.
  • Change select for poll. At the time of writing the WAMR implementation of select returns -1, an error and basically doesn’t work. Instead we can ask Mongoose to use the poll API which does work with WAMR. Select and Poll provide basically the same functionality, but have differing origin stories, but that’s probably a blog post for the future.

These changes get us a working mongoose header file!