@celilo/e2e 0.17.1 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/e2e-status CHANGED
@@ -32,4 +32,4 @@ done
32
32
  echo ""
33
33
  echo "=== DDNS Simulator ==="
34
34
  echo -n " Status: "
35
- docker compose -p "$PROJECT_NAME" exec -T namecheap-dns curl -s http://localhost:8080/status 2>/dev/null || echo "(not running)"
35
+ docker compose -p "$PROJECT_NAME" exec -T namecheap-dns curl -s --cacert /etc/ssl/namecheap-sim/namecheap-ca.crt https://dynamicdns.park-your-domain.com/status --resolve dynamicdns.park-your-domain.com:443:127.0.0.1 2>/dev/null || echo "(not running)"
@@ -0,0 +1,21 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDfTCCAmWgAwIBAgIUdjRGJDcAnXSrb8e8pgTGItaituQwDQYJKoZIhvcNAQEL
3
+ BQAwJjEkMCIGA1UEAwwbY2VsaWxvIGUyZSBOYW1lY2hlYXAgc2ltIENBMCAXDTI2
4
+ MDgyMTA2NDEyNFoYDzIxMjYwNzI4MDY0MTI0WjAqMSgwJgYDVQQDDB9keW5hbWlj
5
+ ZG5zLnBhcmsteW91ci1kb21haW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
6
+ MIIBCgKCAQEAqWyqjNCSLxfJLhgwJBT78SzmLb9sRdHCrXYrxeqBF3dHR3aM5/oj
7
+ CuiwT1l8P87cCauTbfHXSpOKRa0HDaQ5ArCQgBOxcK0Q4FzeUKRUtfbA9QR+b7ym
8
+ FQDSV7nYG8oOFnQybr/uKpfRaficzGQQJZDZ3mEXC24J5LLc0PYc6kgASZMy8QY6
9
+ PkCVdzX8viu0J3jGL780W/7AkNuLyzBmBs9SOyWy8vETY4MsRgm0Umv/Rh0MD33P
10
+ n/D5EFYb8bbKAWnEXbpY7p7N7o2Er1IrNq1SD4TCkmR9VLeNm+pdYt6RKBaFoAYd
11
+ Jbhk5YjnefqvlCPYyW2jyHOkh2I6up/P4QIDAQABo4GcMIGZMCoGA1UdEQQjMCGC
12
+ H2R5bmFtaWNkbnMucGFyay15b3VyLWRvbWFpbi5jb20wCQYDVR0TBAIwADALBgNV
13
+ HQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYDVR0OBBYEFMpDRmoxUAwc
14
+ WJg/0WKwllOse/y7MB8GA1UdIwQYMBaAFNLIv6wf7Q7vOHzctgjyYKGapXkwMA0G
15
+ CSqGSIb3DQEBCwUAA4IBAQAgp2bS+l/5mcoHVTlpt7pKbSx/LNzKAoatbchIQuoI
16
+ uaEy77r6XhLC1BAcqQRIQ38zBDHSQHcR6bI4nSY24HnczZHxM9r6LoJ8MKvXuuH+
17
+ rllrJb3qqvQECO9y4N3bvoK2RT6EjkwqNRsuvn0qtfSOkY2StJrMBdIlFjlAO0FZ
18
+ Bu1+ZBYeqRP+AoTidjzSuU7JgGhGEHLdKJnoBlo4o9FbHgGBw8wgQiUUnWUfcHhw
19
+ Uy+6f2RvK8jrPh2baWnqxpLRBBZdfmN4yu7JKs88uMBpRA9lD29essb/Q9MP8F/H
20
+ 2N0zizK46OSkimSMKACbI5U/f2E7JCuQukZV6VotrchC
21
+ -----END CERTIFICATE-----
@@ -0,0 +1,28 @@
1
+ -----BEGIN PRIVATE KEY-----
2
+ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCpbKqM0JIvF8ku
3
+ GDAkFPvxLOYtv2xF0cKtdivF6oEXd0dHdozn+iMK6LBPWXw/ztwJq5Nt8ddKk4pF
4
+ rQcNpDkCsJCAE7FwrRDgXN5QpFS19sD1BH5vvKYVANJXudgbyg4WdDJuv+4ql9Fp
5
+ +JzMZBAlkNneYRcLbgnkstzQ9hzqSABJkzLxBjo+QJV3Nfy+K7QneMYvvzRb/sCQ
6
+ 24vLMGYGz1I7JbLy8RNjgyxGCbRSa/9GHQwPfc+f8PkQVhvxtsoBacRduljuns3u
7
+ jYSvUis2rVIPhMKSZH1Ut42b6l1i3pEoFoWgBh0luGTliOd5+q+UI9jJbaPIc6SH
8
+ Yjq6n8/hAgMBAAECggEAMUj3qgPoaoOgVeTaaDykd6mIgglb3UmiOKSWIjpP+DwO
9
+ A6dkYP0eYYeD9mlBICy0TKnZNtPxvXOC9zbYxOqfXP1w45kW9oCZ1rKdR1cuf0p7
10
+ Iit+/lEh/R0ikztwNtFF3NlLbHUvurs0Q3/7WOfI3Kyw/CRBOGV3wkwXuT/G9I2p
11
+ PzPc5eB0XThMXw5TfpcQfRedCOTjGelVk/fDF7cqRuK/g9f77XTfZ76ftLFBREvv
12
+ /7v8rJMoljL08fpU30r1xAFRNQHLfkUVLQxB1l3g9BJ91VieSUHgWljdS4hGDvf/
13
+ djfSIzq4bpkkoYVt8jGOA9+7peahVhWzCEdjVFTlKwKBgQDSi4v+1SCllgqoLSRe
14
+ FvFlWEwMeJWGN2qqxrXfWURgK8ODMgDxYjvwtFr5kO+JfWFrsT5DLYduWOyUkU/O
15
+ b8Qw3Sd2MAgU2cO/ejf/xDIgyIVao+Mg+x2pAADm6NFknUiBHOvZ0MbLZHUpa7uD
16
+ Z0fFXDqhKyIAGC0DvKqD8qp26wKBgQDOAHSZ1kfic074FmUU136I82pwdViVrEHA
17
+ OMNmNaqnrU9HkKqrwIqHrOTdEMNQnkB+OMmJNK6H4N54PILze+TR+EYu6d1BiTWK
18
+ 3TAyhaA/eNGa3NuVyg8/wLFPxHj7QLUpv7JsqBKg45/GlPyXU5HtDX0Bsi3COlfX
19
+ YlQi70y5YwKBgHFnwMxON/t4ydy0E/aqHPQdjzjYapGUn9vElIntJys152VvNZrO
20
+ 8sJAIxBxxNQluKetHIBKsbv6IdQUQQxMnzxCotLCJFg2w30Lc8ZMA0QtPccIp3pI
21
+ SD2TRukZVk//d+IzlcYI8krznTfmU2X/52SivOG6veGXJAfzQ1U3DCJpAoGAde4Q
22
+ 0li3gk3csW2wpEPRbnlbP9INynF4mqBkTEkAz0kKD8Aj1Hs/UviJdHy6M5bE+P1t
23
+ cpy0JZl4AoExvk346rMlCopbXb885ahGhUW2PV/fLratBo1MOYUIfM/PKpcanzHc
24
+ 5RM6xtxIhZiZGdB/w6NrIuNsCrr3upfR7sXruF0CgYEAskixy85n2KZzr8kEz3Vb
25
+ ji7zwB9Nl5qSYThc73dmaDCeLGeStfvpjg6TZNHtB9IXzpCo11D1jN04uHigMhAt
26
+ N5hvFmI5eLGLL6EEpMusms9EVPkclybMnS/ykjOX4YNxwTn5fi28ITN36VSulFN/
27
+ OWZYLOMmP6gMqfNJ7ktAA5o=
28
+ -----END PRIVATE KEY-----
@@ -0,0 +1,20 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDLzCCAhegAwIBAgIUeuo47ZC6Ka3Zfq7a36MZPnASRQwwDQYJKoZIhvcNAQEL
3
+ BQAwJjEkMCIGA1UEAwwbY2VsaWxvIGUyZSBOYW1lY2hlYXAgc2ltIENBMCAXDTI2
4
+ MDgyMTA2NDEyNFoYDzIxMjYwNzI4MDY0MTI0WjAmMSQwIgYDVQQDDBtjZWxpbG8g
5
+ ZTJlIE5hbWVjaGVhcCBzaW0gQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
6
+ AoIBAQC2W2bH5m1J6SAkRTT7vspz22V9mWlv1TzWOqVuuRBFQ4IrBlSKrCrgOdi6
7
+ /IAu9rkY2DJsgt3ts4L4IhJnA3VRGU+UltdAwPuo5eOHGq9aLPwaa/Rb7q4Act+a
8
+ 53wVp3eLDAEvY4fk+dHUay87Ar2sXC+8WgwVZwUGoOpRE7/IWab9Q98a3hDYMQ1h
9
+ B71z2Ecz1g1z1w/zBqjiR4IMwgVV2VZ9yCmXBSad2mVCPf/51rWuY9ooZw73MbRc
10
+ PByejx0Fa8jigfFszG/oE2i7alO3ISmJzODgX8AuqqO3yVokKGTUY1DsJxas1U27
11
+ vhlHtYfVEhnUXgWyZphWvr38DslLAgMBAAGjUzBRMB0GA1UdDgQWBBTSyL+sH+0O
12
+ 7zh83LYI8mChmqV5MDAfBgNVHSMEGDAWgBTSyL+sH+0O7zh83LYI8mChmqV5MDAP
13
+ BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA4BF303aKFEXpTHrPY
14
+ L3zopaL6EYTL6fzU8dhB5ZwCXfCJUa5Rx860kIFSjSaUd3rCfJ/70MTe9SQUGKVF
15
+ dt659/PA8olQtZQJsNOSjduQxAgwGe3T8V3G/qDogGDTAJKWGMQCWYSEwBjr30pQ
16
+ keyX1ahBzWwSg3FmdEOt35SfhvFQ4S/TAoEM33QARq+jJfMBupPNSIETgcr+Rimn
17
+ YWbshg1zfXuQnju9qV3R1bEST7ZSH+CjXhPu6CCbu/jTT3lPRIB04w2Yw5IAi1Kh
18
+ SzCF3TlQEUXpkPhlYpglhW0q3wgfp7QmnquNLHo6E8I+PyBjkSkg+Xq2cw14bE0i
19
+ MWHp
20
+ -----END CERTIFICATE-----
@@ -24,9 +24,19 @@ iptables -P FORWARD ACCEPT
24
24
  # DNS
25
25
  echo "nameserver 203.0.113.1" > /etc/resolv.conf
26
26
 
27
- # Start dnsmasq DHCP server on internal network
28
- dnsmasq --conf-dir=/etc/dnsmasq.d --keep-in-foreground --log-facility=- &
29
- echo "dnsmasq DHCP server started"
27
+ # Start dnsmasq DHCP server on internal network.
28
+ #
29
+ # Skippable, because celilo can serve DHCP itself (`modules/dnsmasq-dhcp`) and
30
+ # two DHCP servers on one broadcast domain race: whichever answers a DISCOVER
31
+ # first wins, and a test asserting which one served a lease would be asserting
32
+ # a coin toss. An operator moving DHCP to celilo turns the router's off, so a
33
+ # suite that tests celilo's DHCP models that by setting ROUTER_DHCP=off.
34
+ if [ "${ROUTER_DHCP:-on}" = "off" ]; then
35
+ echo "dnsmasq DHCP server NOT started (ROUTER_DHCP=off; celilo serves DHCP here)"
36
+ else
37
+ dnsmasq --conf-dir=/etc/dnsmasq.d --keep-in-foreground --log-facility=- &
38
+ echo "dnsmasq DHCP server started"
39
+ fi
30
40
 
31
41
  # Start greenwave simulator
32
42
  if [ -f /simulator/server.ts ]; then
@@ -2,8 +2,6 @@
2
2
  set -e
3
3
 
4
4
  # System-wide environment for all processes (hooks, ansible, etc.)
5
- echo 'DDNS_ENDPOINT=http://100.64.0.55:8080/update' >> /etc/environment
6
- export DDNS_ENDPOINT="http://100.64.0.55:8080/update"
7
5
 
8
6
  # Generate SSH keys if needed
9
7
  /config/ssh/generate-keys.sh /ssh-keys
@@ -105,7 +103,6 @@ cat > /root/.zshrc << ZSHRC
105
103
  export PATH="/root/.bun/bin:/usr/local/bin:\$PATH"
106
104
 
107
105
  # Simulated network overrides
108
- export DDNS_ENDPOINT="http://100.64.0.55:8080/update"
109
106
 
110
107
  # History
111
108
  HISTFILE=/root/.zsh_history
@@ -35,6 +35,12 @@ RUN apt-get update && apt-get install -y \
35
35
  procps \
36
36
  zsh \
37
37
  gnupg \
38
+ # The hook jail (openspec/changes/hook-process-boundary, stage 2) runs every
39
+ # hook under bubblewrap. Without it the management box runs hooks unjailed,
40
+ # which is a valid mode but not the one the e2e suite is there to exercise.
41
+ # The container needs security_opt relaxations to let it build a namespace —
42
+ # see the measured ladder in docker-compose-generator.ts.
43
+ bubblewrap \
38
44
  && rm -rf /var/lib/apt/lists/*
39
45
 
40
46
  # --- Terraform, plus an OFFLINE provider mirror -------------------------------
@@ -96,6 +102,12 @@ COPY config/pebble/pebble-ca.crt /usr/local/share/ca-certificates/pebble-ca.crt
96
102
  # external_web's authoritative post-publish check HTTPS-GETs the route from
97
103
  # here, which is why mgmt in particular must trust it.
98
104
  COPY config/cpanel-host/cpanel-host-ca.crt /usr/local/share/ca-certificates/cpanel-host-ca.crt
105
+ # Trust the Namecheap DDNS sim's CA. Its cert is likewise PRE-INSTALLED rather
106
+ # than Pebble-issued: celilo does not own a third-party API's TLS. This is what
107
+ # lets namecheap's hooks call the REAL endpoint URL over real TLS, instead of
108
+ # being redirected at an http://ip:port by an environment variable that an
109
+ # allow-listed hook environment no longer carries.
110
+ COPY config/namecheap/namecheap-ca.crt /usr/local/share/ca-certificates/namecheap-ca.crt
99
111
  RUN update-ca-certificates
100
112
 
101
113
  # Tell bun (and Node-compat code) to merge the system CA bundle with
@@ -14,6 +14,12 @@ RUN apt-get update && apt-get install -y \
14
14
  RUN curl -fsSL https://bun.sh/install | bash
15
15
  ENV PATH="/root/.bun/bin:$PATH"
16
16
 
17
+ # The endpoint's own certificate, PRE-INSTALLED the way a real third-party
18
+ # service's is (same pattern as the cPanel host sim). The CA rides along so the
19
+ # container can probe its own HTTPS endpoint without disabling verification.
20
+ COPY config/namecheap/ddns-tls.crt config/namecheap/ddns-tls.key config/namecheap/namecheap-ca.crt /etc/ssl/namecheap-sim/
21
+ RUN chmod 600 /etc/ssl/namecheap-sim/ddns-tls.key
22
+
17
23
  COPY simulators/namecheap-ddns/ /simulator/
18
24
  COPY config/routing/dns-routes.sh /dns-routes.sh
19
25
  RUN chmod +x /dns-routes.sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celilo/e2e",
3
- "version": "0.17.1",
3
+ "version": "0.19.0",
4
4
  "description": "E2E test infrastructure for Celilo-deployed applications. Provides a simulated internet with DNS hierarchy, ACME server, firewalls, and target machines in Docker.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -37,7 +37,7 @@
37
37
  "README.md"
38
38
  ],
39
39
  "dependencies": {
40
- "@celilo/capabilities": "^2.4.0",
40
+ "@celilo/capabilities": "^3.1.0",
41
41
  "@celilo/cli-display": "^0.2.0",
42
42
  "@celilo/event-bus": "^0.6.0",
43
43
  "@celilo/terraform-fake": "^0.3.1",
@@ -46,7 +46,24 @@ const PASSWORD_MAP: Record<string, string> = (() => {
46
46
  return {};
47
47
  })();
48
48
  const FALLBACK_PASSWORD = process.env.DDNS_PASSWORD || 'test123';
49
- const PORT = Number(process.env.DDNS_PORT) || 8080;
49
+ /**
50
+ * The real Namecheap DDNS endpoint is `https://dynamicdns.park-your-domain.com`,
51
+ * so that is what this serves: port 443, with a certificate for that name.
52
+ *
53
+ * It used to listen on plain HTTP at 8080 and the rig pointed the module at
54
+ * `http://100.64.0.55:8080/update` through a `DDNS_ENDPOINT` environment
55
+ * variable. That bypassed both the DNS chain the rig had already built (the
56
+ * `park-your-domain.com` zone has always been there) and TLS, and it stopped
57
+ * working when hooks became subprocesses with an allow-listed environment
58
+ * (openspec/changes/hook-process-boundary, design D5).
59
+ *
60
+ * The certificate is PRE-INSTALLED rather than issued by Pebble, the same way
61
+ * the cPanel host simulator's is, and for the same reason: celilo does not own
62
+ * this service's TLS. A real third-party API has a certificate from a real CA
63
+ * long before celilo talks to it, and there is no ACME exchange to model.
64
+ */
65
+ const PORT = Number(process.env.DDNS_PORT) || 443;
66
+ const TLS_DIR = process.env.DDNS_TLS_DIR || '/etc/ssl/namecheap-sim';
50
67
 
51
68
  function expectedPasswordFor(domain: string): string {
52
69
  return PASSWORD_MAP[domain] ?? FALLBACK_PASSWORD;
@@ -107,6 +124,10 @@ function normalizeIp(address: string): string {
107
124
 
108
125
  const server = Bun.serve({
109
126
  port: PORT,
127
+ tls: {
128
+ cert: Bun.file(`${TLS_DIR}/ddns-tls.crt`),
129
+ key: Bun.file(`${TLS_DIR}/ddns-tls.key`),
130
+ },
110
131
  fetch(req, server) {
111
132
  const url = new URL(req.url);
112
133
 
@@ -813,6 +813,11 @@ function buildNetworkHandle(
813
813
  await handle.celilo(`module config set iptables firewall_ip ${firewallIp}`);
814
814
  await handle.celilo(`module config set iptables nat_ip ${natIp}`);
815
815
  await handle.celilo(`module config set iptables zones '${JSON.stringify(declaredZones)}'`);
816
+ // Caller-supplied settings last, so a suite can turn on an opt-in policy
817
+ // the way an operator would rather than assert against the default.
818
+ for (const [key, value] of Object.entries(opts.config ?? {})) {
819
+ await handle.celilo(`module config set iptables ${key} ${value}`);
820
+ }
816
821
  // `check: false` returns the failed deploy instead of throwing, so a test
817
822
  // can assert on WHAT the refusal said. Interface classification refuses
818
823
  // by design (D12 onboarding), and a refusal is only useful if it names
@@ -1259,8 +1264,14 @@ export async function startNetwork(config: NetworkConfig): Promise<NetworkHandle
1259
1264
  );
1260
1265
  }
1261
1266
 
1262
- // Wait for DHCP client lease
1263
- if (config.dhcpClient) {
1267
+ // Wait for DHCP client lease.
1268
+ //
1269
+ // Skipped when the router's DHCP is off, because then NOTHING is serving yet:
1270
+ // celilo's own DHCP server is a module, and modules deploy long after the
1271
+ // network is up. Waiting here would time out at 60s on every such suite and
1272
+ // report it as a network-start failure. Those suites drive the client
1273
+ // themselves once their server is deployed.
1274
+ if (config.dhcpClient && config.routerDhcp !== false) {
1264
1275
  console.log('[progress:start] waiting for DHCP client lease | DHCP lease acquired');
1265
1276
  // Self-diagnosing via waitFor's onTimeout (e2e-confidence #255): a DHCP-lease
1266
1277
  // timeout is an intermittent in-suite flake (passes solo). On timeout, dump
@@ -94,6 +94,12 @@ function baseService(opts: {
94
94
  environment?: Record<string, string>;
95
95
  tmpfs?: string[];
96
96
  privileged?: boolean;
97
+ /**
98
+ * Docker `security_opt` entries, e.g. `seccomp=unconfined`. Needed by the
99
+ * management box so the hook jail's bubblewrap can build a namespace inside
100
+ * a container — see the measured ladder at its call site.
101
+ */
102
+ security_opt?: string[];
97
103
  dns?: string[];
98
104
  /** Host devices to expose, e.g. `/dev/net/tun` for a userspace WireGuard tunnel. */
99
105
  devices?: string[];
@@ -215,7 +221,13 @@ function defaultTopologyServices(config: NetworkConfig): Record<string, unknown>
215
221
  volumes: ['ssh-keys:/ssh-keys:ro'],
216
222
  // Which real ISP router this sim stands in for. The protocol is identical
217
223
  // across the two; only the TR-181 vendor extension prefix differs.
218
- environment: { ROUTER_VENDOR_PREFIX: config.routerVendorPrefix ?? 'X_GWS_' },
224
+ environment: {
225
+ ROUTER_VENDOR_PREFIX: config.routerVendorPrefix ?? 'X_GWS_',
226
+ // The simulated router serves DHCP on `internal` by default, which is
227
+ // what an operator's router does. A suite testing celilo's OWN DHCP
228
+ // turns it off, because two servers on one broadcast domain race.
229
+ ROUTER_DHCP: config.routerDhcp === false ? 'off' : 'on',
230
+ },
219
231
  }),
220
232
  };
221
233
  }
@@ -568,7 +580,34 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
568
580
  image:
569
581
  variant === 'vanilla' ? 'celilo-e2e/management:vanilla' : 'celilo-e2e/management:latest',
570
582
  networks: { [zone]: { ipv4_address: ip } },
571
- cap_add: ['NET_ADMIN'],
583
+ // SYS_ADMIN is for the hook jail, NET_ADMIN for the routing the box does
584
+ // anyway. See the security_opt block below for why both are here.
585
+ cap_add: ['NET_ADMIN', 'SYS_ADMIN'],
586
+ // The hook jail runs each hook under bubblewrap, which has to build a
587
+ // user namespace and then mount inside it. Docker denies that four
588
+ // separate ways, and every one of these is load-bearing — measured on a
589
+ // colima Ubuntu 24.04.1 VM (kernel 6.8.0-50,
590
+ // kernel.apparmor_restrict_unprivileged_userns=1), by removing one at a
591
+ // time from the working set:
592
+ //
593
+ // drop seccomp=unconfined -> bwrap: pivot_root: Operation not permitted
594
+ // drop apparmor=unconfined -> bwrap: Failed to make / slave: Permission denied
595
+ // drop systempaths=unconfined -> bwrap: Can't mount proc on /newroot/proc
596
+ // drop SYS_ADMIN -> bwrap: setting up uid map: Permission denied
597
+ //
598
+ // Deliberately NOT `privileged: true`, which design D8 measured as
599
+ // working and task 4.1 asked to improve on. It would also grant every
600
+ // capability and every host device; this set grants four named things.
601
+ //
602
+ // `systempaths=unconfined` is the one D8's ladder was missing: it unmasks
603
+ // the /proc paths Docker hides, which is why "all three" of the earlier
604
+ // options still failed on the proc mount.
605
+ //
606
+ // NOTE this makes the container a WEAK place to test the AppArmor
607
+ // profile: apparmor=unconfined means the restriction the profile exists
608
+ // to satisfy is not in force here. That gate is the celilo-builder probe
609
+ // on a real kernel, never this container.
610
+ security_opt: ['seccomp=unconfined', 'apparmor=unconfined', 'systempaths=unconfined'],
572
611
  // ISS-0157: `module import`'s `bun install` hard-links packages from the
573
612
  // bun cache into node_modules, which on overlayfs forces a copy-up + fsync
574
613
  // per file. On a slow/contended builder disk that storm blows past the
@@ -595,7 +634,6 @@ export function generateTestComposeYaml(config: NetworkConfig, celiloRoot?: stri
595
634
  ...(config.managementVolumes ?? []),
596
635
  ],
597
636
  environment: {
598
- DDNS_ENDPOINT: `http://${SIMULATOR_IPS.NAMECHEAP_DNS}:8080/update`,
599
637
  // On its own control-plane network the only router in reach is fw-main's
600
638
  // leg there — both for the default route and for the segmented zones.
601
639
  DEFAULT_GATEWAY: zone === 'secure-mgmt' ? SECURE_MGMT_GATEWAY : managementDefaultGw,
@@ -821,7 +859,6 @@ export function generateComposeYaml(config: NetworkConfig, celiloRoot = '..'): s
821
859
  ...(config.managementVolumes ?? []),
822
860
  ],
823
861
  environment: {
824
- DDNS_ENDPOINT: `http://${SIMULATOR_IPS.NAMECHEAP_DNS}:8080/update`,
825
862
  DEFAULT_GATEWAY: managementDefaultGw,
826
863
  CELILO_REGISTRY_URL: 'http://e2e-registry.lab',
827
864
  },
@@ -140,6 +140,23 @@ export class NetworkBuilder {
140
140
  return this;
141
141
  }
142
142
 
143
+ /**
144
+ * Stop the simulated ISP router serving DHCP on `internal`.
145
+ *
146
+ * For suites where CELILO serves DHCP (`modules/dnsmasq-dhcp`). Two servers
147
+ * on one broadcast domain race, so without this a lease assertion is a coin
148
+ * toss. It is also what an operator does when moving DHCP to celilo, so the
149
+ * suite models the migration rather than dodging a conflict.
150
+ *
151
+ * NOTE: `withDhcpClient()`'s start-up lease wait expects SOMETHING to answer.
152
+ * Pair this with a suite that deploys celilo's DHCP server, or the client
153
+ * gets no lease and the network never comes up.
154
+ */
155
+ withoutRouterDhcp(): this {
156
+ this.config.routerDhcp = false;
157
+ return this;
158
+ }
159
+
143
160
  /**
144
161
  * Add the Proxmox API simulator on `secure-mgmt`.
145
162
  *
@@ -23,7 +23,7 @@ export const SIMULATOR_IPS = {
23
23
  ROOT_DNS: '100.64.0.53',
24
24
  /** Authoritative TLD DNS for `com.`, `org.`, `net.`. */
25
25
  TLD_DNS: '100.64.0.54',
26
- /** Authoritative DNS for the customer domains; also serves the DDNS update endpoint at port 8080. */
26
+ /** Authoritative DNS for the customer domains; also serves the DDNS update endpoint at https://dynamicdns.park-your-domain.com. */
27
27
  NAMECHEAP_DNS: '100.64.0.55',
28
28
  /** Internal e2e module registry (replaces production celilo.computer in tests). */
29
29
  REGISTRY: '100.64.0.56',
package/src/types.ts CHANGED
@@ -66,6 +66,19 @@ export interface NetworkConfig {
66
66
  * serves both rather than a near-duplicate second one.
67
67
  */
68
68
  routerVendorPrefix?: string;
69
+ /**
70
+ * Whether the simulated ISP router serves DHCP on `internal`. Defaults to
71
+ * true, which is what an operator's router does and what every existing suite
72
+ * relies on.
73
+ *
74
+ * Set false when celilo serves DHCP instead (`modules/dnsmasq-dhcp`). Two
75
+ * DHCP servers on one broadcast domain race — whichever answers a DISCOVER
76
+ * first wins — so a suite asserting WHICH server issued a lease would be
77
+ * asserting a coin toss. Turning the router's off is also what a real
78
+ * operator does when moving DHCP to celilo, so this models the migration
79
+ * rather than papering over a conflict.
80
+ */
81
+ routerDhcp?: boolean;
69
82
  /** Extra volume mounts for the management container (host:container format) */
70
83
  managementVolumes: string[];
71
84
  /** Include a DHCP client container on the internal network */
@@ -449,7 +462,19 @@ export interface NetworkHandle {
449
462
  *
450
463
  * @param opts.natIp - NAT IP for port-forwarded traffic (default 10.226.1.253)
451
464
  */
452
- deployFirewall(opts?: { natIp?: string; check?: boolean }): Promise<ExecResult>;
465
+ /**
466
+ * @param opts.config - Extra `module config set iptables <key> <value>` pairs,
467
+ * applied after the topology-derived ones and before the deploy. For
468
+ * settings whose whole point is that they are OPT-IN: the harness deploys a
469
+ * DEFAULT firewall, so a suite testing an opt-in policy has to turn it on
470
+ * the way an operator would, and asserting on a default deploy would only
471
+ * ever measure the default.
472
+ */
473
+ deployFirewall(opts?: {
474
+ natIp?: string;
475
+ check?: boolean;
476
+ config?: Record<string, string>;
477
+ }): Promise<ExecResult>;
453
478
 
454
479
  /**
455
480
  * Deploy the `greenwave` module against the ISP router (`fw-isp`) — the box