diff --git a/docker-compose.arm.yml b/docker-compose.arm.yml index a0dc8be..43fddff 100644 --- a/docker-compose.arm.yml +++ b/docker-compose.arm.yml @@ -19,8 +19,9 @@ services: platform: linux/amd64 i2pd_yggdrasil: + # Build from local ARM context in this repo to avoid read-only upstream mirror constraints. build: - context: ./i2pd_yggdrasil_docker/src + context: ./i2pd_yggdrasil_arm dockerfile: Dockerfile platform: linux/arm64 diff --git a/docker-compose.yml b/docker-compose.yml index 4d8301a..370d25f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -64,7 +64,8 @@ services: interval: 30s timeout: 10s retries: 3 - start_period: 60s + # Key generation + peer discovery can take several minutes on emulated ARM. + start_period: 15m sysctls: - "net.ipv6.conf.all.disable_ipv6=0" mac_address: ce:22:b8:0e:6e:78 diff --git a/i2pd_yggdrasil_arm/Dockerfile b/i2pd_yggdrasil_arm/Dockerfile new file mode 100644 index 0000000..eb1d87e --- /dev/null +++ b/i2pd_yggdrasil_arm/Dockerfile @@ -0,0 +1,148 @@ +# syntax=docker/dockerfile:1 +ARG UBUNTU_VERSION=24.04 + +# I2P build stage +# Build on target platform so produced binaries match runtime architecture. +FROM --platform=$TARGETPLATFORM ubuntu:${UBUNTU_VERSION} AS builder_i2pd +ARG I2PD_VERSION=2.58.0 +ARG I2PD_COMPILER=gcc +ARG BUILDPLATFORM +ARG TARGETPLATFORM +ARG TARGETARCH + +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + apt-get -y upgrade && \ + apt-get install -y \ + $I2PD_COMPILER \ + git make cmake debhelper \ + libboost-date-time-dev \ + libboost-filesystem-dev \ + libboost-program-options-dev \ + libboost-system-dev \ + libssl-dev \ + zlib1g-dev \ + libminiupnpc-dev && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /BUILD_I2PD/ +RUN git config --global advice.detachedHead false && \ + git clone --depth 1 --branch $I2PD_VERSION https://github.com/PurpleI2P/i2pd.git + +WORKDIR /BUILD_I2PD/i2pd/build +RUN cmake -DCMAKE_BUILD_TYPE=Release -DWITH_AESNI=${AESNI_SUPPORT:-OFF} -DWITH_UPNP=ON . && \ + make -j$(nproc) + +# Yggdrasil build stage +# Build on target platform so produced binaries match runtime architecture. +FROM --platform=$TARGETPLATFORM ubuntu:${UBUNTU_VERSION} AS builder_yggdrasil +ARG YGGDRASIL_VERSION=v0.5.12 +ARG BUILDPLATFORM +ARG TARGETPLATFORM +ARG TARGETARCH + +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + apt-get -y upgrade && \ + apt-get install -y git golang && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /BUILD_YGGDRASIL/ +RUN git config --global advice.detachedHead false && \ + git clone --depth 1 --branch $YGGDRASIL_VERSION https://github.com/yggdrasil-network/yggdrasil-go.git + +ENV CGO_ENABLED=0 +WORKDIR /BUILD_YGGDRASIL/yggdrasil-go +# Add genkeys to build and build binaries +RUN sed -i 's/yggdrasil yggdrasilctl/yggdrasil yggdrasilctl genkeys/g' build && \ + ./build + +# Utility scripts for enhanced Yggdrasil functionality +RUN git clone --depth 1 --branch v0 https://github.com/oldnick85/yggdrasil_get_keys.git /UTILS/yggdrasil_get_keys && \ + git clone --depth 1 --branch v4 https://github.com/oldnick85/yggdrasil_find_public_peers.git /UTILS/yggdrasil_find_public_peers && \ + rm -rf /UTILS/yggdrasil_get_keys/.git /UTILS/yggdrasil_find_public_peers/.git + +# Final runtime image +FROM ubuntu:${UBUNTU_VERSION} +ARG TARGETARCH + +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + apt-get -y upgrade && \ + apt-get install -y \ + libboost-date-time-dev \ + libboost-filesystem-dev \ + libboost-program-options-dev \ + libboost-system-dev \ + libssl3 \ + zlib1g \ + libminiupnpc17 \ + git \ + python3 \ + python3-pip \ + iputils-ping \ + ca-certificates && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ + update-ca-certificates + +# ==== UTILS ==== +WORKDIR /UTILS/ +COPY --from=builder_yggdrasil /UTILS/ /UTILS/ + +# Install Python dependencies securely +RUN python3 -m pip install --no-cache-dir --break-system-packages \ + -r /UTILS/yggdrasil_get_keys/requirements.txt \ + -r /UTILS/yggdrasil_find_public_peers/requirements.txt + +# Cache public peers during build for fallback +RUN python3 /UTILS/yggdrasil_find_public_peers/yggdrasil_find_public_peers.py \ + --yggdrasil-conf="" \ + --yggdrasil-peers-json="/UTILS/yggdrasil_find_public_peers/public_peers.json" || true + +# ==== I2P CONFIGURATION ==== +# I2P Service Ports +# Web console +EXPOSE 7070 +# HTTP proxy +EXPOSE 4444 +# SOCKS proxy +EXPOSE 4447 +# SAM bridge +EXPOSE 7656 +# BOB bridge +EXPOSE 2827 +# I2CP +EXPOSE 7654 +# I2PControl +EXPOSE 7650 +# Main listener +EXPOSE 10765 + +WORKDIR /I2PD/ +COPY --from=builder_i2pd /BUILD_I2PD/i2pd/build/i2pd . +COPY --from=builder_i2pd /BUILD_I2PD/i2pd/contrib/certificates ./certificates +COPY ./i2pd.conf . + +# Increase file descriptor limit for better performance +RUN ulimit -n 4096 + +# ==== YGGDRASIL CONFIGURATION ==== +# Yggdrasil listener +EXPOSE 10654 + +WORKDIR /YGGDRASIL/ +COPY --from=builder_yggdrasil /BUILD_YGGDRASIL/yggdrasil-go/yggdrasil . +COPY --from=builder_yggdrasil /BUILD_YGGDRASIL/yggdrasil-go/yggdrasilctl . +COPY --from=builder_yggdrasil /BUILD_YGGDRASIL/yggdrasil-go/genkeys . +COPY ./yggdrasil.conf . + +# ==== FINAL SETUP ==== +WORKDIR / +COPY ./entrypoint.sh . +RUN chmod +x /entrypoint.sh + +# Health check to ensure services are running +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD bash -c 'pgrep i2pd && pgrep yggdrasil' + +CMD ["bash", "/entrypoint.sh"] diff --git a/i2pd_yggdrasil_arm/entrypoint.sh b/i2pd_yggdrasil_arm/entrypoint.sh new file mode 100644 index 0000000..372ebfb --- /dev/null +++ b/i2pd_yggdrasil_arm/entrypoint.sh @@ -0,0 +1,65 @@ +#!/bin/bash +set -e # Exit on any error + +echo "Starting I2P over Yggdrasil setup..." + +# Function to handle graceful shutdown +cleanup() { + echo "Received shutdown signal, stopping services..." + kill -TERM $i2pd_pid $yggdrasil_pid 2>/dev/null + wait + echo "Services stopped gracefully." + exit 0 +} + +trap cleanup SIGTERM SIGINT + +# Generate strong Yggdrasil address (https://yggdrasil-network.github.io/configuration.html#generating-stronger-addresses-and-prefixes) +echo "Generating Yggdrasil keys..." +python3 /UTILS/yggdrasil_get_keys/yggdrasil_get_keys.py \ + --genkeys="/YGGDRASIL/genkeys" \ + --yggdrasil-conf="/YGGDRASIL/yggdrasil.conf" \ + --timeout=60 \ + --environment + +# Find and configure public Yggdrasil peers +echo "Discovering Yggdrasil public peers..." +python3 /UTILS/yggdrasil_find_public_peers/yggdrasil_find_public_peers.py \ + --yggdrasil-conf="/YGGDRASIL/yggdrasil.conf" \ + --yggdrasil-peers-json="/UTILS/yggdrasil_find_public_peers/public_peers.json" \ + --parallel=4 \ + --pings=10 \ + --best=6 \ + --max-from-country=2 \ + --ping-interval=0.5 + +# Enable IPv6 (required for Yggdrasil) +echo "Enabling IPv6..." +sysctl net.ipv6.conf.all.disable_ipv6=0 || true + +# Start Yggdrasil in background +echo "Starting Yggdrasil..." +/YGGDRASIL/yggdrasil -useconffile /YGGDRASIL/yggdrasil.conf & +yggdrasil_pid=$! + +# Wait for Yggdrasil to establish connections +echo "Waiting for Yggdrasil network connectivity (60 seconds)..." +sleep 60 + +# Start I2P in background +echo "Starting I2P..." +/I2PD/i2pd --datadir /I2PD --conf /I2PD/i2pd.conf & +i2pd_pid=$! + +echo "Both services started successfully!" +echo "I2P Web Console: http://localhost:7070" +echo "Yggdrasil status: check container logs" + +# Wait for any process to exit and handle restart if needed +wait -n + +# If we reach here, one process died +echo "One of the services stopped unexpectedly, shutting down..." +kill -TERM $i2pd_pid $yggdrasil_pid 2>/dev/null +wait +exit 1 \ No newline at end of file diff --git a/i2pd_yggdrasil_arm/i2pd.conf b/i2pd_yggdrasil_arm/i2pd.conf new file mode 100644 index 0000000..7bce5d8 --- /dev/null +++ b/i2pd_yggdrasil_arm/i2pd.conf @@ -0,0 +1,300 @@ +## Configuration file for a typical i2pd user +## See https://i2pd.readthedocs.io/en/latest/user-guide/configuration/ +## for more options you can use in this file. + +## Lines that begin with "## " try to explain what's going on. Lines +## that begin with just "#" are disabled commands: you can enable them +## by removing the "#" symbol. + +## Tunnels config file +## Default: ~/.i2pd/tunnels.conf or /var/lib/i2pd/tunnels.conf +# tunconf = /var/lib/i2pd/tunnels.conf + +## Tunnels config files path +## Use that path to store separated tunnels in different config files. +## Default: ~/.i2pd/tunnels.d or /var/lib/i2pd/tunnels.d +# tunnelsdir = /var/lib/i2pd/tunnels.d + +## Path to certificates used for verifying .su3, families +## Default: ~/.i2pd/certificates or /var/lib/i2pd/certificates +# certsdir = /var/lib/i2pd/certificates + +## Where to write pidfile (default: i2pd.pid, not used in Windows) +# pidfile = /run/i2pd.pid + +## Logging configuration section +## By default logs go to stdout with level 'info' and higher +## For Windows OS by default logs go to file with level 'warn' and higher +## +## Logs destination (valid values: stdout, file, syslog) +## * stdout - print log entries to stdout +## * file - log entries to a file +## * syslog - use syslog, see man 3 syslog +# log = file +## Path to logfile (default - autodetect) +# logfile = /var/log/i2pd/i2pd.log +## Log messages above this level (debug, info, *warn, error, none) +## If you set it to none, logging will be disabled +loglevel = info # show informational messages (including SOCKS startup) +## Write full CLF-formatted date and time to log (default: write only time) +# logclftime = true + +## Daemon mode. Router will go to background after start. Ignored on Windows +# daemon = true + +## Specify a family, router belongs to (default - none) +# family = + +## Network interface to bind to +## Updates address4/6 options if they are not set +# ifname = +## You can specify different interfaces for IPv4 and IPv6 +# ifname4 = +# ifname6 = + +## Local address to bind transport sockets to +## Overrides host option if: +## For ipv4: if ipv4 = true and nat = false +## For ipv6: if 'host' is not set or ipv4 = true +# address4 = +# address6 = + +## External IPv4 or IPv6 address to listen for connections +## By default i2pd sets IP automatically +## Sets published NTCP2v4/SSUv4 address to 'host' value if nat = true +## Sets published NTCP2v6/SSUv6 address to 'host' value if ipv4 = false +# host = 1.2.3.4 + +## Port to listen for connections +## By default i2pd picks random port. You MUST pick a random number too, +## don't just uncomment this +port = 10765 + +## Enable communication through ipv4 +# IPv4 networking is required for I2P router connectivity and reseeding. +# Earlier we disabled it to restrict traffic to Yggdrasil only, but that +# prevented the router from ever contacting peers or downloading its +# initial NetDB. Enable at least for bootstrap; once the router has +# peers it will happily tunnel them over Yggdrasil if configured. +ipv4 = true +## Enable communication through ipv6 +# This MUST be true so i2pd can talk over the Yggdrasil mesh – without it +# Yggdrasil networking is broken, and the container will not be able to +# reach the I2P network at all. +ipv6 = true + +## Enable SSU transport (default = true) +ssu = false + +## Bandwidth configuration +## L limit bandwidth to 32KBs/sec, O - to 256KBs/sec, P - to 2048KBs/sec, +## X - unlimited +## Default is L (regular node) and X if floodfill mode enabled. If you want to +## share more bandwidth without floodfill mode, uncomment that line and adjust +## value to your possibilities +# bandwidth = L +## Max % of bandwidth limit for transit. 0-100. 100 by default +# share = 100 + +## Router will not accept transit tunnels, disabling transit traffic completely +## (default = false) +# notransit = true + +## Router will be floodfill +## Note: that mode uses much more network connections and CPU! +floodfill = true + +[ntcp2] +## Enable NTCP2 transport (default = true) +# enabled = true +## Publish address in RouterInfo (default = true) +# published = true +## Port for incoming connections (default is global port option value) +# port = 4567 + +[ssu2] +## Enable SSU2 transport (default = false for 2.43.0) +enabled = true +## Publish address in RouterInfo (default = false for 2.43.0) +published = true +## Port for incoming connections (default is global port option value or port + 1 if SSU is enabled) +# port = 4567 + +[http] +## Web Console settings +## Uncomment and set to 'false' to disable Web Console +# enabled = true +## Address and port service will listen on +address = 0.0.0.0 +port = 7070 +## Path to web console, default "/" +# webroot = / +## Uncomment following lines to enable Web Console authentication +# auth = true +# user = i2pd +# pass = changeme +## Select webconsole language +## Currently supported english (default), afrikaans, armenian, chinese, french, +## german, russian, turkmen, ukrainian and uzbek languages +# lang = english +hostname = localhost +#strictheaders = false + +[httpproxy] +## Uncomment and set to 'false' to disable HTTP Proxy +# enabled = true +## Address and port service will listen on +address = 0.0.0.0 +port = 4444 +## Optional keys file for proxy local destination +# keys = http-proxy-keys.dat +## Enable address helper for adding .i2p domains with "jump URLs" (default: true) +# addresshelper = true +## Address of a proxy server inside I2P, which is used to visit regular Internet +# outproxy = http://false.i2p +## httpproxy section also accepts I2CP parameters, like "inbound.length" etc. + +# [dns] section removed because the I2PD version used in this image +# does not support a built-in DNS server. Name resolution for .i2p domains +# is performed internally by the SOCKS proxy, so the external DNS listener +# is unnecessary. If you upgrade to a future I2PD release that adds this +# feature, you can restore the section. +# +# [dns] +# ## enable DNS server for resolving *.i2p names +# enabled = true +# address = 0.0.0.0 +# port = 53 + + +[socksproxy] +## Uncomment and set to 'false' to disable SOCKS Proxy +enabled = true +## Address and port service will listen on +address = 0.0.0.0 +port = 4447 +## Optional keys file for proxy local destination +# keys = socks-proxy-keys.dat +## Socks outproxy. Example below is set to use Tor for all connections except i2p +## Uncomment and set to 'true' to enable using of SOCKS outproxy +# outproxy.enabled = false +## Address and port of outproxy +# outproxy = 127.0.0.1 +# outproxyport = 9050 +## socksproxy section also accepts I2CP parameters, like "inbound.length" etc. + +[sam] +## Comment or set to 'false' to disable SAM Bridge +enabled = true +## Address and port service will listen on +address = 0.0.0.0 +port = 7656 + +[bob] +## Uncomment and set to 'true' to enable BOB command channel +# enabled = false +## Address and port service will listen on +# address = 127.0.0.1 +# port = 2827 + +[i2cp] +## Uncomment and set to 'true' to enable I2CP protocol +enabled = true +## Address and port service will listen on +address = 0.0.0.0 +port = 7654 + +[i2pcontrol] +## Uncomment and set to 'true' to enable I2PControl protocol +# enabled = false +## Address and port service will listen on +# address = 127.0.0.1 +# port = 7650 +## Authentication password. "itoopie" by default +# password = itoopie + +[precomputation] +## Enable or disable elgamal precomputation table +## By default, enabled on i386 hosts +# elgamal = true + +[upnp] +## Enable or disable UPnP: automatic port forwarding (enabled by default in WINDOWS, ANDROID) +# enabled = false +## Name i2pd appears in UPnP forwardings list (default = I2Pd) +# name = I2Pd + +[meshnets] +## Enable connectivity over the Yggdrasil network +yggdrasil = true +## You can bind address from your Yggdrasil subnet 300::/64 +## The address must first be added to the network interface +# yggaddress = + +[reseed] +## Options for bootstrapping into I2P network, aka reseeding +## Enable or disable reseed data verification. +verify = true +## URLs to request reseed data from, separated by comma +## Default: "mainline" I2P Network reseeds +# urls = https://reseed.i2p-projekt.de/,https://i2p.mooo.com/netDb/,https://netdb.i2p2.no/ +## Reseed URLs through the Yggdrasil, separated by comma +# yggurls = http://[324:9de3:fea4:f6ac::ace]:7070/ +## Path to local reseed data file (.su3) for manual reseeding +# file = /path/to/i2pseeds.su3 +## or HTTPS URL to reseed from +# file = https://legit-website.com/i2pseeds.su3 +## Path to local ZIP file or HTTPS URL to reseed from +# zipfile = /path/to/netDb.zip +## If you run i2pd behind a proxy server, set proxy server for reseeding here +## Should be http://address:port or socks://address:port +# proxy = http://127.0.0.1:8118 +## Minimum number of known routers, below which i2pd triggers reseeding. 25 by default +# threshold = 25 + +[addressbook] +## AddressBook subscription URL for initial setup +## Default: reg.i2p at "mainline" I2P Network +# defaulturl = http://shx5vqsw7usdaunyzr2qmes2fq37oumybpudrd4jjj4e4vk4uusa.b32.i2p/hosts.txt +## Optional subscriptions URLs, separated by comma +# subscriptions = http://reg.i2p/hosts.txt,http://identiguy.i2p/hosts.txt,http://stats.i2p/cgi-bin/newhosts.txt,http://rus.i2p/hosts.txt + +[limits] +## Maximum active transit sessions (default:2500) +# transittunnels = 2500 +## Limit number of open file descriptors (0 - use system limit) +# openfiles = 0 +## Maximum size of corefile in Kb (0 - use system limit) +# coresize = 0 + +[trust] +## Enable explicit trust options. false by default +# enabled = true +## Make direct I2P connections only to routers in specified Family. +# family = MyFamily +## Make direct I2P connections only to routers specified here. Comma separated list of base64 identities. +# routers = +## Should we hide our router from other routers? false by default +# hidden = true + +[exploratory] +## Exploratory tunnels settings with default values +# inbound.length = 2 +# inbound.quantity = 3 +# outbound.length = 2 +# outbound.quantity = 3 + +[persist] +## Save peer profiles on disk (default: true) +# profiles = true +## Save full addresses on disk (default: true) +# addressbook = true + +[cpuext] +## Use CPU AES-NI instructions set when work with cryptography when available (default: true) +# aesni = true +## Use CPU AVX instructions set when work with cryptography when available (default: true) +# avx = true +## Force usage of CPU instructions set, even if they not found +## DO NOT TOUCH that option if you really don't know what are you doing! +# force = false diff --git a/i2pd_yggdrasil_arm/yggdrasil.conf b/i2pd_yggdrasil_arm/yggdrasil.conf new file mode 100644 index 0000000..db45fc3 --- /dev/null +++ b/i2pd_yggdrasil_arm/yggdrasil.conf @@ -0,0 +1,82 @@ +{ + # List of connection strings for outbound peer connections in URI format, + # e.g. tls://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j. These connections + # will obey the operating system routing table, therefore you should + # use this section when you may connect via different interfaces. + Peers: [] + + # List of connection strings for outbound peer connections in URI format, + # arranged by source interface, e.g. { "eth0": [ "tls://a.b.c.d:e" ] }. + # Note that SOCKS peerings will NOT be affected by this option and should + # go in the "Peers" section instead. + InterfacePeers: {} + + # Listen addresses for incoming connections. You will need to add + # listeners in order to accept incoming peerings from non-local nodes. + # Multicast peer discovery will work regardless of any listeners set + # here. Each listener should be specified in URI format as above, e.g. + # tls://0.0.0.0:0 or tls://[::]:0 to listen on all interfaces. + Listen: [ + tls://[::]:10654 + ] + + # Listen address for admin connections. Default is to listen for local + # connections either on TCP/9001 or a UNIX socket depending on your + # platform. Use this value for yggdrasilctl -endpoint=X. To disable + # the admin socket, use the value "none" instead. + AdminListen: unix:///var/run/yggdrasil.sock + + # Configuration for which interfaces multicast peer discovery should be + # enabled on. Each entry in the list should be a json object which may + # contain Regex, Beacon, Listen, and Port. Regex is a regular expression + # which is matched against an interface name, and interfaces use the + # first configuration that they match gainst. Beacon configures whether + # or not the node should send link-local multicast beacons to advertise + # their presence, while listening for incoming connections on Port. + # Listen controls whether or not the node listens for multicast beacons + # and opens outgoing connections. + MulticastInterfaces: + [ + { + Regex: .* + Beacon: true + Listen: true + Port: 0 + Priority: 0 + } + ] + + # List of peer public keys to allow incoming peering connections + # from. If left empty/undefined then all connections will be allowed + # by default. This does not affect outgoing peerings, nor does it + # affect link-local peers discovered via multicast. + AllowedPublicKeys: [] + + # Your public key. Your peers may ask you for this to put + # into their AllowedPublicKeys configuration. + PublicKey: + + # Your private key. DO NOT share this with anyone! + PrivateKey: + + # Local network interface name for TUN adapter, or "auto" to select + # an interface automatically, or "none" to run without TUN. + IfName: auto + + # Maximum Transmission Unit (MTU) size for your local TUN interface. + # Default is the largest supported size for your platform. The lowest + # possible value is 1280. + IfMTU: 65535 + + # By default, nodeinfo contains some defaults including the platform, + # architecture and Yggdrasil version. These can help when surveying + # the network and diagnosing network routing problems. Enabling + # nodeinfo privacy prevents this, so that only items specified in + # "NodeInfo" are sent back if specified. + NodeInfoPrivacy: false + + # Optional node info. This must be a { "key": "value", ... } map + # or set as null. This is entirely optional but, if set, is visible + # to the whole network on request. + NodeInfo: {} +} \ No newline at end of file diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100755 index 0000000..812b228 --- /dev/null +++ b/scripts/smoke-test.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +COMPOSE=(docker compose -f "$ROOT_DIR/docker-compose.yml" -f "$ROOT_DIR/docker-compose.arm.yml") + +services=(dark3proxy darkdns darkscale darkpihole darki2p) +dns_queries=(google.com torproject.org github.com wikipedia.org example.onion example.i2p) + +echo "[smoke] stack status" +"${COMPOSE[@]}" ps + +echo +echo "[smoke] restart/health counters" +for c in "${services[@]}"; do + if [[ "$c" == "darkpihole" ]]; then + docker inspect -f "$c status={{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}} restart={{.RestartCount}}" "$c" + elif [[ "$c" == "darki2p" ]]; then + docker inspect -f "$c status={{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}} restart={{.RestartCount}}" "$c" + else + docker inspect -f "$c status={{.State.Status}} restart={{.RestartCount}}" "$c" + fi +done + +echo +echo "[smoke] dns timings via pihole" +for pass in 1 2; do + echo "-- pass $pass --" + for q in "${dns_queries[@]}"; do + out="$(docker exec darkpihole sh -lc "dig @127.0.0.1 +time=2 +tries=1 '$q' 2>&1" || true)" + status="$(printf "%s\n" "$out" | sed -n 's/.*status: \([A-Z]*\).*/\1/p' | head -n1)" + qtime="$(printf "%s\n" "$out" | sed -n 's/;; Query time: \([0-9]*\).*/\1/p' | head -n1)" + [[ -n "$status" ]] || status="NO_RESPONSE" + [[ -n "$qtime" ]] || qtime="-1" + printf '%s status=%s query_ms=%s\n' "$q" "$status" "$qtime" + done + echo +done + +echo "[smoke] key error scan" +for c in "${services[@]}"; do + echo "=== $c ===" + docker logs --tail 120 "$c" 2>&1 | grep -Ei "error|failed|not found|panic|traceback|unhealthy|refused|parse" | tail -n 20 || echo "(no matching patterns)" + echo +done + +echo "[smoke] done"