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
63 changes: 27 additions & 36 deletions nim-test-node/regression/Dockerfile_shadow
Original file line number Diff line number Diff line change
@@ -1,62 +1,55 @@
# Dynamic-linked nim test-node, for the Shadow simulator.
#
# Mirrors Dockerfile but DROPS the static linker flags (`--passL:"-static
# -mmusl"` and `--passL:"-static-libgcc -static-libstdc++"`).
# Mirrors Dockerfile (same base, nimble and lockfile, amd64 only), plus the lsquic patch
# and -d:lsquic_no_sendmmsg. Shadow's syscall interposer only hooks dynamically linked
# ELFs, and it does not implement sendmmsg.
#
# Why: Shadow's syscall interposer is LD_PRELOAD-based and only hooks
# dynamically linked ELFs. A statically linked nim binary fails at startup with
# "Failed to verify plugin path '...': not a dynamically linked ELF".
#
# Build via kaniko on a lab worker (see shadow/kaniko-build-test-node-job.yaml).
# Pushes to radiken/dst-test-node-shadow:latest.

FROM debian:bookworm-slim AS builder
# Build via kaniko on a lab worker (see shadow/kaniko-build-test-node-job.yaml), once per
# NODE_BINARY.

ARG NODE_BINARY=node
ARG NIM_VERSION=2.2.6
FROM nimlang/nim:2.2.10 AS builder

WORKDIR /node

RUN apt-get update && apt-get install -y --no-install-recommends \
curl git build-essential ca-certificates \
libssl3 iproute2 procps xz-utils \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
git \
build-essential \
ca-certificates \
curl \
libssl3 \
iproute2 \
procps \
&& rm -rf /var/lib/apt/lists/*

RUN git config --global http.sslVerify false
RUN git config --global http.version HTTP/1.1

RUN curl -fsSL "https://nim-lang.org/download/nim-${NIM_VERSION}-linux_x64.tar.xz" -o /tmp/nim.tar.xz \
&& tar -xf /tmp/nim.tar.xz -C /tmp \
&& cp -r "/tmp/nim-${NIM_VERSION}/"* /usr/local \
&& rm -rf /usr/local/nim* \
&& mv "/tmp/nim-${NIM_VERSION}" "/usr/local/nim-${NIM_VERSION}" \
&& ln -sf "/usr/local/nim-${NIM_VERSION}/bin/nim" /usr/local/bin/nim \
&& ln -sf "/usr/local/nim-${NIM_VERSION}/bin/nimble" /usr/local/bin/nimble
# fix #1837: honor locked package URLs during installation (#1838)
RUN nimble install -y 'nimble@#07caee397d628c9e93d81048268365c4c2414a80'
ENV PATH="/root/.nimble/bin:${PATH}"

COPY . .
COPY test_node.nimble nimble.lock config.nims ./
RUN nimble refresh && nimble setup

RUN nimble refresh
RUN nimble install -dy --verbose
COPY . .

# Floor lsquic's engine tick re-arms so Shadow can advance simulated time.
# Gated by LSQUIC_TICK_FLOOR_US (unset/0 = stock). See README.md.
COPY lsquic-tick-floor.patch /tmp/lsquic-tick-floor.patch
# Floor lsquic's engine tick re-arms so Shadow can advance simulated time, and let the
# build opt out of sendmmsg. Gated by LSQUIC_TICK_FLOOR_US (unset/0 = stock). See README.md.
RUN set -eu; \
patched=0; \
for d in /root/.nimble/pkgcache/*lsquic*/ /root/.nimble/pkgs2/lsquic-*/; do \
[ -f "${d}lsquic/context/context.nim" ] || continue; \
if grep -q tickFloorUs "${d}lsquic/context/context.nim"; then echo "already patched $d"; continue; fi; \
( cd "$d" && git apply -p1 /tmp/lsquic-tick-floor.patch ); \
( cd "$d" && git apply -p1 /node/lsquic-tick-floor.patch ); \
echo "patched $d"; patched=$((patched+1)); \
done; \
[ "$patched" -gt 0 ] || { echo "no lsquic package found to patch"; exit 1; }

# Dynamic link: no `-static`, no `-mmusl`, no `-static-libgcc/-libstdc++`.
ARG NODE_BINARY=node
RUN test "${NODE_BINARY}" = "node" -o "${NODE_BINARY}" = "bootstrap"
RUN nimble c \
--os:linux --cpu:amd64 \
-d:chronicles_colors=None --threads:on --mm:refc \
-d:metrics -d:libp2p_network_protocols_metrics -d:release \
-d:lsquic_no_sendmmsg \
--out:/node/main \
"${NODE_BINARY}/main.nim" \
&& chmod +x /node/main \
Expand All @@ -65,9 +58,7 @@ RUN nimble c \
&& grep -qa LSQUIC_TICK_FLOOR_US /node/main \
&& echo "TICK-FLOOR VERIFIED IN BINARY"

# Tiny final stage: the binary + its runtime deps. The image's role is to hand
# off /node/main to an init container, not to run anything itself. We still
# include glibc/libssl/libstdc++ in case someone wants to `docker run` it.
# The binary and its runtime deps; an init container copies /node/main out for Shadow.
FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
Expand Down
7 changes: 5 additions & 2 deletions nim-test-node/regression/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ message delay and expose metrics on port `8008`.
- `Dockerfile`: simple normal Docker build for the current Docker target
platform.
- `Dockerfile_shadow`: Shadow-specific Docker build. It creates a dynamically
linked amd64 binary and applies the lsquic tick-floor patch.
linked amd64 binary, applies the lsquic tick-floor patch and builds without
`sendmmsg`, which Shadow does not implement.

## Building the normal Docker images

Expand Down Expand Up @@ -106,7 +107,9 @@ exposing both upstream in nim-lsquic.

## Maintaining the patch

It targets lsquic 0.5.4's layout, pinned in `test_node.nimble`. `nimble c`
It targets lsquic 0.8.1's layout, pinned in `nimble.lock`. It also guards
lsquic's `sendmmsg` path behind `-d:lsquic_no_sendmmsg`, which only
`Dockerfile_shadow` sets. `nimble c`
re-resolves deps before compiling, so the Dockerfile patches the package cache
(`~/.nimble/pkgcache`, what nimble copies from) rather than the installed copy.
The build asserts it patched something and greps the binary for
Expand Down
10 changes: 9 additions & 1 deletion nim-test-node/regression/env.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import strutils, os, osproc
from std/net import getPrimaryIPAddr, IpAddress, `$`
import chronos, metrics/chronos_httpserver, chronicles
from nativesockets import getHostname
from nativesockets import getHostname, getHostByName

logScope:
topics = "dst"
Expand All @@ -27,6 +27,14 @@ let

proc listenHost*(): string =
## The interface the pod routes out of; 0.0.0.0 would announce loopback too.
if inShadow:
# Shadow has no default route to probe; the host's own name resolves to its IP.
try:
for ip in getHostByName(getHostname()).addrList:
if not ip.startsWith("127.") and ip != "0.0.0.0":
return ip
except CatchableError as e:
warn "Could not resolve own hostname in Shadow", error = e.msg
try:
$getPrimaryIPAddr()
except CatchableError:
Expand Down
12 changes: 12 additions & 0 deletions nim-test-node/regression/lsquic-tick-floor.patch
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,15 @@ diff -ruN orig/lsquic/context/server.nim patched/lsquic/context/server.nim

ctx.stream_if = struct_lsquic_stream_if(
on_new_conn: onNewConn,
diff -ruN orig/lsquic/context/io.nim patched/lsquic/context/io.nim
--- orig/lsquic/context/io.nim 2026-09-16 11:11:51
+++ patched/lsquic/context/io.nim 2026-09-16 11:11:51
@@ -137,7 +137,7 @@

let specsArr = cast[ptr UncheckedArray[struct_lsquic_out_spec]](specs)

- when defined(linux):
+ when defined(linux) and not defined(lsquic_no_sendmmsg):
var
destStorages {.noinit.}: array[SendmmsgBatchSize, Sockaddr_storage]
msgs {.noinit.}: array[SendmmsgBatchSize, MMsgHdr]
5 changes: 5 additions & 0 deletions nim-test-node/regression/shutdown_utils.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import chronos
import ./env

proc waitShutdownSignal*() {.async.} =
if inShadow:
# Shadow cannot register signal handlers; it stops the process at sim end.
await newFuture[void]("waitShutdownSignal.shadow")
return
let
sigIntFut = waitSignal(SIGINT)
sigTermFut = waitSignal(SIGTERM)
Expand Down
8 changes: 5 additions & 3 deletions shadow/kaniko-build-test-node-job.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
# kubectl -n zerotesting-shadow wait --for=condition=Complete \
# --for=condition=Failed --timeout=900s job/test-node-shadow-build
#
# Builds the regression test-node (nim-test-node/regression) straight from master.
# Builds the regression test-node (nim-test-node/regression) straight from master. For the
# bootstrap, set NODE_BINARY=bootstrap and push to radiken/dst-test-node-shadow-bootstrap.
# Prereq: dockerhub-creds secret in this namespace (see shadow/kaniko-build-job.yaml).
apiVersion: batch/v1
kind: Job
Expand Down Expand Up @@ -36,9 +37,10 @@ spec:
- name: kaniko
image: gcr.io/kaniko-project/executor:v1.23.2
args:
- "--context=git://github.com/vacp2p/dst-libp2p-test-node.git#refs/heads/master"
- "--context=git://github.com/iftech/dst-libp2p-test-node.git#refs/heads/master"
- "--context-sub-path=nim-test-node/regression"
- "--dockerfile=Dockerfile_amd64_shadow"
- "--dockerfile=Dockerfile_shadow"
- "--build-arg=NODE_BINARY=node"
- "--destination=radiken/dst-test-node-shadow:latest"
- "--cache=true"
- "--cache-repo=radiken/dst-test-node-shadow-cache"
Expand Down
Loading