Skip to content

Commit 5f424bb

Browse files
authored
Merge pull request #23 from hyperlight-dev/networking
2 parents f3c3ce4 + fceb11f commit 5f424bb

19 files changed

Lines changed: 1087 additions & 56 deletions

File tree

.github/workflows/benchmarks.yml

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -536,11 +536,20 @@ jobs:
536536
permissions: {}
537537
steps:
538538
- run: |
539-
results=("${{ needs.build-image.result }}" "${{ needs.bench-linux.result }}" "${{ needs.bench-windows.result }}")
540-
for r in "${results[@]}"; do
541-
if [[ "$r" != "success" && "$r" != "skipped" ]]; then
542-
echo "Job failed with result: $r"
543-
exit 1
539+
declare -A results=(
540+
[build-image]="${{ needs.build-image.result }}"
541+
[bench-linux]="${{ needs.bench-linux.result }}"
542+
[bench-windows]="${{ needs.bench-windows.result }}"
543+
)
544+
failed=0
545+
for job in "${!results[@]}"; do
546+
r="${results[$job]}"
547+
if [[ "$r" != "success" ]]; then
548+
echo "FAIL: $job = $r"
549+
failed=1
544550
fi
545551
done
552+
if [[ "$failed" -eq 1 ]]; then
553+
exit 1
554+
fi
546555
echo "All checks passed"

.github/workflows/host-checks.yml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,9 @@ jobs:
8585
permissions: {}
8686
steps:
8787
- run: |
88-
results=("${{ needs.checks.result }}")
89-
for r in "${results[@]}"; do
90-
if [[ "$r" != "success" && "$r" != "skipped" ]]; then
91-
echo "Job failed with result: $r"
92-
exit 1
93-
fi
94-
done
88+
r="${{ needs.checks.result }}"
89+
if [[ "$r" != "success" ]]; then
90+
echo "FAIL: checks = $r"
91+
exit 1
92+
fi
9593
echo "All checks passed"

.github/workflows/test-examples.yml

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ jobs:
6565
- python-agent
6666
- python-agent-driver
6767
- powershell
68+
- networking-py
6869
steps:
6970
- uses: actions/checkout@v4
7071

@@ -232,6 +233,10 @@ jobs:
232233
memory: "1Gi"
233234
args: "-- -NoProfile -File /scripts/hello.ps1"
234235
expect: "Hello, World! From PowerShell on Hyperlight"
236+
- example: networking-py
237+
memory: "512Mi"
238+
args: "--net -- /urllib_get.py"
239+
expect: "SUCCESS: urllib GET worked!"
235240
steps:
236241
- uses: actions/checkout@v4
237242

@@ -402,6 +407,7 @@ jobs:
402407
- python-agent
403408
- python-agent-driver
404409
- powershell
410+
- networking-py
405411
steps:
406412
- uses: actions/checkout@v4
407413

@@ -559,6 +565,10 @@ jobs:
559565
memory: "1Gi"
560566
args: "-- -NoProfile -File /scripts/hello.ps1"
561567
expect: "Hello, World! From PowerShell on Hyperlight"
568+
- example: networking-py
569+
memory: "512Mi"
570+
args: "--net -- /urllib_get.py"
571+
expect: "SUCCESS: urllib GET worked!"
562572
steps:
563573
- uses: actions/checkout@v4
564574

@@ -814,11 +824,25 @@ jobs:
814824
permissions: {}
815825
steps:
816826
- run: |
817-
results=("${{ needs.build-example.result }}" "${{ needs.runtime-test.result }}" "${{ needs.package-images-for-windows.result }}" "${{ needs.runtime-test-windows.result }}" "${{ needs.pyhl-snapshot-test.result }}")
818-
for r in "${results[@]}"; do
819-
if [[ "$r" != "success" && "$r" != "skipped" ]]; then
820-
echo "Job failed with result: $r"
821-
exit 1
827+
# Every job must succeed. "skipped" means a dependency failed
828+
# and the job never ran — that must fail the gate, not sneak
829+
# through as green.
830+
declare -A results=(
831+
[build-example]="${{ needs.build-example.result }}"
832+
[runtime-test]="${{ needs.runtime-test.result }}"
833+
[package-images-for-windows]="${{ needs.package-images-for-windows.result }}"
834+
[runtime-test-windows]="${{ needs.runtime-test-windows.result }}"
835+
[pyhl-snapshot-test]="${{ needs.pyhl-snapshot-test.result }}"
836+
)
837+
failed=0
838+
for job in "${!results[@]}"; do
839+
r="${results[$job]}"
840+
if [[ "$r" != "success" ]]; then
841+
echo "FAIL: $job = $r"
842+
failed=1
822843
fi
823844
done
845+
if [[ "$failed" -eq 1 ]]; then
846+
exit 1
847+
fi
824848
echo "All checks passed"

examples/networking-py/Dockerfile

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Networking Python example on Hyperlight
2+
#
3+
# Extends the Python base runtime with networking test scripts
4+
# and DNS configuration.
5+
6+
ARG BASE=ghcr.io/hyperlight-dev/hyperlight-unikraft/python-base:latest
7+
FROM ${BASE} AS rootfs
8+
COPY http_get.py /http_get.py
9+
COPY echo_server.py /echo_server.py
10+
COPY urllib_get.py /urllib_get.py
11+
COPY https_test.py /https_test.py
12+
13+
# --- CPIO rootfs builder ---
14+
FROM alpine:3.20 AS cpio
15+
RUN apk add --no-cache cpio findutils ca-certificates
16+
COPY --from=rootfs / /rootfs/
17+
18+
# DNS configuration for glibc's getaddrinfo (added in the cpio stage
19+
# because the python-base image has no shell)
20+
RUN mkdir -p /rootfs/etc && \
21+
echo "nameserver 8.8.8.8" > /rootfs/etc/resolv.conf && \
22+
echo "nameserver 8.8.4.4" >> /rootfs/etc/resolv.conf && \
23+
echo "hosts: files dns" > /rootfs/etc/nsswitch.conf && \
24+
echo "127.0.0.1 localhost" > /rootfs/etc/hosts
25+
26+
# CA certificates for TLS
27+
RUN mkdir -p /rootfs/etc/ssl/certs /rootfs/usr/lib/ssl && \
28+
cp /etc/ssl/certs/ca-certificates.crt /rootfs/etc/ssl/certs/ && \
29+
ln -sf /etc/ssl/certs/ca-certificates.crt /rootfs/usr/lib/ssl/cert.pem
30+
31+
RUN cd /rootfs && find . | cpio -o -H newc > /output.cpio 2>/dev/null

examples/networking-py/Justfile

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# networking-py on Hyperlight
2+
#
3+
# just rootfs - Build rootfs via Docker (cross-platform)
4+
# just build - Build kernel
5+
# just run-get - Run HTTP GET test
6+
# just run-echo - Run echo server
7+
# just clean - Remove build artifacts
8+
9+
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
10+
export DOCKER_BUILDKIT := "0"
11+
12+
kernel := ".unikraft/build/networking-py-hyperlight_hyperlight-x86_64"
13+
initrd := "initrd.cpio"
14+
memory := "512Mi"
15+
image := "networking-py-hyperlight"
16+
17+
# Run HTTP GET test (raw sockets)
18+
run-get:
19+
hyperlight-unikraft {{kernel}} --initrd {{initrd}} --memory {{memory}} --net -- /http_get.py
20+
21+
# Run HTTP GET test (urllib — high-level stdlib)
22+
run-urllib:
23+
hyperlight-unikraft {{kernel}} --initrd {{initrd}} --memory {{memory}} --net -- /urllib_get.py
24+
25+
# Run HTTPS (TLS) test
26+
run-https:
27+
hyperlight-unikraft {{kernel}} --initrd {{initrd}} --memory {{memory}} --net -- /https_test.py
28+
29+
# Run echo server
30+
run-echo:
31+
hyperlight-unikraft {{kernel}} --initrd {{initrd}} --memory {{memory}} --net -- /echo_server.py
32+
33+
# Build rootfs via Docker (cross-platform)
34+
rootfs:
35+
docker build --platform linux/amd64 --target cpio -t {{image}}-cpio .
36+
- docker rm -f {{image}}-tmp
37+
docker create --name {{image}}-tmp {{image}}-cpio /bin/true
38+
docker cp {{image}}-tmp:/output.cpio ./{{initrd}}
39+
docker rm -f {{image}}-tmp
40+
41+
# Build kernel via kraft (Linux)
42+
[unix]
43+
build:
44+
-kraft-hyperlight build --plat hyperlight --arch x86_64
45+
46+
# Pull pre-built kernel from GHCR (Windows — no kernel published yet)
47+
[windows]
48+
build:
49+
Write-Host "No pre-built kernel for networking-py yet. Build on Linux first."
50+
51+
# Clean build artifacts
52+
[unix]
53+
clean:
54+
rm -rf .unikraft {{initrd}}
55+
56+
[windows]
57+
clean:
58+
if (Test-Path .unikraft) { Remove-Item -Recurse -Force .unikraft }
59+
if (Test-Path {{initrd}}) { Remove-Item -Force {{initrd}} }
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""Simple TCP echo server for testing host-proxied networking.
2+
3+
Binds to 0.0.0.0:8080 and echoes back whatever a client sends,
4+
prefixed with "ECHO: ". Exits after the first client disconnects.
5+
"""
6+
import socket
7+
8+
HOST = "0.0.0.0"
9+
PORT = 8080
10+
11+
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
12+
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
13+
srv.bind((HOST, PORT))
14+
srv.listen(1)
15+
print(f"Listening on {HOST}:{PORT}...")
16+
17+
conn, addr = srv.accept()
18+
print(f"Connection from {addr}")
19+
20+
while True:
21+
data = conn.recv(1024)
22+
if not data:
23+
break
24+
reply = b"ECHO: " + data
25+
conn.sendall(reply)
26+
print(f"Echoed {len(data)} bytes")
27+
28+
conn.close()
29+
srv.close()
30+
print("Server done.")

examples/networking-py/http_get.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Simple HTTP GET test for Hyperlight networking.
2+
3+
Uses raw sockets to avoid DNS dependency for initial testing.
4+
Connects to example.com and issues a GET /.
5+
"""
6+
import socket
7+
import sys
8+
9+
HOST = "172.66.147.243"
10+
PORT = 80
11+
PATH = "/"
12+
13+
print(f"Connecting to {HOST}:{PORT}...")
14+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
15+
sock.connect((HOST, PORT))
16+
print("Connected!")
17+
18+
request = f"GET {PATH} HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"
19+
sock.sendall(request.encode())
20+
print("Request sent, waiting for response...")
21+
22+
response = b""
23+
while True:
24+
chunk = sock.recv(4096)
25+
if not chunk:
26+
break
27+
response += chunk
28+
29+
sock.close()
30+
31+
text = response.decode("utf-8", errors="replace")
32+
lines = text.split("\r\n")
33+
print(f"Status: {lines[0]}")
34+
print(f"Body length: {len(text)} bytes")
35+
36+
if "200 OK" in lines[0]:
37+
print("SUCCESS: HTTP GET worked!")
38+
else:
39+
print(f"UNEXPECTED: {lines[0]}")
40+
sys.exit(1)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import urllib.request
2+
import sys
3+
4+
print("Testing HTTPS (TLS) through proxied sockets...")
5+
try:
6+
r = urllib.request.urlopen('https://api.github.com', timeout=10)
7+
print(f"Status: {r.status}")
8+
print("SUCCESS: HTTPS works!")
9+
except Exception as e:
10+
print(f"FAILED: {e}")
11+
sys.exit(1)

examples/networking-py/kraft.yaml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
specification: '0.6'
2+
name: networking-py-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: 4
14+
15+
# Suppress all kernel logging
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 - cpiovfs for zero-copy mount
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_LIBCPIOVFS: 'y'
32+
CONFIG_LIBUKCPIO: 'y'
33+
34+
# ELF loader - execute Python binary
35+
CONFIG_APPELFLOADER: 'y'
36+
CONFIG_APPELFLOADER_VFSEXEC: 'y'
37+
CONFIG_APPELFLOADER_CUSTOMAPPNAME: 'n'
38+
CONFIG_APPELFLOADER_VFSEXEC_ENVPATH: 'n'
39+
CONFIG_APPELFLOADER_VFSEXEC_PATH: '/usr/local/bin/python3'
40+
CONFIG_APPELFLOADER_VFSEXEC_EXECBIT: 'n'
41+
CONFIG_LIBPOSIX_ENVIRON_ENVP0: 'PATH=/usr/local/bin:/usr/bin:/bin'
42+
CONFIG_LIBPOSIX_ENVIRON_ENVP1: 'LD_LIBRARY_PATH=/usr/local/lib'
43+
CONFIG_LIBELF: 'y'
44+
45+
# Random number support (required for Python)
46+
CONFIG_LIBUKRANDOM_CMDLINE_SEED: 'y'
47+
CONFIG_LIBUKRANDOM_GETRANDOM: 'y'
48+
49+
# Threading support
50+
CONFIG_LIBUKBOOT_MAINTHREAD: 'y'
51+
CONFIG_LIBPOSIX_PROCESS_ARCH_PRCTL: 'y'
52+
CONFIG_LIBPOSIX_PROCESS_MULTITHREADING: 'y'
53+
CONFIG_LIBPOSIX_FUTEX: 'y'
54+
55+
# MPI + mmap
56+
CONFIG_LIBUKMPI: 'y'
57+
CONFIG_LIBUKMMAP: 'y'
58+
59+
# devfs + /dev/hcall (required for host-proxied networking)
60+
CONFIG_LIBDEVFS: 'y'
61+
CONFIG_LIBDEVFS_AUTOMOUNT: 'y'
62+
CONFIG_HYPERLIGHT_HCALL: 'y'
63+
64+
# Networking: host-proxied sockets
65+
CONFIG_LIBPOSIX_SOCKET: 'y'
66+
CONFIG_LIBHOSTSOCK: 'y'
67+
68+
# FD infrastructure (required by posix-socket)
69+
CONFIG_LIBPOSIX_FDIO: 'y'
70+
CONFIG_LIBPOSIX_FDTAB: 'y'
71+
CONFIG_LIBUKFILE: 'y'
72+
73+
libraries:
74+
app-elfloader:
75+
source: https://github.com/unikraft/app-elfloader.git
76+
version: plat-hyperlight
77+
libelf:
78+
source: https://github.com/unikraft/lib-libelf.git
79+
version: staging
80+
81+
targets:
82+
- architecture: x86_64
83+
platform: hyperlight
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""High-level HTTP GET using urllib (Python stdlib).
2+
3+
Demonstrates that Python's standard urllib.request module works
4+
over the host-proxied socket layer with full DNS resolution.
5+
"""
6+
import urllib.request
7+
import sys
8+
9+
URL = "http://example.com/"
10+
11+
print(f"Fetching {URL} ...")
12+
try:
13+
with urllib.request.urlopen(URL, timeout=10) as resp:
14+
body = resp.read().decode("utf-8", errors="replace")
15+
print(f"Status: {resp.status}")
16+
print(f"Body length: {len(body)} bytes")
17+
if "Example Domain" in body:
18+
print("SUCCESS: urllib GET worked!")
19+
else:
20+
print("WARNING: unexpected body content")
21+
except Exception as e:
22+
print(f"FAILED: {e}")
23+
sys.exit(1)

0 commit comments

Comments
 (0)