Skip to content

Commit 4460ae2

Browse files
authored
Merge pull request #97 from hyperlight-dev/feat/networking-examples
feat: add Go and .NET Kestrel HTTP server examples
2 parents c2092b3 + f8d20d3 commit 4460ae2

14 files changed

Lines changed: 825 additions & 7 deletions

File tree

.github/workflows/test-examples.yml

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ jobs:
6161
- python-agent-driver
6262
- powershell
6363
- networking-py
64+
- go-http
65+
- dotnet-http
6466
steps:
6567
- uses: actions/checkout@v4
6668

@@ -223,6 +225,14 @@ jobs:
223225
- example: networking-py
224226
args: "--port 8080 -- /echo_server_test.py"
225227
expect: "SUCCESS: bind\\+listen on port 8080 allowed"
228+
- example: go-http
229+
args: "--port 8080 -- /bin/server"
230+
expect: "Hello from Hyperlight-Unikraft!"
231+
http_port: "8080"
232+
- example: dotnet-http
233+
args: "--port 8080 -- /app/KestrelHyperlight"
234+
expect: "Hello from Kestrel on Hyperlight!"
235+
http_port: "8080"
226236
steps:
227237
- uses: actions/checkout@v4
228238

@@ -338,6 +348,42 @@ jobs:
338348
mkdir -p "$mount_dir"
339349
mount_args="--mount $mount_dir:/host"
340350
fi
351+
# HTTP server examples: start in background, poll, curl, kill.
352+
http_port="${{ matrix.http_port }}"
353+
if [ -n "$http_port" ]; then
354+
mem_args=""
355+
if [ -n "$memory" ]; then
356+
mem_args="-m $memory"
357+
fi
358+
hyperlight-unikraft -q $mem_args "$kernel" --initrd "$cpio" ${{ matrix.args }} &
359+
server_pid=$!
360+
sleep 3
361+
ready=0
362+
for i in $(seq 1 30); do
363+
if curl -s --max-time 10 "http://127.0.0.1:${http_port}" >/dev/null 2>&1; then
364+
ready=1
365+
break
366+
fi
367+
sleep 1
368+
done
369+
if [ "$ready" -ne 1 ]; then
370+
echo "FAIL: server did not become ready within timeout"
371+
kill "$server_pid" 2>/dev/null || true
372+
exit 1
373+
fi
374+
response=$(curl -s --max-time 10 "http://127.0.0.1:${http_port}")
375+
kill "$server_pid" 2>/dev/null || true
376+
wait "$server_pid" 2>/dev/null || true
377+
echo "=== HTTP response ==="
378+
echo "$response"
379+
if echo "$response" | grep -qF "$expect"; then
380+
echo "PASS: matched /$expect/"
381+
else
382+
echo "FAIL: did not match /$expect/"
383+
exit 1
384+
fi
385+
exit 0
386+
fi
341387
case "${{ matrix.driver }}" in
342388
multifn-test)
343389
cmd=(timeout 60 /home/runner/work/hyperlight-unikraft/hyperlight-unikraft/host/target/release/multifn-test "$kernel" "$cpio")
@@ -403,6 +449,8 @@ jobs:
403449
- python-agent-driver
404450
- powershell
405451
- networking-py
452+
- go-http
453+
- dotnet-http
406454
steps:
407455
- uses: actions/checkout@v4
408456

@@ -554,6 +602,14 @@ jobs:
554602
- example: networking-py
555603
args: "--port 8080 -- /echo_server_test.py"
556604
expect: "SUCCESS: bind\\+listen on port 8080 allowed"
605+
- example: go-http
606+
args: "--port 8080 -- /bin/server"
607+
expect: "Hello from Hyperlight-Unikraft!"
608+
http_port: "8080"
609+
- example: dotnet-http
610+
args: "--port 8080 -- /app/KestrelHyperlight"
611+
expect: "Hello from Kestrel on Hyperlight!"
612+
http_port: "8080"
557613
steps:
558614
- uses: actions/checkout@v4
559615

@@ -622,6 +678,54 @@ jobs:
622678
$PSNativeCommandUseErrorActionPreference = $false
623679
$ErrorActionPreference = 'Continue'
624680
681+
# HTTP server examples: start in background, poll, curl, kill.
682+
$httpPort = '${{ matrix.http_port }}'
683+
if ($httpPort -ne '') {
684+
$argList = @()
685+
if ($runArgs -ne '') {
686+
$argList = $runArgs.Split(' ') | Where-Object { $_ -ne '' }
687+
}
688+
$memArgs = @()
689+
if ($memory -ne '') {
690+
$memArgs = @('-m', $memory)
691+
}
692+
$stderrLog = Join-Path $env:RUNNER_TEMP 'hl-stderr.log'
693+
$proc = Start-Process -FilePath 'hyperlight-unikraft' `
694+
-ArgumentList (@('-q') + $memArgs + @($kernel, '--initrd', $cpio) + $argList) `
695+
-PassThru -NoNewWindow -RedirectStandardError $stderrLog
696+
# Give the server time to boot the unikernel and start listening
697+
Start-Sleep -Seconds 5
698+
$ready = $false
699+
for ($i = 0; $i -lt 30; $i++) {
700+
try {
701+
$null = Invoke-WebRequest -Uri "http://127.0.0.1:${httpPort}" -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
702+
$ready = $true
703+
break
704+
} catch {
705+
Start-Sleep -Seconds 1
706+
}
707+
}
708+
if (-not $ready) {
709+
Write-Host "FAIL: server did not become ready within timeout"
710+
Write-Host "=== stderr (last 200 lines) ==="
711+
Get-Content $stderrLog -Tail 200 -ErrorAction SilentlyContinue
712+
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
713+
exit 1
714+
}
715+
$raw = (Invoke-WebRequest -Uri "http://127.0.0.1:${httpPort}" -UseBasicParsing -TimeoutSec 10).Content
716+
if ($raw -is [byte[]]) { $resp = [System.Text.Encoding]::UTF8.GetString($raw) } else { $resp = $raw }
717+
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
718+
Write-Host "=== HTTP response ==="
719+
Write-Host $resp
720+
if ($resp -match [regex]::Escape($expect)) {
721+
Write-Host "PASS: matched /$expect/"
722+
exit 0
723+
} else {
724+
Write-Host "FAIL: did not match /$expect/"
725+
exit 1
726+
}
727+
}
728+
625729
switch ($driver) {
626730
'multifn-test' {
627731
$out = & multifn-test $kernel $cpio 2>&1

examples/dotnet-http/Dockerfile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
2+
WORKDIR /app
3+
COPY Program.cs KestrelHyperlight.csproj ./
4+
RUN dotnet publish -c Release -r linux-musl-x64 --self-contained \
5+
-p:PublishTrimmed=true \
6+
-p:InvariantGlobalization=true \
7+
-o /out
8+
9+
FROM scratch AS rootfs
10+
11+
# .NET app + runtime (self-contained)
12+
COPY --from=build /out/ /app/
13+
14+
# musl dynamic linker
15+
COPY --from=build /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1
16+
17+
# C++ runtime libraries (needed by .NET runtime)
18+
COPY --from=build /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6
19+
COPY --from=build /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1
20+
21+
# --- CPIO rootfs builder (used by: docker build --target cpio) ---
22+
FROM alpine:3.20 AS cpio
23+
RUN apk add --no-cache cpio findutils
24+
COPY --from=rootfs / /rootfs/
25+
RUN cd /rootfs && find . | cpio -o -H newc > /output.cpio 2>/dev/null

examples/dotnet-http/Justfile

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# dotnet-http (Kestrel) on Hyperlight
2+
#
3+
# just run - Run the HTTP server (--port implies --net)
4+
# just rootfs - Build rootfs via Docker (cross-platform)
5+
# just build - Build/pull kernel
6+
# just clean - Remove build artifacts
7+
8+
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
9+
export DOCKER_BUILDKIT := "0"
10+
11+
kernel := ".unikraft/build/dotnet-http-hyperlight_hyperlight-x86_64"
12+
initrd := "initrd.cpio"
13+
memory := "128Mi"
14+
image := "dotnet-http-hyperlight"
15+
16+
# Run the HTTP server (curl http://localhost:8080 to test)
17+
run:
18+
hyperlight-unikraft {{kernel}} --initrd {{initrd}} --memory {{memory}} --port 8080 -- /app/KestrelHyperlight
19+
20+
# Build rootfs via Docker (cross-platform)
21+
rootfs:
22+
docker build --platform linux/amd64 --target cpio -t {{image}}-cpio .
23+
- docker rm -f {{image}}-tmp
24+
docker create --name {{image}}-tmp {{image}}-cpio /bin/true
25+
docker cp {{image}}-tmp:/output.cpio ./{{initrd}}
26+
docker rm -f {{image}}-tmp
27+
28+
# Build kernel via kraft (Linux)
29+
[unix]
30+
build:
31+
-kraft-hyperlight build --plat hyperlight --arch x86_64
32+
33+
# Pull pre-built kernel from GHCR (Windows — no kernel published yet)
34+
[windows]
35+
build:
36+
Write-Host "No pre-built kernel for dotnet-http yet. Build on Linux first."
37+
38+
# Clean + rebuild everything
39+
rebuild: clean build rootfs
40+
41+
# Clean build artifacts
42+
[unix]
43+
clean:
44+
rm -rf .unikraft {{initrd}}
45+
46+
[windows]
47+
clean:
48+
if (Test-Path .unikraft) { Remove-Item -Recurse -Force .unikraft }
49+
if (Test-Path {{initrd}}) { Remove-Item -Force {{initrd}} }
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
<PropertyGroup>
3+
<TargetFramework>net9.0</TargetFramework>
4+
<ImplicitUsings>enable</ImplicitUsings>
5+
<InvariantGlobalization>true</InvariantGlobalization>
6+
</PropertyGroup>
7+
</Project>

examples/dotnet-http/Program.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
var builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions());
2+
builder.WebHost.UseKestrelCore();
3+
4+
var app = builder.Build();
5+
app.Urls.Add("http://0.0.0.0:8080");
6+
7+
app.Run(async context =>
8+
{
9+
await context.Response.WriteAsync("Hello from Kestrel on Hyperlight!");
10+
});
11+
12+
Console.WriteLine("Listening on :8080");
13+
await app.RunAsync();

examples/dotnet-http/kraft.yaml

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
specification: '0.6'
2+
name: dotnet-http-hyperlight
3+
4+
unikraft:
5+
source: https://github.com/unikraft/unikraft.git
6+
version: plat-hyperlight
7+
kconfig:
8+
# Platform
9+
CONFIG_PLAT_HYPERLIGHT: 'y'
10+
CONFIG_PAGING: 'n'
11+
CONFIG_LIBUKVMEM: 'n'
12+
CONFIG_LIBUKINTCTLR_HYPERLIGHT: 'y'
13+
CONFIG_HYPERLIGHT_MAX_GUEST_LOG_LEVEL: 0
14+
15+
# Suppress kernel logging for performance
16+
CONFIG_LIBUKPRINT_KLVL_CRIT: 'y'
17+
CONFIG_LIBUKPRINT_PRINT_TIME: 'n'
18+
CONFIG_LIBUKPRINT_PRINT_SRCNAME: 'n'
19+
CONFIG_LIBUKBOOT_BANNER_NONE: 'y'
20+
21+
# Size optimization
22+
CONFIG_OPTIMIZE_SIZE: 'y'
23+
CONFIG_OPTIMIZE_PIE: 'y'
24+
25+
# VFS and initrd support
26+
CONFIG_LIBVFSCORE: 'y'
27+
CONFIG_LIBVFSCORE_AUTOMOUNT_CI: 'y'
28+
CONFIG_LIBVFSCORE_AUTOMOUNT_CI_CUSTOM: 'y'
29+
CONFIG_LIBVFSCORE_AUTOMOUNT_CI0_MP: '/'
30+
CONFIG_LIBVFSCORE_AUTOMOUNT_CI0_DRIVER: 'cpiovfs'
31+
CONFIG_LIBVFSCORE_AUTOMOUNT_CI0_UKOPTS_IFINITRD0: 'y'
32+
CONFIG_LIBCPIOVFS: 'y'
33+
CONFIG_LIBUKCPIO: 'y'
34+
35+
# ELF loader - .NET apphost binary
36+
CONFIG_APPELFLOADER: 'y'
37+
CONFIG_APPELFLOADER_VFSEXEC: 'y'
38+
CONFIG_APPELFLOADER_CUSTOMAPPNAME: 'n'
39+
CONFIG_APPELFLOADER_VFSEXEC_ENVPATH: 'n'
40+
CONFIG_APPELFLOADER_VFSEXEC_PATH: '/app/KestrelHyperlight'
41+
CONFIG_APPELFLOADER_VFSEXEC_EXECBIT_CHECK: 'n'
42+
CONFIG_LIBPOSIX_ENVIRON_ENVP0: '"PATH=/app:/usr/bin:/bin"'
43+
CONFIG_LIBPOSIX_ENVIRON_ENVP1: '"DOTNET_GCHeapHardLimit=0x8000000"'
44+
CONFIG_LIBPOSIX_ENVIRON_ENVP2: '"DOTNET_gcServer=0"'
45+
CONFIG_LIBPOSIX_ENVIRON_ENVP3: '"DOTNET_EnableWriteXorExecute=0"'
46+
CONFIG_LIBPOSIX_ENVIRON_ENVP4: '"COMPlus_EnableDiagnostics=0"'
47+
CONFIG_LIBPOSIX_ENVIRON_ENVP5: '"DOTNET_DefaultStackSize=0x10000"'
48+
CONFIG_LIBPOSIX_ENVIRON_ENVP6: '"hostBuilder__reloadConfigOnChange=false"'
49+
CONFIG_LIBELF: 'y'
50+
51+
# Random number support
52+
CONFIG_LIBUKRANDOM_CMDLINE_SEED: 'y'
53+
CONFIG_LIBUKRANDOM_GETRANDOM: 'y'
54+
55+
# Stack size (order 9 = 2^9 pages = 2MB, needed for CoreCLR's 1.5MB alloca)
56+
CONFIG_STACK_SIZE_PAGE_ORDER: 9
57+
58+
# Threading support (required for .NET runtime)
59+
CONFIG_LIBUKBOOT_MAINTHREAD: 'y'
60+
CONFIG_LIBPOSIX_PROCESS_ARCH_PRCTL: 'y'
61+
CONFIG_LIBPOSIX_PROCESS_MULTITHREADING: 'y'
62+
CONFIG_LIBPOSIX_PROCESS_SIGNAL: 'y'
63+
CONFIG_LIBPOSIX_FUTEX: 'y'
64+
CONFIG_LIBUKSCHED: 'y'
65+
CONFIG_LIBUKSCHEDCOOP: 'y'
66+
CONFIG_LIBUKMPI: 'y'
67+
CONFIG_LIBUKLOCK: 'y'
68+
CONFIG_LIBUKLOCK_SEMAPHORE: 'y'
69+
CONFIG_LIBUKLOCK_MUTEX: 'y'
70+
71+
# mmap needed by CoreCLR memory management
72+
CONFIG_LIBUKMMAP: 'y'
73+
74+
# Event/polling support
75+
CONFIG_LIBPOSIX_POLL: 'y'
76+
CONFIG_LIBPOSIX_EVENTFD: 'y'
77+
CONFIG_LIBPOSIX_FDIO: 'y'
78+
CONFIG_LIBPOSIX_FDTAB: 'y'
79+
CONFIG_LIBUKFILE: 'y'
80+
81+
# devfs + /dev/hcall (required for host-proxied networking)
82+
CONFIG_LIBDEVFS: 'y'
83+
CONFIG_LIBDEVFS_AUTOMOUNT: 'y'
84+
CONFIG_HYPERLIGHT_HCALL: 'y'
85+
86+
# Networking: host-proxied sockets
87+
CONFIG_LIBPOSIX_SOCKET: 'y'
88+
CONFIG_LIBHOSTSOCK: 'y'
89+
90+
91+
libraries:
92+
app-elfloader:
93+
source: https://github.com/unikraft/app-elfloader.git
94+
version: plat-hyperlight
95+
libelf:
96+
source: https://github.com/unikraft/lib-libelf.git
97+
version: staging
98+
99+
targets:
100+
- architecture: x86_64
101+
platform: hyperlight

examples/go-http/Dockerfile

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
FROM golang:1.21.3-bookworm AS build
2+
3+
WORKDIR /src
4+
5+
COPY ./main.go /src/main.go
6+
7+
RUN set -xe; \
8+
CGO_ENABLED=0 \
9+
GOARCH=amd64 \
10+
go build \
11+
-buildmode=pie \
12+
-ldflags="-linkmode=internal" \
13+
-o /server main.go \
14+
;
15+
16+
# Strip PT_INTERP from the static PIE binary: Go's internal linker adds
17+
# /lib64/ld-linux-x86-64.so.2 as interpreter for PIE on Linux, but the
18+
# binary is fully self-contained and does not need a dynamic linker.
19+
# Unikraft's elfloader fails when the interpreter is listed but absent.
20+
# Patch the program header: change PT_INTERP (type=3) -> PT_NULL (type=0).
21+
COPY patch_interp.py /patch_interp.py
22+
RUN python3 /patch_interp.py /server
23+
24+
FROM scratch AS rootfs
25+
26+
COPY --from=build /server /bin/server
27+
28+
# --- CPIO rootfs builder (used by: docker build --target cpio) ---
29+
FROM alpine:3.20 AS cpio
30+
RUN apk add --no-cache cpio findutils
31+
COPY --from=rootfs / /rootfs/
32+
RUN cd /rootfs && find . | cpio -o -H newc > /output.cpio 2>/dev/null

0 commit comments

Comments
 (0)