diff --git a/.github/workflows/library-chronyd4.8.yaml b/.github/workflows/library-chronyd4.8.yaml new file mode 100644 index 00000000..aab35521 --- /dev/null +++ b/.github/workflows/library-chronyd4.8.yaml @@ -0,0 +1,134 @@ +name: library/chronyd:4.8 + +on: + schedule: + - cron: '0 3 * * *' + + push: + branches: [main] + paths: + - 'library/chronyd/4.8/**' + - '.github/workflows/library-chronyd4.8.yaml' + - '!library/chronyd/4.8/README.md' + + pull_request: + types: [opened, synchronize, reopened] + branches: [main] + paths: + - 'library/chronyd/4.8/**' + - '.github/workflows/library-chronyd4.8.yaml' + - '!library/chronyd/4.8/README.md' + +# Automatically cancel in-progress actions on the same branch +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - plat: qemu + arch: x86_64 + - plat: fc + arch: x86_64 + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Build chronyd4.8 + uses: unikraft/kraftkit@staging + with: + loglevel: debug + workdir: library/chronyd/4.8 + runtimedir: /github/workspace/.kraftkit + plat: ${{ matrix.plat }} + arch: ${{ matrix.arch }} + push: false + output: oci://index.unikraft.io/unikraft.org/chronyd:4.8 + + - name: Archive OCI digests + uses: actions/upload-artifact@v4 + with: + name: oci-digests-${{ matrix.arch }}-${{ matrix.plat }} + path: ${{ github.workspace }}/.kraftkit/oci/digests + if-no-files-found: error + + push: + if: ${{ github.event_name == 'push' || github.event_name == 'schedule' }} + needs: [ build ] + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Login to OCI registry + uses: docker/login-action@v3 + with: + registry: index.unikraft.io + username: ${{ secrets.REG_USERNAME }} + password: ${{ secrets.REG_TOKEN }} + + - name: Retrieve, merge and push OCI digests + uses: ./.github/actions/merge-oci-digests + with: + name: index.unikraft.io/unikraft.org/chronyd:4.8 + push: true + + test: + needs: [ build ] + strategy: + fail-fast: false + matrix: + include: + - plat: qemu + arch: x86_64 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y qemu-system-x86 qemu-utils + + - name: Install KraftKit + run: curl -sSfL https://get.kraftkit.sh | sudo sh -s -- -y + + - name: Build and Run chronyd4.8 + env: + KRAFTKIT_NO_CHECK_UPDATES: true + UK_CFLAGS: "-std=gnu17" + run: | + cd library/chronyd/4.8 + kraft build --plat ${{ matrix.plat }} --arch ${{ matrix.arch }} + kraft run --rm -W -M 16M -p 1123:123/udp --plat ${{ matrix.plat }} --arch ${{ matrix.arch }} . > /tmp/kraft-chronyd.log 2>&1 & + echo $! > /tmp/kraft_pid + + - name: Wait for chronyd to serve synchronized time + run: | + cd library/chronyd/4.8 + for i in $(seq 1 180); do + if python3 test.py >/tmp/chronyd-test.log 2>&1; then + echo "chronyd on ${{ matrix.plat }} served synchronized time." + cat /tmp/chronyd-test.log + exit 0 + fi + sleep 1 + done + echo "chronyd did not serve synchronized time after 180 seconds." + cat /tmp/chronyd-test.log || true + cat /tmp/kraft-chronyd.log || true + exit 1 + + - name: Stop chronyd instance + if: ${{ always() }} + run: | + if [ -f /tmp/kraft_pid ]; then + kill "$(cat /tmp/kraft_pid)" || true + fi diff --git a/README.md b/README.md index 0083ff38..04c1b62c 100644 --- a/README.md +++ b/README.md @@ -73,3 +73,4 @@ kraft pkg ls --apps --update | [![](https://github.com/unikraft/catalog/actions/workflows/library-ruby3.2.yaml/badge.svg)](https://github.com/unikraft/catalog/actions/workflows/library-ruby3.2.yaml) | [`unikraft.org/ruby:3.2`](library/ruby/3.2) | | [![](https://github.com/unikraft/catalog/actions/workflows/library-skipper0.18.yaml/badge.svg)](https://github.com/unikraft/catalog/actions/workflows/library-skipper0.18.yaml) | [`unikraft.org/skipper:0.18`](library/skipper/0.18) | | [![](https://github.com/unikraft/catalog/actions/workflows/library-surreal1.1.yaml/badge.svg)](https://github.com/unikraft/catalog/actions/workflows/library-surreal1.1.yaml) | [`unikraft.org/surreal:1.1`](library/surreal/1.1) | +| [![](https://github.com/unikraft/catalog/actions/workflows/library-chronyd4.8.yaml/badge.svg)](https://github.com/unikraft/catalog/actions/workflows/library-chronyd4.8.yaml) | [`unikraft.org/chronyd:4.8`](library/chronyd/4.8) | diff --git a/library/chronyd/4.8/.dockerignore b/library/chronyd/4.8/.dockerignore new file mode 100644 index 00000000..73e7ae05 --- /dev/null +++ b/library/chronyd/4.8/.dockerignore @@ -0,0 +1,2 @@ +.unikraft +.config* diff --git a/library/chronyd/4.8/Dockerfile b/library/chronyd/4.8/Dockerfile new file mode 100644 index 00000000..d9dec2d1 --- /dev/null +++ b/library/chronyd/4.8/Dockerfile @@ -0,0 +1,40 @@ +# chrony built from source with HAVE_RECVMMSG removed +FROM alpine:3.24 AS build + +ARG CHRONY_VERSION=4.8 + +RUN apk add --no-cache gcc make musl-dev nettle-dev +ADD https://chrony-project.org/releases/chrony-${CHRONY_VERSION}.tar.gz /chrony.tar.gz +RUN tar xzf /chrony.tar.gz && \ + cd /chrony-${CHRONY_VERSION} && \ + ./configure --prefix=/usr --sysconfdir=/etc/chrony \ + --disable-cmdmon --disable-refclock --disable-ipv6 \ + --without-libcap --without-seccomp \ + --without-nss --without-tomcrypt \ + --without-gnutls && \ + sed -i '/HAVE_RECVMMSG/d' config.h && \ + ! grep -q HAVE_RECVMMSG config.h && \ + make -j"$(nproc)" chronyd && \ + strip chronyd && \ + cp chronyd /usr/sbin/chronyd + +# runtime dirs, staged so scratch COPY can pick them up +RUN mkdir -p /out/etc/chrony /out/run/chrony /out/var/lib/chrony + +FROM scratch + +COPY --from=build /usr/sbin/chronyd /usr/sbin/chronyd +COPY --from=build /etc/passwd /etc/passwd +COPY --from=build /etc/group /etc/group +COPY --from=build /out/etc/chrony /etc/chrony +COPY --from=build /out/run/chrony /run/chrony +COPY --from=build /out/var/lib/chrony /var/lib/chrony + +# musl loader (+ libc alias) and nettle, for SECHASH symmetric-key auth +COPY --from=build /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 +COPY --from=build /lib/ld-musl-x86_64.so.1 /lib/libc.musl-x86_64.so.1 +COPY --from=build /usr/lib/libnettle.so.8 /usr/lib/libnettle.so.8 + +# Static configuration +COPY ./conf/chrony.conf /etc/chrony/chrony.conf +COPY ./conf/resolv.conf /etc/resolv.conf diff --git a/library/chronyd/4.8/Kraftfile b/library/chronyd/4.8/Kraftfile new file mode 100644 index 00000000..3120813d --- /dev/null +++ b/library/chronyd/4.8/Kraftfile @@ -0,0 +1,142 @@ +spec: v0.6 + +name: chronyd + +rootfs: ./Dockerfile + +# -d foreground, -x to never touch the system clock (its syscalls are stubs anyway) +# -u root skips the privilege drop (single-user unikernel) +# -L 0 informational logging to stdout +cmd: ["/usr/sbin/chronyd", "-d", "-x", "-u", "root", "-L", "0"] + +template: + source: https://github.com/unikraft/app-elfloader.git + version: staging + +unikraft: + source: https://github.com/unikraft/unikraft.git + version: staging + kconfig: + # Configurations options for app-elfloader + # (they can't be part of the template atm) + CONFIG_LIBPOSIX_PROCESS_ARCH_PRCTL: 'y' + CONFIG_APPELFLOADER_BRK: 'y' + CONFIG_APPELFLOADER_CUSTOMAPPNAME: 'y' + CONFIG_APPELFLOADER_STACK_NBPAGES: 128 + CONFIG_APPELFLOADER_VFSEXEC_EXECBIT: 'n' + CONFIG_APPELFLOADER_VFSEXEC: 'y' + CONFIG_APPELFLOADER_HFS: 'y' + CONFIG_APPELFLOADER_HFS_ETCRESOLVCONF: 'n' + CONFIG_APPELFLOADER_HFS_ETCHOSTS: 'y' + CONFIG_APPELFLOADER_HFS_ETCHOSTNAME: 'y' + CONFIG_APPELFLOADER_HFS_REPLACEEXIST: 'y' + # Unikraft options + CONFIG_HAVE_PAGING_DIRECTMAP: 'y' + CONFIG_HAVE_PAGING: 'y' + CONFIG_I8042: 'y' + CONFIG_LIBPOSIX_ENVIRON_ENVP0: "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + CONFIG_LIBPOSIX_ENVIRON_ENVP1: "LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib" + CONFIG_LIBPOSIX_ENVIRON_ENVP2: "HOME=/" + CONFIG_LIBPOSIX_ENVIRON: 'y' + CONFIG_LIBPOSIX_ENVIRON_LIBPARAM: 'y' + CONFIG_LIBPOSIX_ENVIRON_LIBPARAM_MAXCOUNT: '64' + CONFIG_LIBPOSIX_EVENTFD: 'y' + CONFIG_LIBPOSIX_FDIO: 'y' + CONFIG_LIBPOSIX_FDTAB: 'y' + CONFIG_LIBPOSIX_FUTEX: 'y' + CONFIG_LIBPOSIX_MMAP: 'y' + CONFIG_LIBPOSIX_NETLINK: 'y' + CONFIG_LIBPOSIX_PIPE: 'y' + CONFIG_LIBPOSIX_POLL: 'y' + CONFIG_LIBPOSIX_PROCESS: 'y' + CONFIG_LIBPOSIX_PROCESS_MULTITHREADING: 'y' + CONFIG_LIBPOSIX_SOCKET: 'y' + CONFIG_LIBPOSIX_SYSINFO: 'y' + CONFIG_LIBPOSIX_TIME: 'y' + CONFIG_LIBPOSIX_TIMERFD: 'y' + CONFIG_LIBPOSIX_UNIXSOCKET: 'y' + CONFIG_LIBPOSIX_USER_GID: 0 + CONFIG_LIBPOSIX_USER_GROUPNAME: "root" + CONFIG_LIBPOSIX_USER_UID: 0 + CONFIG_LIBPOSIX_USER_USERNAME: "root" + CONFIG_LIBPOSIX_USER: 'y' + CONFIG_LIBSYSCALL_SHIM_HANDLER_ULTLS: 'y' + CONFIG_LIBSYSCALL_SHIM_HANDLER: 'y' + CONFIG_LIBSYSCALL_SHIM_LEGACY_VERBOSE: 'y' + CONFIG_LIBSYSCALL_SHIM: 'y' + CONFIG_LIBUKALLOCPOOL: 'y' + CONFIG_LIBUKBLKDEV_MAXNBQUEUES: '1' + CONFIG_LIBUKBLKDEV_DISPATCHERTHREADS: 'y' + CONFIG_LIBUKBLKDEV_SYNC_IO_BLOCKED_WAITING: 'y' + CONFIG_LIBUKBLKDEV: 'y' + CONFIG_LIBUKBOOT_BANNER_MINIMAL: 'y' + CONFIG_LIBUKBOOT_MAINTHREAD: 'y' + CONFIG_LIBUKBOOT_SHUTDOWNREQ_HANDLER: 'y' + CONFIG_LIBUKCPIO: 'y' + CONFIG_LIBUKDEBUG_CRASH_SCREEN: 'y' + CONFIG_LIBUKDEBUG_ENABLE_ASSERT: 'y' + CONFIG_LIBUKDEBUG_PRINT_SRCNAME: 'n' + CONFIG_LIBUKDEBUG_PRINT_TIME: 'y' + CONFIG_LIBUKDEBUG_PRINTK_ERR: 'y' + CONFIG_LIBUKDEBUG_PRINTK: 'y' + CONFIG_LIBUKDEBUG: 'y' + CONFIG_LIBUKFALLOC: 'y' + CONFIG_LIBUKMPI: 'n' + CONFIG_LIBUKSIGNAL: 'y' + CONFIG_LIBUKRANDOM_DEVFS: 'y' + CONFIG_LIBUKRANDOM: 'y' + CONFIG_LIBUKRANDOM_GETRANDOM: 'y' + CONFIG_LIBUKVMEM_DEMAND_PAGE_IN_SIZE: 12 + CONFIG_LIBUKVMEM_PAGEFAULT_HANDLER_PRIO: 4 + CONFIG_LIBUKVMEM: 'y' + CONFIG_LIBVFSCORE: 'n' + CONFIG_LIBPOSIX_VFS_FSTAB: 'y' + CONFIG_LIBPOSIX_VFS_FSTAB_BUILTIN: 'y' + CONFIG_LIBPOSIX_VFS_FSTAB_BUILTIN_INITRD: 'y' + CONFIG_LIBPOSIX_VFS_FSTAB_MOUNT_DEV: 'y' + CONFIG_LIBUK9P: 'y' + CONFIG_OPTIMIZE_DEADELIM: 'y' + CONFIG_OPTIMIZE_LTO: 'y' + CONFIG_PAGING: 'y' + CONFIG_STACK_SIZE_PAGE_ORDER: 4 # 128 * 4K = 512K + CONFIG_UKPLAT_MEMREGION_MAX_COUNT: 64 + CONFIG_LIBUKNETDEV_EINFO_LIBPARAM: 'y' + + # Debug options + # CONFIG_LIBUKDEBUG_PRINTD: 'y' + # CONFIG_LIBUKDEBUG_PRINTK_INFO: 'y' + # CONFIG_LIBSYSCALL_SHIM_STRACE: 'y' + # CONFIG_LIBSYSCALL_SHIM_DEBUG: 'y' + +libraries: + lwip: + source: https://github.com/unikraft/lib-lwip.git + version: staging + kconfig: + CONFIG_LWIP_LOOPIF: 'y' + CONFIG_LWIP_UKNETDEV: 'y' + CONFIG_LWIP_LOOPBACK: 'y' + CONFIG_LWIP_TCP: 'y' + CONFIG_LWIP_UDP: 'y' + CONFIG_LWIP_RAW: 'y' + CONFIG_LWIP_WND_SCALE: 'y' + CONFIG_LWIP_TCP_KEEPALIVE: 'y' + CONFIG_LWIP_THREADS: 'y' + CONFIG_LWIP_HEAP: 'y' + CONFIG_LWIP_SOCKET: 'y' + CONFIG_LWIP_AUTOIFACE: 'y' + CONFIG_LWIP_IPV4: 'y' + # IPv6 off on purpose: with AF_INET6 available chrony prefers the (unreachable) v6 addresses of upstream servers and never syncs; + CONFIG_LWIP_IPV6: 'n' + CONFIG_LWIP_DHCP: 'y' + CONFIG_LWIP_DNS: 'y' + CONFIG_LWIP_NUM_TCPCON: 64 + CONFIG_LWIP_NUM_TCPLISTENERS: 64 + CONFIG_LWIP_ICMP: 'y' + libelf: + source: https://github.com/unikraft/lib-libelf.git + version: staging + +targets: +- fc/x86_64 +- qemu/x86_64 diff --git a/library/chronyd/4.8/README.md b/library/chronyd/4.8/README.md new file mode 100644 index 00000000..f969be84 --- /dev/null +++ b/library/chronyd/4.8/README.md @@ -0,0 +1,64 @@ +# chronyd 4.8 + +This directory contains the definition for the `unikraft.org/chronyd:4.8` image, +an NTP server running [chrony](https://chrony-project.org/) in binary +compatibility mode. + +To run this image, [install Unikraft's companion command-line toolchain `kraft`](https://unikraft.org/docs/cli) +and then you can run: + +```console +kraft run -M 16M -p 1123:123/udp --plat qemu --arch x86_64 unikraft.org/chronyd:4.8 +``` + +If the `--plat` argument is left out, it defaults to `qemu`. +If the `--arch` argument is left out, it defaults to your system's CPU architecture. + +The `/udp` suffix is required: `kraft` forwards TCP when no protocol is given, +and NTP queries then get no reply from an otherwise healthy server. Host port +123 needs root to bind, so the example uses a high port; serve real clients with +`-p 123:123/udp` as root. + +Wait ~20 s after boot for the upstream sync (iburst), then query the server: + +```console +python3 test.py +``` + +It reports the stratum, the reference ID of the selected upstream and the served +time, exiting non-zero until chronyd has synchronized. + +## Building from source + +To build the image locally, clone this repository and `cd` into this directory. +Then run: + +```console +kraft build --plat qemu --arch x86_64 . +``` + +To run the locally built image, use `.` (_dot_, the current directory) as the +final argument: + +```console +kraft run -M 16M -p 1123:123/udp --plat qemu . +``` + +## Notes + +- chrony is compiled from source with `HAVE_RECVMMSG` removed from `config.h`. + Unikraft has no `recvmmsg` syscall and the stock Alpine binary has no runtime + fallback, so it busy-loops on a socket it can never drain. +- The configuration is baked in at build time from + [conf/chrony.conf](conf/chrony.conf); a unikernel has no shell to generate one + at startup. Edit it and rebuild to change servers. +- chronyd runs with `-x`, so it never adjusts the guest clock, whose set/adjust + syscalls are stubs on Unikraft. It still serves corrected time to clients. +- [conf/chrony.conf](conf/chrony.conf) sets `allow all`, so the instance answers + any client that can reach the port. Narrow it (`allow 192.168.0.0/16`) before + exposing it to an untrusted network. + +## See also + +- [How to run unikernels locally in Unikraft's Documentation](https://unikraft.org/docs/cli/running). +- [chrony's documentation](https://chrony-project.org/documentation.html) diff --git a/library/chronyd/4.8/conf/chrony.conf b/library/chronyd/4.8/conf/chrony.conf new file mode 100644 index 00000000..79bd6f9c --- /dev/null +++ b/library/chronyd/4.8/conf/chrony.conf @@ -0,0 +1,17 @@ +# Static equivalent of docker-chronyd's generated config +server 0.pool.ntp.org iburst +server 1.pool.ntp.org iburst +server 2.pool.ntp.org iburst +server 3.pool.ntp.org iburst +server time.cloudflare.com iburst +server time1.google.com iburst +server time2.google.com iburst +server time3.google.com iburst +server time4.google.com iburst + +# Uncomment if /var/lib/chrony gets a persistent mount. +#driftfile /var/lib/chrony/chrony.drift + +# Serve time to any client that can reach us, required for the server role: +# chronyd denies NTP client access entirely when no allow directive is set. +allow all diff --git a/library/chronyd/4.8/conf/resolv.conf b/library/chronyd/4.8/conf/resolv.conf new file mode 100644 index 00000000..4e7546e8 --- /dev/null +++ b/library/chronyd/4.8/conf/resolv.conf @@ -0,0 +1,3 @@ +nameserver 1.1.1.1 +nameserver 8.8.8.8 +nameserver 9.9.9.9 diff --git a/library/chronyd/4.8/test.py b/library/chronyd/4.8/test.py new file mode 100755 index 00000000..a2d083e7 --- /dev/null +++ b/library/chronyd/4.8/test.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Query the chronyd unikernel: test.py [host] [port] (default 127.0.0.1 1123). + +Exits non-zero unless chronyd answers and has synchronized upstream. +""" + +import socket +import struct +import sys +import time + +REF_TIME_1970 = 2208988800 # NTP epoch (1900) -> Unix epoch (1970) + + +def RequestTimefromNtp(addr="127.0.0.1", port=1123): + client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + client.settimeout(5) + client.sendto(b"\x1b" + 47 * b"\0", (addr, port)) + data, _ = client.recvfrom(48) + t = struct.unpack("!12I", data)[10] - REF_TIME_1970 + stratum = data[1] + refid = ".".join(str(b) for b in data[12:16]) # selected upstream + return stratum, refid, time.ctime(t) + + +if __name__ == "__main__": + host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1" + port = int(sys.argv[2]) if len(sys.argv) > 2 else 1123 + try: + stratum, refid, served = RequestTimefromNtp(host, port) + except TimeoutError: + sys.exit(f"no reply from {host}:{port}") + + print("stratum:", stratum) + print("refid: ", refid) + print("time: ", served) + + if stratum == 0: + sys.exit("answering but not synchronized yet, retry shortly")