From bbf00882fc80f105f36cfd7d792e8afaa3bea494 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Fri, 3 Jul 2026 21:04:50 -0600 Subject: [PATCH 1/2] fix: use /32 host netmask for loopback aliases to stop mDNSResponder storm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setup-loopback.sh created each 127.0.0.x alias with the implicit class-A /8 netmask, so every alias claimed the entire 127.0.0.0/8 network. At large pool counts (e.g. 254) the overlapping-subnet interface addresses drive macOS mDNSResponder's address-conflict defense (PacketRRConflict) into an O(n^2) storm — pinning a CPU core and flooding port 5353 with loopback mDNS announcements. Configuring each alias with a host /32 netmask removes the subnet overlap; measured 254 aliases /8 => ~65% CPU vs 254 /32 => ~0%. Also remove any pre-existing alias before re-adding so re-running the script converts a machine still configured with the old /8 aliases. Co-Authored-By: Claude Opus 4.8 --- README.md | 2 ++ scripts/setup-loopback.sh | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0644b7d..c26adcf 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ npx harper-integration-test-setup-loopback This script requires `sudo` and respects the `HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT` environment variable (defaults to 32). +On macOS the aliases are configured with a host (`/32`) netmask. This matters: with the default class-A (`/8`) netmask every `127.0.0.x` alias claims the whole `127.0.0.0/8` network, and at large pool counts the resulting set of overlapping-subnet interface addresses drives `mDNSResponder`'s address-conflict defense into a CPU-pinning storm of mDNS announcements on port 5353. If you previously ran an older version of this script, re-run it to convert the existing aliases to `/32`. + ## API The lifecycle and utility APIs below are framework-agnostic. They manage Harper child processes and a cross-process loopback address pool. Use them in the setup/teardown hooks of whichever test framework you prefer. diff --git a/scripts/setup-loopback.sh b/scripts/setup-loopback.sh index 1100b99..e1ef741 100755 --- a/scripts/setup-loopback.sh +++ b/scripts/setup-loopback.sh @@ -35,7 +35,20 @@ fi END=$((START + COUNT - 1)) for i in $(seq $START $END); do - sudo ifconfig lo0 alias 127.0.0.$i up + # Use a host (/32) netmask, not the implicit class-A /8. Without an explicit netmask, + # macOS gives each 127.0.0.x alias a 255.0.0.0 mask, so every alias claims to own the + # entire 127.0.0.0/8 network. With a large COUNT that means dozens/hundreds of interface + # addresses all asserting the same subnet, which drives mDNSResponder's address-conflict + # defense (PacketRRConflict) into an O(n^2) storm — pinning a CPU core and flooding + # 5353 with loopback announcements. A /32 host route removes the subnet overlap: each + # alias is an isolated host, so there is no conflict cascade even at the full 254-address + # pool. (Measured: 254 aliases /8 => ~65% CPU; 254 aliases /32 => ~0% CPU.) + # + # Remove any pre-existing alias first so re-running this converts a machine previously + # configured with the old /8 aliases; ifconfig alias on an existing address does not + # reliably reset its netmask. The `-alias` is a harmless no-op if the address is absent. + sudo ifconfig lo0 -alias 127.0.0.$i 2>/dev/null + sudo ifconfig lo0 alias 127.0.0.$i netmask 255.255.255.255 up done echo "✓ Configured $COUNT loopback addresses (127.0.0.$START-127.0.0.$END)" \ No newline at end of file From 21f40c08b0b294cfc5cbca12f4acd75b7cc01a08 Mon Sep 17 00:00:00 2001 From: Kris Zyp Date: Mon, 13 Jul 2026 12:19:07 -0600 Subject: [PATCH 2/2] Add launchd daemon for reboot-persistent loopback aliases (macOS) macOS does not persist lo0 ifconfig aliases across reboots, so the loopback pool must be reconfigured on every boot. Add an optional launchd daemon that runs the setup at boot: - Make setup-loopback.sh root-aware: skip sudo (and the sudo -v tty prompt) when already running as root, so the same script serves both interactive use and boot-time execution by launchd. - Add scripts/io.harperdb.loopback-setup.plist (RunAtLoad one-shot). - Document the install/verify/uninstall flow in the README. Co-Authored-By: Claude Opus 4.8 --- README.md | 26 ++++++++++++++++++++ scripts/io.harperdb.loopback-setup.plist | 30 ++++++++++++++++++++++++ scripts/setup-loopback.sh | 15 ++++++++---- 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 scripts/io.harperdb.loopback-setup.plist diff --git a/README.md b/README.md index c26adcf..8c60325 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,32 @@ This script requires `sudo` and respects the `HARPER_INTEGRATION_TEST_LOOPBACK_P On macOS the aliases are configured with a host (`/32`) netmask. This matters: with the default class-A (`/8`) netmask every `127.0.0.x` alias claims the whole `127.0.0.0/8` network, and at large pool counts the resulting set of overlapping-subnet interface addresses drives `mDNSResponder`'s address-conflict defense into a CPU-pinning storm of mDNS announcements on port 5353. If you previously ran an older version of this script, re-run it to convert the existing aliases to `/32`. +### Persisting across reboots (macOS) + +`ifconfig` aliases live only in the running kernel — macOS does not persist manually added `lo0` aliases, so they vanish on every reboot and the pool has to be reconfigured. For a dev machine that reboots regularly, install a `launchd` daemon that runs the setup script at boot. `scripts/io.harperdb.loopback-setup.plist` is provided for this; the script is root-aware, so it needs no `sudo` when launchd runs it as root. + +```sh +# 1. Copy the setup script to a stable, root-owned path (the plist points here) +sudo mkdir -p /usr/local/sbin +sudo install -m 755 -o root -g wheel \ + "$(npm root)/@harperfast/integration-testing/scripts/setup-loopback.sh" \ + /usr/local/sbin/harper-loopback-setup.sh + +# 2. Install the daemon (must be root:wheel and mode 644 or launchd rejects it) +sudo install -m 644 -o root -g wheel \ + "$(npm root)/@harperfast/integration-testing/scripts/io.harperdb.loopback-setup.plist" \ + /Library/LaunchDaemons/io.harperdb.loopback-setup.plist + +# 3. Load and run it now (also runs at every boot via RunAtLoad) +sudo launchctl bootstrap system /Library/LaunchDaemons/io.harperdb.loopback-setup.plist + +# 4. Verify +cat /var/log/harper-loopback-setup.log # ✓ Configured ... line +ifconfig lo0 | grep '127.0.0' | tail -3 # aliases present +``` + +Edit the `HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT` value in the installed plist to change the pool size (it defaults to 32, matching the script). To remove the daemon: `sudo launchctl bootout system/io.harperdb.loopback-setup` and delete the plist. If you edit the installed plist, `bootout` then `bootstrap` again to reload it. + ## API The lifecycle and utility APIs below are framework-agnostic. They manage Harper child processes and a cross-process loopback address pool. Use them in the setup/teardown hooks of whichever test framework you prefer. diff --git a/scripts/io.harperdb.loopback-setup.plist b/scripts/io.harperdb.loopback-setup.plist new file mode 100644 index 0000000..5fe14d3 --- /dev/null +++ b/scripts/io.harperdb.loopback-setup.plist @@ -0,0 +1,30 @@ + + + + + Label + io.harperdb.loopback-setup + + ProgramArguments + + /bin/bash + /usr/local/sbin/harper-loopback-setup.sh + + + + RunAtLoad + + + + EnvironmentVariables + + HARPER_INTEGRATION_TEST_LOOPBACK_POOL_COUNT + 32 + + + StandardOutPath + /var/log/harper-loopback-setup.log + StandardErrorPath + /var/log/harper-loopback-setup.log + + diff --git a/scripts/setup-loopback.sh b/scripts/setup-loopback.sh index e1ef741..7870e89 100755 --- a/scripts/setup-loopback.sh +++ b/scripts/setup-loopback.sh @@ -1,7 +1,14 @@ #!/bin/bash -# Prompt for password upfront -sudo -v +# Run ifconfig via sudo when invoked interactively, or directly when already root +# (e.g. from the launchd daemon at boot, where there is no tty for `sudo -v`). +if [ "$(id -u)" -eq 0 ]; then + SUDO="" +else + SUDO="sudo" + # Prompt for password upfront + sudo -v +fi # The pool starts at 127.0.0.2 by default (127.0.0.1 is left for other services on localhost). # Override via the HARPER_INTEGRATION_TEST_LOOPBACK_POOL_START environment variable. @@ -47,8 +54,8 @@ for i in $(seq $START $END); do # Remove any pre-existing alias first so re-running this converts a machine previously # configured with the old /8 aliases; ifconfig alias on an existing address does not # reliably reset its netmask. The `-alias` is a harmless no-op if the address is absent. - sudo ifconfig lo0 -alias 127.0.0.$i 2>/dev/null - sudo ifconfig lo0 alias 127.0.0.$i netmask 255.255.255.255 up + $SUDO ifconfig lo0 -alias 127.0.0.$i 2>/dev/null + $SUDO ifconfig lo0 alias 127.0.0.$i netmask 255.255.255.255 up done echo "✓ Configured $COUNT loopback addresses (127.0.0.$START-127.0.0.$END)" \ No newline at end of file