diff --git a/bincompat-c-http/.gitignore b/bincompat-c-http/.gitignore new file mode 100644 index 00000000..a60a136e --- /dev/null +++ b/bincompat-c-http/.gitignore @@ -0,0 +1,2 @@ +/workdir/ +/initrd.cpio diff --git a/bincompat-c-http/.scripts/README.md b/bincompat-c-http/.scripts/README.md new file mode 100644 index 00000000..24fc0f51 --- /dev/null +++ b/bincompat-c-http/.scripts/README.md @@ -0,0 +1,48 @@ +# Scripts for Bincompat C HTTP server on Unikraft + +These are companions instruction to the main instructions in the [`README`](README.md). + +Use scripts as quick actions for building and running the bincompat C HTTP server on Unikraft: + +**Note**: Run scripts from the application directory. + +## Build for / : + +```console +./.scripts/build/. +``` + +e.g.: + +```console +./.scripts/build/qemu.x86_64 +./.scripts/build/fc.x86_64 +``` + +## Build for / using a different compiler + +```console +CC=/path/to/compiler ./.scripts/build/. +``` + +e.g. + +```console +CC=/usr/bin/gcc-12 ./.scripts/build/qemu.x86_64 +CC=/usr/bin/clang ./.scripts/build/qemu.x86_64 +CC=/usr/bin/gcc-12 ./.scripts/build/fc.x86_64 +CC=/usr/bin/clang ./.scripts/build/fc.x86_64 +``` + +## Run on / + +```console +./.scripts/run/. +``` + +e.g. + +```console +./.scripts/run/qemu.x86_64 +./.scripts/run/fc.x86_64 +``` diff --git a/bincompat-c-http/.scripts/build/fc.x86_64 b/bincompat-c-http/.scripts/build/fc.x86_64 new file mode 100755 index 00000000..e57e511a --- /dev/null +++ b/bincompat-c-http/.scripts/build/fc.x86_64 @@ -0,0 +1,4 @@ +#!/bin/sh + +./.scripts/build/rootfs.x86_64 +./.scripts/build/kernel.fc.x86_64 diff --git a/bincompat-c-http/.scripts/build/kernel.fc.x86_64 b/bincompat-c-http/.scripts/build/kernel.fc.x86_64 new file mode 100755 index 00000000..18b3504d --- /dev/null +++ b/bincompat-c-http/.scripts/build/kernel.fc.x86_64 @@ -0,0 +1,4 @@ +#!/bin/sh + +. ./.scripts/common.sh +build_elfloader_base fc.x86_64 diff --git a/bincompat-c-http/.scripts/build/kernel.qemu.x86_64 b/bincompat-c-http/.scripts/build/kernel.qemu.x86_64 new file mode 100755 index 00000000..86bd156c --- /dev/null +++ b/bincompat-c-http/.scripts/build/kernel.qemu.x86_64 @@ -0,0 +1,4 @@ +#!/bin/sh + +. ./.scripts/common.sh +build_elfloader_base qemu.x86_64 diff --git a/bincompat-c-http/.scripts/build/qemu.x86_64 b/bincompat-c-http/.scripts/build/qemu.x86_64 new file mode 100755 index 00000000..96adfc6c --- /dev/null +++ b/bincompat-c-http/.scripts/build/qemu.x86_64 @@ -0,0 +1,4 @@ +#!/bin/sh + +./.scripts/build/rootfs.x86_64 +./.scripts/build/kernel.qemu.x86_64 diff --git a/bincompat-c-http/.scripts/build/rootfs.x86_64 b/bincompat-c-http/.scripts/build/rootfs.x86_64 new file mode 100755 index 00000000..dc4bf990 --- /dev/null +++ b/bincompat-c-http/.scripts/build/rootfs.x86_64 @@ -0,0 +1,6 @@ +#!/bin/sh + +rm -f initrd.cpio +make -C rootfs/ clean +make -C rootfs/ +./workdir/unikraft/support/scripts/mkcpio initrd.cpio ./rootfs/ diff --git a/bincompat-c-http/.scripts/common.sh b/bincompat-c-http/.scripts/common.sh new file mode 100644 index 00000000..be6a21bb --- /dev/null +++ b/bincompat-c-http/.scripts/common.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +elfloader_base="elfloader-net" + +if ! test -d workdir; then + mkdir workdir +fi + +ln -sfn "../../$elfloader_base/workdir" workdir/base + +build_elfloader_base() +{ + ( + cd "../$elfloader_base" && + ./setup.sh && + make distclean && + "./.scripts/build/$1" + ) || exit 1 +} diff --git a/bincompat-c-http/.scripts/run/fc.x86_64 b/bincompat-c-http/.scripts/run/fc.x86_64 new file mode 100755 index 00000000..5b40e2cd --- /dev/null +++ b/bincompat-c-http/.scripts/run/fc.x86_64 @@ -0,0 +1,20 @@ +#!/bin/sh + +if test ! -f initrd.cpio; then + echo "No initrd.cpio file found." 1>&2 + echo "Did you run the build script?" 1>&2 + exit 1 +fi + +base_kernel="workdir/base/build/elfloader_fc-x86_64" +if test ! -f "$base_kernel"; then + echo "No kernel file $base_kernel." 1>&2 + echo "Did you run the build script?" 1>&2 + exit 1 +fi + +rm -f firecracker.socket + +firecracker-x86_64 \ + --api-sock firecracker.socket \ + --config-file fc.x86_64.json diff --git a/bincompat-c-http/.scripts/run/qemu.x86_64 b/bincompat-c-http/.scripts/run/qemu.x86_64 new file mode 100755 index 00000000..5b6575a6 --- /dev/null +++ b/bincompat-c-http/.scripts/run/qemu.x86_64 @@ -0,0 +1,36 @@ +#!/bin/sh + +if test ! -f initrd.cpio; then + echo "No initrd.cpio file found." 1>&2 + echo "Did you run the build script?" 1>&2 + exit 1 +fi + +base_kernel="workdir/base/build/elfloader_qemu-x86_64" +if test ! -f "$base_kernel"; then + echo "No kernel file $base_kernel." 1>&2 + echo "Did you run the build script?" 1>&2 + exit 1 +fi + +{ +# Remove previously created network interface. +sudo ip link set dev virbr0 down +sudo ip link del dev virbr0 +sudo ip link set dev tap0 down +sudo ip link del dev tap0 +} > /dev/null 2>&1 + +# Create bridge interface for QEMU networking. +sudo ip link add dev virbr0 type bridge +sudo ip address add 172.44.0.1/24 dev virbr0 +sudo ip link set dev virbr0 up + +sudo qemu-system-x86_64 \ + -nographic \ + -m 64 \ + -cpu max \ + -netdev bridge,id=en0,br=virbr0 -device virtio-net-pci,netdev=en0 \ + -append "elfloader_qemu-x86_64 netdev.ip=172.44.0.2/24:172.44.0.1::: vfs.fstab=[ \"initrd0:/:extract::ramfs=1:\" ] -- /http_server" \ + -kernel "$base_kernel" \ + -initrd ./initrd.cpio diff --git a/bincompat-c-http/.scripts/test/.gitignore b/bincompat-c-http/.scripts/test/.gitignore new file mode 100644 index 00000000..8297d08b --- /dev/null +++ b/bincompat-c-http/.scripts/test/.gitignore @@ -0,0 +1 @@ +/log/ diff --git a/bincompat-c-http/.scripts/test/README.md b/bincompat-c-http/.scripts/test/README.md new file mode 100644 index 00000000..50c9ba12 --- /dev/null +++ b/bincompat-c-http/.scripts/test/README.md @@ -0,0 +1,14 @@ +# Testing C Hello on Unikraft + +These are companion instructions to the main instructions in the [`README`](../../README.md) and to the scripted run instructions in the [`.scripts/README`](../README.md). +Use these scripts to test C Hello on Unikraft. + +**Note**: Run scripts from the application directory. + +Use the `all.sh` script to test all builds and all available runs: + +```console +./.scripts/test/all.sh +``` + +Logs are stored in the `./.scripts/test/log/` directory. diff --git a/bincompat-c-http/.scripts/test/all.sh b/bincompat-c-http/.scripts/test/all.sh new file mode 100755 index 00000000..b0c2c0c4 --- /dev/null +++ b/bincompat-c-http/.scripts/test/all.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +test_build() +{ + printf "%-46s ... " build."$1" + ./.scripts/build/"$1" > ./.scripts/test/log/build."$1" 2>&1 + if test $? -eq 0; then + echo "PASSED" + else + echo "FAILED" + fi +} + +test_build_run() +{ + printf "%-46s ... " build."$1" + ./.scripts/build/"$1" > ./.scripts/test/log/build."$1" 2>&1 + if test $? -eq 0; then + echo "PASSED" + else + echo "FAILED" + fi + + printf " %-42s ... " run."$1" + ./.scripts/test/wrapper.sh ./.scripts/run/"$1" 2> ./.scripts/test/log/run."$1" +} + +./setup.sh +test -d ./.scripts/test/log || mkdir ./.scripts/test/log +test_build_run qemu.x86_64 +test_build_run fc.x86_64 diff --git a/bincompat-c-http/.scripts/test/common.sh b/bincompat-c-http/.scripts/test/common.sh new file mode 100644 index 00000000..897205bb --- /dev/null +++ b/bincompat-c-http/.scripts/test/common.sh @@ -0,0 +1,134 @@ +#!/bin/bash + +if test -z "$log_file"; then + log_file="/dev/null" +fi + +clean_up() +{ + { + # Clean up any previous instances. + sudo pkill -9 qemu-system + sudo pkill -9 firecracker + kraft stop --all + kraft rm --all + sudo kraft stop --all + sudo kraft rm --all + + # Remove previously created network interfaces. + sudo ip link set dev tap0 down + sudo ip link del dev tap0 + sudo ip link set dev virbr0 down + sudo ip link del dev virbr0 + } > /dev/null 2>&1 +} + +start_instance() +{ + # Start instance. + setsid --fork "$start_command" 1>&2 & + if test $? -ne 0; then + echo "Cannot start instance" 1>&2 + echo "FAILED" + clean_up + exit 1 + fi +} + +test_ping() +{ + host="$1" + + # Connect to instance. + ping -c 1 "$host" 1>&2 + if test $? -ne 0; then + echo "Cannot ping $host" 1>&2 + echo "FAILED" + clean_up + exit 1 + fi +} + +test_curl_connect() +{ + host="$1" + port="$2" + + # Query instance. + curl --retry 1 --connect-timeout 1 --max-time 10 "$host":"$port" 1>&2 + if test $? -ne 0; then + echo "Cannot connect to $host:$port" 1>&2 + echo "FAILED" + clean_up + exit 1 + fi +} + +test_curl_check_reply() +{ + host="$1" + port="$2" + message="$3" + + # Check server message contents. + curl --retry 1 --connect-timeout 1 --max-time 10 "$host":"$port" | grep "$message" 1>&2 + if test $? -ne 0; then + echo "Wrong message from $host:$port" 1>&2 + echo "FAILED" + clean_up + exit 1 + fi +} + +test_netcat_connect() +{ + host="$1" + port="$2" + + # Check connection. + netcat -w 3 "$host" "$port" < /dev/null 1>&2 + if test $? -ne 0; then + echo "Cannot connect to $host:$port" 1>&2 + echo "FAILED" + clean_up + exit 1 + fi +} + +test_redis_connect() +{ + host="$1" + port="$2" + + redis-cli -h "$host" -p "$port" < /dev/null 1>&2 + if test $? -ne 0; then + echo "Cannot connect client to Redis server at $host:$port" 1>&2 + echo "FAILED" + clean_up + exit 1 + fi +} + +test_redis_cli() +{ + host="$1" + port="$2" + + redis-cli -h "$host" -p "$port" set a 1 1>&2 + redis-cli -h "$host" -p "$port" get a 1>&2 + if test $? -eq 1; then + echo "FAILED" + echo "Cannot talk to Redis server at $host:$port" 1>&2 + clean_up + exit 1 + fi +} + +end_with_success() +{ + echo "PASSED" + clean_up + exit 0 +} + +start_command="$1" diff --git a/bincompat-c-http/.scripts/test/wrapper.sh b/bincompat-c-http/.scripts/test/wrapper.sh new file mode 100755 index 00000000..40f5066c --- /dev/null +++ b/bincompat-c-http/.scripts/test/wrapper.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +. ./.scripts/test/common.sh + +if test $# -ne 1; then + echo "Unknown arguments." 1>&2 + echo "Usage: $0 " 1>&2 + exit 1 +fi + +# Clean up previous instances. +clean_up + +# Start instance. +start_instance 2>&1 | grep "Hello from Unikraft!" 1>&2 +if test $? -ne 0; then + echo "Wrong message printed." 1>&2 + echo "FAILED" + clean_up + exit 1 +fi + +# Stop instance. +end_with_success diff --git a/bincompat-c-http/README.md b/bincompat-c-http/README.md new file mode 100644 index 00000000..3f9d91c2 --- /dev/null +++ b/bincompat-c-http/README.md @@ -0,0 +1,163 @@ +# Linux Binary-Compatible C HTTP server on Unikraft + +Build and run a simple C HTTP server compiled as a Linux ELF using the [Unikraft binary-compatibility layer](https://unikraft.org/docs/concepts/compatibility). +This application makes use of `elfloader-basic` as its base, make sure to check its [README](../elfloader-basic/README.md) for more information. +Follow the instructions below to set up, configure, build, and run an HTTP server. +Make sure you installed the [requirements](../README.md#requirements). + +At this time, this application is only compatible with QEMU and Firecracker on the x86_64 platform. + +## Quick Setup (aka TLDR) + +For a quick setup, run the commands below. +Note that you still need to install the [requirements](../README.md#requirements). +Before everything, make sure you run the [top-level `setup.sh` script](../setup.sh). + +To build and run the HTTP server Linux ELF using the Unikraft ELF Loader, first you will have to follow the [instructions](../elfloader-net/README.md#set-up) for configuring and building `elfloader-basic` - follow those instructions within the `../elfloader-net` directory normally. +After that, you can use the commands below: + +```console +./setup.sh +make -C rootfs/ clean all +./workdir/unikraft/support/scripts/mkcpio initrd.cpio ./rootfs/ +qemu-system-x86_64 \ + -nographic \ + -m 64 \ + -cpu max \ + -netdev bridge,id=en0,br=virbr0 -device virtio-net-pci,netdev=en0 \ + -append "elfloader_qemu-x86_64 netdev.ip=172.44.0.2/24:172.44.0.1::: vfs.fstab=[ \"initrd0:/:extract::ramfs=1:\" ] -- /http_server" \ + -kernel "../elfloader-net/workdir/build/elfloader_qemu-x86_64" \ + -initrd ./initrd.cpio +``` + +This will run the Unikraft ELF Loader with an HTTP server Linux ELF. +You can see how to test it in the ["Test" section](#test). +To close the virtual machine, see the instructions in the ["Close QEMU" section](#close-qemu). + +If you are experimenting with your own code or you encounter a bug which prevents the virtual machine from stopping, see the instructions in the ["Forcefully Terminating a Machine" section](#forcefully-terminating-a-machine). + +Information about every step and about other types of builds is detailed below. + +## Prepare the Linux ELF + +The source for the ELF application resides in `rootfs/http_server.c`. + +Whenever the code is changed, the `make` command below should be executed, after which the root filesystem may be rebuilt to reflect the changes (see [below](#build-the-filesystem)). + +Build the Linux ELF to be used by the ELF Loader using: + +```console +make -C rootfs/ clean all +``` + +The resulting ELF will be packed in an initial ramdisk CPIO file and will be passed to the ELF Loader at runtime. + +## Set Up + +Set up the required repositories. +For this, you have two options: + +1. Use the `setup.sh` script: + + ```console + ./setup.sh + ``` + + It will create symbolic links to the required repositories in `../repos/`. + Be sure to run the [top-level `setup.sh` script](../setup.sh). + + If you want use a custom variant of repositories (e.g. apply your own patch, make modifications), update it accordingly in the `../repos/` directory. + +1. Have your custom setup of repositories in the `workdir/` directory. + Clone, update and customize repositories to your own needs. + +## Clean + +While not required, you can clean the previous build artifacts: + +```console +make -C rootfs/ clean +``` + +## Build the Filesystem + +The filesystem is to be packed into `initrd.cpio`, an initial ramdisk CPIO file. +Use the command below for that: + +```console +rm -f initrd.cpio +./workdir/unikraft/support/scripts/mkcpio initrd.cpio ./rootfs/ +``` + +## Run + +Run the resulting image using the corresponding platform tool. +Firecracker requires KVM support. + +A successful run will show a message such as the one below: + +```text +Powered by +o. .o _ _ __ _ +Oo Oo ___ (_) | __ __ __ _ ' _) :_ +oO oO ' _ `| | |/ / _)' _` | |_| _) +oOo oOO| | | | | (| | | (_) | _) :_ + OoOoO ._, ._:_:_,\_._, .__,_:_, \___) + Pan 0.19.0~e4d07100 +Hello from Unikraft! +``` + +This means that the ELF Loader loaded and ran successfully the `http_server` binary in `rootfs/`. + +### Run on QEMU/x86_64 + +```console +qemu-system-x86_64 \ + -nographic \ + -m 64 \ + -cpu max \ + -netdev bridge,id=en0,br=virbr0 -device virtio-net-pci,netdev=en0 \ + -append "elfloader_qemu-x86_64 netdev.ip=172.44.0.2/24:172.44.0.1::: vfs.fstab=[ \"initrd0:/:extract::ramfs=1:\" ] -- /http_server" \ + -kernel "../elfloader-net/workdir/build/elfloader_qemu-x86_64" \ + -initrd ./initrd.cpio +``` + +### Run on Firecracker/x86_64 + +Note that you must build the `elfloader-basic` Firecracker kernel before this step. + +```console +rm -f firecracker.socket +firecracker-x86_64 --config-file fc.x86_64.json --api-sock firecracker.socket +``` + +The user running the above command must be able to use KVM. +Typically this means being part of the `kvm` group. +Otherwise, run the command above as root or prefixed by `sudo`. + +### Forcefully Terminating a Machine + +To close the QEMU virtual machine, use the `Ctrl+a x` keyboard shortcut; +that is press the `Ctrl` and `a` keys at the same time and then, separately, press the `x` key. + +To close the Firecracker virtual machine, open another console and use the command: + +```console +sudo pkill -f firecracker +``` + +## Customize + +The program provided is a simple demonstration of a statically-linked executable, which means it doesn't require anything but the ELF itself to run correctly within the Unikraft kernel. + +It's important to note that the executable must be compiled as a PIE (_Position Independent Executable_). + +Both of these details are captured by the `-static-pie` flag inside `rootfs/Makefile`. + +### Dynamic Linking + +Besides statically-linked binaries, the `elfloader` layer also supports dynamically-linked executables. + +In order to set this up, the dynamic libraries (also called shared objects on Linux) have to be placed in the appropriate locations within the root filesystem (including the dynamic linker / loader, e.g. `/lib64/ld-linux-x86-64.so.2`). + +For more details and further instructions, check out [this document](https://github.com/unikraft/app-elfloader/blob/3bb91cc6f4305f9fb75f137ca86c5c8ca7abdd41/README.md#executing-elf-binaries). diff --git a/bincompat-c-http/fc.x86_64.json b/bincompat-c-http/fc.x86_64.json new file mode 100644 index 00000000..700f21f5 --- /dev/null +++ b/bincompat-c-http/fc.x86_64.json @@ -0,0 +1,21 @@ +{ + "boot-source": { + "kernel_image_path": "../elfloader-basic/workdir/build/elfloader_fc-x86_64", + "boot_args": "elfloader_fc-x86_64 netdev.ip=172.44.0.2/24:172.44.0.1::: vfs.fstab=[ \"initrd0:/:extract::ramfs=1:\" ] -- /http_server", + "initrd_path": "initrd.cpio" + }, + "drives": [], + "machine-config": { + "vcpu_count": 1, + "mem_size_mib": 64, + "smt": false, + "track_dirty_pages": false + }, + "network-interfaces": [ + { + "iface_id": "net1", + "guest_mac": "06:00:ac:10:00:02", + "host_dev_name": "tap0" + } + ] +} diff --git a/bincompat-c-http/rootfs/.gitignore b/bincompat-c-http/rootfs/.gitignore new file mode 100644 index 00000000..7deb3748 --- /dev/null +++ b/bincompat-c-http/rootfs/.gitignore @@ -0,0 +1 @@ +/http_server diff --git a/bincompat-c-http/rootfs/Makefile b/bincompat-c-http/rootfs/Makefile new file mode 100644 index 00000000..56ff7c0a --- /dev/null +++ b/bincompat-c-http/rootfs/Makefile @@ -0,0 +1,12 @@ +CFLAGS = -fPIC -Wall +LDFLAGS = -static-pie + +.PHONY: all clean + +all: http_server + +http_server: http_server.c + $(CC) $(LDFLAGS) $(CFLAGS) -o $@ $^ + +clean: + -rm -f http_server diff --git a/bincompat-c-http/rootfs/http_server.c b/bincompat-c-http/rootfs/http_server.c new file mode 100644 index 00000000..d8793660 --- /dev/null +++ b/bincompat-c-http/rootfs/http_server.c @@ -0,0 +1,81 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ +/* + * Copyright (c) 2023, Unikraft GmbH and the Unikraft Authors. + */ + +#include +#include +#include +#include +#include +#include + +#define LISTEN_PORT 8080 +static const char reply[] = "HTTP/1.1 200 OK\r\n" \ + "Content-Type: text/html\r\n" \ + "Content-Length: 12\r\n" \ + "Connection: close\r\n" \ + "\r\n" \ + "Bye, World!\n"; + +#define BUFLEN 2048 +static char recvbuf[BUFLEN]; + +int main(int argc __attribute__((unused)), + char *argv[] __attribute__((unused))) +{ + int rc = 0; + int srv, client; + ssize_t n; + struct sockaddr_in srv_addr; + + srv = socket(AF_INET, SOCK_STREAM, 0); + if (srv < 0) { + fprintf(stderr, "Failed to create socket: %d\n", errno); + goto out; + } + + srv_addr.sin_family = AF_INET; + srv_addr.sin_addr.s_addr = INADDR_ANY; + srv_addr.sin_port = htons(LISTEN_PORT); + + rc = bind(srv, (struct sockaddr *) &srv_addr, sizeof(srv_addr)); + if (rc < 0) { + fprintf(stderr, "Failed to bind socket: %d\n", errno); + goto out; + } + + /* Accept one simultaneous connection */ + rc = listen(srv, 1); + if (rc < 0) { + fprintf(stderr, "Failed to listen on socket: %d\n", errno); + goto out; + } + + printf("Listening on port %d...\n", LISTEN_PORT); + while (1) { + client = accept(srv, NULL, 0); + if (client < 0) { + fprintf(stderr, + "Failed to accept incoming connection: %d\n", + errno); + goto out; + } + + /* Receive some bytes (ignore errors) */ + read(client, recvbuf, BUFLEN); + + /* Send reply */ + n = write(client, reply, sizeof(reply) - 1); + if (n < 0) + fprintf(stderr, "Failed to send a reply\n"); + else + printf("Sent a reply\n"); + + /* Close connection */ + close(client); + } + +out: + return rc; +} diff --git a/bincompat-c-http/setup.sh b/bincompat-c-http/setup.sh new file mode 100755 index 00000000..eb53896b --- /dev/null +++ b/bincompat-c-http/setup.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +check_exists_and_create_symlink() +{ + path="$1" + + if ! test -d workdir/"$path"; then + if ! test -d ../repos/"$path"; then + echo "No directory ../repos/$path. Run the top-level setup.sh script first." 1>&2 + exit 1 + fi + depth=$(echo "$path" | awk -F / '{ print NF }') + if test "$depth" -eq 1; then + ln -sfn ../../repos/"$path" workdir/"$path" + elif test "$depth" -eq 2; then + ln -sfn ../../../repos/"$path" workdir/"$path" + else + echo "Unknown depth of path $path." 1>&2 + exit 1 + fi + fi +} + +if ! test -d workdir; then + mkdir workdir +fi + +if ! test -d workdir/libs; then + mkdir workdir/libs +fi + +if ! test -d workdir/apps; then + mkdir workdir/apps +fi + +check_exists_and_create_symlink "unikraft"