Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -536,11 +536,20 @@ jobs:
permissions: {}
steps:
- run: |
results=("${{ needs.build-image.result }}" "${{ needs.bench-linux.result }}" "${{ needs.bench-windows.result }}")
for r in "${results[@]}"; do
if [[ "$r" != "success" && "$r" != "skipped" ]]; then
echo "Job failed with result: $r"
exit 1
declare -A results=(
[build-image]="${{ needs.build-image.result }}"
[bench-linux]="${{ needs.bench-linux.result }}"
[bench-windows]="${{ needs.bench-windows.result }}"
)
failed=0
for job in "${!results[@]}"; do
r="${results[$job]}"
if [[ "$r" != "success" ]]; then
echo "FAIL: $job = $r"
failed=1
fi
done
if [[ "$failed" -eq 1 ]]; then
exit 1
fi
echo "All checks passed"
12 changes: 5 additions & 7 deletions .github/workflows/host-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,9 @@ jobs:
permissions: {}
steps:
- run: |
results=("${{ needs.checks.result }}")
for r in "${results[@]}"; do
if [[ "$r" != "success" && "$r" != "skipped" ]]; then
echo "Job failed with result: $r"
exit 1
fi
done
r="${{ needs.checks.result }}"
if [[ "$r" != "success" ]]; then
echo "FAIL: checks = $r"
exit 1
fi
echo "All checks passed"
34 changes: 29 additions & 5 deletions .github/workflows/test-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ jobs:
- python-agent
- python-agent-driver
- powershell
- networking-py
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -232,6 +233,10 @@ jobs:
memory: "1Gi"
args: "-- -NoProfile -File /scripts/hello.ps1"
expect: "Hello, World! From PowerShell on Hyperlight"
- example: networking-py
memory: "512Mi"
args: "--net -- /urllib_get.py"
expect: "SUCCESS: urllib GET worked!"
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -402,6 +407,7 @@ jobs:
- python-agent
- python-agent-driver
- powershell
- networking-py
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -559,6 +565,10 @@ jobs:
memory: "1Gi"
args: "-- -NoProfile -File /scripts/hello.ps1"
expect: "Hello, World! From PowerShell on Hyperlight"
- example: networking-py
memory: "512Mi"
args: "--net -- /urllib_get.py"
expect: "SUCCESS: urllib GET worked!"
steps:
- uses: actions/checkout@v4

Expand Down Expand Up @@ -814,11 +824,25 @@ jobs:
permissions: {}
steps:
- run: |
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 }}")
for r in "${results[@]}"; do
if [[ "$r" != "success" && "$r" != "skipped" ]]; then
echo "Job failed with result: $r"
exit 1
# Every job must succeed. "skipped" means a dependency failed
# and the job never ran — that must fail the gate, not sneak
# through as green.
declare -A results=(
[build-example]="${{ needs.build-example.result }}"
[runtime-test]="${{ needs.runtime-test.result }}"
[package-images-for-windows]="${{ needs.package-images-for-windows.result }}"
[runtime-test-windows]="${{ needs.runtime-test-windows.result }}"
[pyhl-snapshot-test]="${{ needs.pyhl-snapshot-test.result }}"
)
failed=0
for job in "${!results[@]}"; do
r="${results[$job]}"
if [[ "$r" != "success" ]]; then
echo "FAIL: $job = $r"
failed=1
fi
done
if [[ "$failed" -eq 1 ]]; then
exit 1
fi
echo "All checks passed"
31 changes: 31 additions & 0 deletions examples/networking-py/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Networking Python example on Hyperlight
#
# Extends the Python base runtime with networking test scripts
# and DNS configuration.

ARG BASE=ghcr.io/hyperlight-dev/hyperlight-unikraft/python-base:latest
FROM ${BASE} AS rootfs
COPY http_get.py /http_get.py
COPY echo_server.py /echo_server.py
COPY urllib_get.py /urllib_get.py
COPY https_test.py /https_test.py

# --- CPIO rootfs builder ---
FROM alpine:3.20 AS cpio
RUN apk add --no-cache cpio findutils ca-certificates
COPY --from=rootfs / /rootfs/

# DNS configuration for glibc's getaddrinfo (added in the cpio stage
# because the python-base image has no shell)
RUN mkdir -p /rootfs/etc && \
echo "nameserver 8.8.8.8" > /rootfs/etc/resolv.conf && \
echo "nameserver 8.8.4.4" >> /rootfs/etc/resolv.conf && \
echo "hosts: files dns" > /rootfs/etc/nsswitch.conf && \
echo "127.0.0.1 localhost" > /rootfs/etc/hosts

# CA certificates for TLS
RUN mkdir -p /rootfs/etc/ssl/certs /rootfs/usr/lib/ssl && \
cp /etc/ssl/certs/ca-certificates.crt /rootfs/etc/ssl/certs/ && \
ln -sf /etc/ssl/certs/ca-certificates.crt /rootfs/usr/lib/ssl/cert.pem

RUN cd /rootfs && find . | cpio -o -H newc > /output.cpio 2>/dev/null
59 changes: 59 additions & 0 deletions examples/networking-py/Justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# networking-py on Hyperlight
#
# just rootfs - Build rootfs via Docker (cross-platform)
# just build - Build kernel
# just run-get - Run HTTP GET test
# just run-echo - Run echo server
# just clean - Remove build artifacts

set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
export DOCKER_BUILDKIT := "0"

kernel := ".unikraft/build/networking-py-hyperlight_hyperlight-x86_64"
initrd := "initrd.cpio"
memory := "512Mi"
image := "networking-py-hyperlight"

# Run HTTP GET test (raw sockets)
run-get:
hyperlight-unikraft {{kernel}} --initrd {{initrd}} --memory {{memory}} --net -- /http_get.py

# Run HTTP GET test (urllib — high-level stdlib)
run-urllib:
hyperlight-unikraft {{kernel}} --initrd {{initrd}} --memory {{memory}} --net -- /urllib_get.py

# Run HTTPS (TLS) test
run-https:
hyperlight-unikraft {{kernel}} --initrd {{initrd}} --memory {{memory}} --net -- /https_test.py

# Run echo server
run-echo:
hyperlight-unikraft {{kernel}} --initrd {{initrd}} --memory {{memory}} --net -- /echo_server.py

# Build rootfs via Docker (cross-platform)
rootfs:
docker build --platform linux/amd64 --target cpio -t {{image}}-cpio .
- docker rm -f {{image}}-tmp
docker create --name {{image}}-tmp {{image}}-cpio /bin/true
docker cp {{image}}-tmp:/output.cpio ./{{initrd}}
docker rm -f {{image}}-tmp

# Build kernel via kraft (Linux)
[unix]
build:
-kraft-hyperlight build --plat hyperlight --arch x86_64

# Pull pre-built kernel from GHCR (Windows — no kernel published yet)
[windows]
build:
Write-Host "No pre-built kernel for networking-py yet. Build on Linux first."

# Clean build artifacts
[unix]
clean:
rm -rf .unikraft {{initrd}}

[windows]
clean:
if (Test-Path .unikraft) { Remove-Item -Recurse -Force .unikraft }
if (Test-Path {{initrd}}) { Remove-Item -Force {{initrd}} }
30 changes: 30 additions & 0 deletions examples/networking-py/echo_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Simple TCP echo server for testing host-proxied networking.

Binds to 0.0.0.0:8080 and echoes back whatever a client sends,
prefixed with "ECHO: ". Exits after the first client disconnects.
"""
import socket

HOST = "0.0.0.0"
PORT = 8080

srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind((HOST, PORT))
srv.listen(1)
print(f"Listening on {HOST}:{PORT}...")

conn, addr = srv.accept()
print(f"Connection from {addr}")

while True:
data = conn.recv(1024)
if not data:
break
reply = b"ECHO: " + data
conn.sendall(reply)
print(f"Echoed {len(data)} bytes")

conn.close()
srv.close()
print("Server done.")
40 changes: 40 additions & 0 deletions examples/networking-py/http_get.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Simple HTTP GET test for Hyperlight networking.

Uses raw sockets to avoid DNS dependency for initial testing.
Connects to example.com and issues a GET /.
"""
import socket
import sys

HOST = "172.66.147.243"
PORT = 80
PATH = "/"

print(f"Connecting to {HOST}:{PORT}...")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))
print("Connected!")

request = f"GET {PATH} HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"
sock.sendall(request.encode())
print("Request sent, waiting for response...")

response = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break
response += chunk

sock.close()

text = response.decode("utf-8", errors="replace")
lines = text.split("\r\n")
print(f"Status: {lines[0]}")
print(f"Body length: {len(text)} bytes")

if "200 OK" in lines[0]:
print("SUCCESS: HTTP GET worked!")
else:
print(f"UNEXPECTED: {lines[0]}")
sys.exit(1)
11 changes: 11 additions & 0 deletions examples/networking-py/https_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import urllib.request
import sys

print("Testing HTTPS (TLS) through proxied sockets...")
try:
r = urllib.request.urlopen('https://api.github.com', timeout=10)
print(f"Status: {r.status}")
print("SUCCESS: HTTPS works!")
except Exception as e:
print(f"FAILED: {e}")
sys.exit(1)
83 changes: 83 additions & 0 deletions examples/networking-py/kraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
specification: '0.6'
name: networking-py-hyperlight

unikraft:
source: https://github.com/unikraft/unikraft.git
version: plat-hyperlight
kconfig:
# Platform
CONFIG_PLAT_HYPERLIGHT: 'y'
CONFIG_PAGING: 'n'
CONFIG_LIBUKVMEM: 'n'
CONFIG_LIBUKINTCTLR_HYPERLIGHT: 'y'
CONFIG_HYPERLIGHT_MAX_GUEST_LOG_LEVEL: 4

# Suppress all kernel logging
CONFIG_LIBUKPRINT_KLVL_CRIT: 'y'
CONFIG_LIBUKPRINT_PRINT_TIME: 'n'
CONFIG_LIBUKPRINT_PRINT_SRCNAME: 'n'
CONFIG_LIBUKBOOT_BANNER_NONE: 'y'

# Size optimization
CONFIG_OPTIMIZE_SIZE: 'y'
CONFIG_OPTIMIZE_PIE: 'y'

# VFS and initrd support - cpiovfs for zero-copy mount
CONFIG_LIBVFSCORE: 'y'
CONFIG_LIBVFSCORE_AUTOMOUNT_CI: 'y'
CONFIG_LIBVFSCORE_AUTOMOUNT_CI_CUSTOM: 'y'
CONFIG_LIBVFSCORE_AUTOMOUNT_CI0_MP: '/'
CONFIG_LIBVFSCORE_AUTOMOUNT_CI0_DRIVER: 'cpiovfs'
CONFIG_LIBCPIOVFS: 'y'
CONFIG_LIBUKCPIO: 'y'

# ELF loader - execute Python binary
CONFIG_APPELFLOADER: 'y'
CONFIG_APPELFLOADER_VFSEXEC: 'y'
CONFIG_APPELFLOADER_CUSTOMAPPNAME: 'n'
CONFIG_APPELFLOADER_VFSEXEC_ENVPATH: 'n'
CONFIG_APPELFLOADER_VFSEXEC_PATH: '/usr/local/bin/python3'
CONFIG_APPELFLOADER_VFSEXEC_EXECBIT: 'n'
CONFIG_LIBPOSIX_ENVIRON_ENVP0: 'PATH=/usr/local/bin:/usr/bin:/bin'
CONFIG_LIBPOSIX_ENVIRON_ENVP1: 'LD_LIBRARY_PATH=/usr/local/lib'
CONFIG_LIBELF: 'y'

# Random number support (required for Python)
CONFIG_LIBUKRANDOM_CMDLINE_SEED: 'y'
CONFIG_LIBUKRANDOM_GETRANDOM: 'y'

# Threading support
CONFIG_LIBUKBOOT_MAINTHREAD: 'y'
CONFIG_LIBPOSIX_PROCESS_ARCH_PRCTL: 'y'
CONFIG_LIBPOSIX_PROCESS_MULTITHREADING: 'y'
CONFIG_LIBPOSIX_FUTEX: 'y'

# MPI + mmap
CONFIG_LIBUKMPI: 'y'
CONFIG_LIBUKMMAP: 'y'

# devfs + /dev/hcall (required for host-proxied networking)
CONFIG_LIBDEVFS: 'y'
CONFIG_LIBDEVFS_AUTOMOUNT: 'y'
CONFIG_HYPERLIGHT_HCALL: 'y'

# Networking: host-proxied sockets
CONFIG_LIBPOSIX_SOCKET: 'y'
CONFIG_LIBHOSTSOCK: 'y'

# FD infrastructure (required by posix-socket)
CONFIG_LIBPOSIX_FDIO: 'y'
CONFIG_LIBPOSIX_FDTAB: 'y'
CONFIG_LIBUKFILE: 'y'

libraries:
app-elfloader:
source: https://github.com/unikraft/app-elfloader.git
version: plat-hyperlight
libelf:
source: https://github.com/unikraft/lib-libelf.git
version: staging

targets:
- architecture: x86_64
platform: hyperlight
23 changes: 23 additions & 0 deletions examples/networking-py/urllib_get.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""High-level HTTP GET using urllib (Python stdlib).

Demonstrates that Python's standard urllib.request module works
over the host-proxied socket layer with full DNS resolution.
"""
import urllib.request
import sys

URL = "http://example.com/"

print(f"Fetching {URL} ...")
try:
with urllib.request.urlopen(URL, timeout=10) as resp:
body = resp.read().decode("utf-8", errors="replace")
print(f"Status: {resp.status}")
print(f"Body length: {len(body)} bytes")
if "Example Domain" in body:
print("SUCCESS: urllib GET worked!")
else:
print("WARNING: unexpected body content")
except Exception as e:
print(f"FAILED: {e}")
sys.exit(1)
Loading
Loading