@celilo/e2e 0.6.0 → 0.6.1
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/README.md +2 -1
- package/config/dns/celilo.computer.zone +16 -0
- package/config/dns/iamtheinternet.org.zone +7 -6
- package/config/dns/knot-namecheap.conf +8 -0
- package/config/dns/knot-tld.conf +4 -0
- package/config/dns/lunacycle.net.zone +16 -0
- package/config/dns/net.zone +11 -0
- package/config/dns/org.zone +5 -0
- package/config/dns/root.zone +2 -0
- package/config/dns/templates/iamtheinternet.org.zone +16 -0
- package/config/dns/templates/lunacycle.net.zone +16 -0
- package/config/resolver/unbound.conf +10 -0
- package/config/routing/management-routes.sh +19 -0
- package/config/routing/resolver-internal-routes.sh +59 -0
- package/docker/Dockerfile.internal-resolver +16 -0
- package/docker/Dockerfile.isitup +37 -0
- package/docker/Dockerfile.registry +34 -0
- package/netapps/authentik.netapp +0 -0
- package/netapps/caddy.netapp +0 -0
- package/netapps/greenwave.netapp +0 -0
- package/netapps/homebridge.netapp +0 -0
- package/netapps/iptables.netapp +0 -0
- package/netapps/knot-unbound-internal.netapp +0 -0
- package/netapps/namecheap.netapp +0 -0
- package/netapps/technitium.netapp +0 -0
- package/netapps/test-ansible-output.netapp +0 -0
- package/package.json +2 -1
- package/simulators/isitup/Caddyfile +11 -0
- package/simulators/isitup/entrypoint.sh +47 -0
- package/simulators/isitup/server.ts +112 -0
- package/simulators/namecheap-ddns/server.ts +31 -2
- package/src/cli/build.ts +11 -1
- package/src/cli/command-registry.ts +3 -2
- package/src/cli/completion.ts +29 -5
- package/src/cli/index.ts +85 -15
- package/src/cli/module-discovery.ts +120 -0
- package/src/cli/scaffold.ts +4 -5
- package/src/container-manager.ts +358 -71
- package/src/docker-compose-generator.ts +119 -17
- package/src/parse-line.ts +14 -3
- package/src/runner.ts +122 -165
- package/src/simulator-ips.ts +42 -0
- package/src/types.ts +24 -0
package/README.md
CHANGED
|
@@ -187,7 +187,8 @@ c machine add 192.168.0.254 --ssh-user root --earmark iptables
|
|
|
187
187
|
c machine add 10.0.10.10 --ssh-user root
|
|
188
188
|
|
|
189
189
|
# Pre-configure secrets and non-derivable config
|
|
190
|
-
c module
|
|
190
|
+
c module config set namecheap domains '["iamtheinternet.org"]'
|
|
191
|
+
c module secret set namecheap ddns_passwords '{"iamtheinternet.org":"test123"}'
|
|
191
192
|
c module config set greenwave router_ip 192.168.0.1
|
|
192
193
|
c module secret set greenwave router_username admin
|
|
193
194
|
c module secret set greenwave router_password admin
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
$ORIGIN celilo.computer.
|
|
2
|
+
$TTL 300
|
|
3
|
+
|
|
4
|
+
@ IN SOA ns1.celilo.computer. admin.celilo.computer. (
|
|
5
|
+
2024020117 300 60 604800 300
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
; Public DNS — RFC 1918 addresses NEVER appear here. The whole point of
|
|
9
|
+
; the e2e simulation is to model real internet connectivity, where
|
|
10
|
+
; private IPs are unreachable from outside. All public-facing names
|
|
11
|
+
; resolve to the firewall's external IP (100.100.0.100), which DNATs
|
|
12
|
+
; inbound to the right internal host.
|
|
13
|
+
@ IN NS ns1.celilo.computer.
|
|
14
|
+
ns1 IN A 100.64.0.55
|
|
15
|
+
@ IN A 100.100.0.100
|
|
16
|
+
www IN A 100.100.0.100
|
|
@@ -2,14 +2,15 @@ $ORIGIN iamtheinternet.org.
|
|
|
2
2
|
$TTL 300
|
|
3
3
|
|
|
4
4
|
@ IN SOA ns1.iamtheinternet.org. admin.iamtheinternet.org. (
|
|
5
|
-
|
|
5
|
+
2024020248 300 60 604800 300
|
|
6
6
|
)
|
|
7
7
|
|
|
8
|
+
; Public DNS — RFC 1918 addresses NEVER appear here. The whole point of
|
|
9
|
+
; the e2e simulation is to model real internet connectivity, where
|
|
10
|
+
; private IPs are unreachable from outside. All public-facing names
|
|
11
|
+
; resolve to the firewall's external IP (100.100.0.100), which DNATs
|
|
12
|
+
; inbound to the right internal host.
|
|
8
13
|
@ IN NS ns1.iamtheinternet.org.
|
|
9
14
|
ns1 IN A 100.64.0.55
|
|
10
15
|
@ IN A 0.0.0.0
|
|
11
|
-
|
|
12
|
-
www IN A 100.100.0.100
|
|
13
|
-
deathstar IN A 100.100.0.100
|
|
14
|
-
test IN A 0.0.0.0
|
|
15
|
-
auth IN A 100.100.0.100
|
|
16
|
+
www IN A 100.100.0.100
|
|
@@ -6,6 +6,14 @@ zone:
|
|
|
6
6
|
file: "/config/iamtheinternet.org.zone"
|
|
7
7
|
storage: "/var/lib/knot"
|
|
8
8
|
|
|
9
|
+
- domain: "celilo.computer"
|
|
10
|
+
file: "/config/celilo.computer.zone"
|
|
11
|
+
storage: "/var/lib/knot"
|
|
12
|
+
|
|
13
|
+
- domain: "lunacycle.net"
|
|
14
|
+
file: "/config/lunacycle.net.zone"
|
|
15
|
+
storage: "/var/lib/knot"
|
|
16
|
+
|
|
9
17
|
- domain: "park-your-domain.com"
|
|
10
18
|
file: "/config/park-your-domain.com.zone"
|
|
11
19
|
storage: "/var/lib/knot"
|
package/config/dns/knot-tld.conf
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
$ORIGIN lunacycle.net.
|
|
2
|
+
$TTL 300
|
|
3
|
+
|
|
4
|
+
@ IN SOA ns1.lunacycle.net. admin.lunacycle.net. (
|
|
5
|
+
2024020108 300 60 604800 300
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
; Public DNS — RFC 1918 addresses NEVER appear here. The whole point of
|
|
9
|
+
; the e2e simulation is to model real internet connectivity, where
|
|
10
|
+
; private IPs are unreachable from outside. All public-facing names
|
|
11
|
+
; resolve to the firewall's external IP (100.100.0.100), which DNATs
|
|
12
|
+
; inbound to the right internal host.
|
|
13
|
+
@ IN NS ns1.lunacycle.net.
|
|
14
|
+
ns1 IN A 100.64.0.55
|
|
15
|
+
@ IN A 100.100.0.100
|
|
16
|
+
www IN A 100.100.0.100
|
package/config/dns/org.zone
CHANGED
|
@@ -10,5 +10,10 @@ org. IN NS tld-dns.lab.
|
|
|
10
10
|
iamtheinternet IN NS ns1.iamtheinternet.org.
|
|
11
11
|
ns1.iamtheinternet.org. IN A 100.64.0.55
|
|
12
12
|
|
|
13
|
+
; External prober simulator — mimics isitup.org's JSON API.
|
|
14
|
+
; Served directly by tld-dns rather than delegated, since we only need a
|
|
15
|
+
; single A record (no subdomains, no SOA of its own).
|
|
16
|
+
isitup IN A 100.64.0.57
|
|
17
|
+
|
|
13
18
|
; Note: Let's Encrypt ACME endpoints are handled by unbound local-data
|
|
14
19
|
; (resolves acme-v02.api.letsencrypt.org → 100.64.0.100 directly)
|
package/config/dns/root.zone
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
$ORIGIN iamtheinternet.org.
|
|
2
|
+
$TTL 300
|
|
3
|
+
|
|
4
|
+
@ IN SOA ns1.iamtheinternet.org. admin.iamtheinternet.org. (
|
|
5
|
+
2024020247 300 60 604800 300
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
; Public DNS — RFC 1918 addresses NEVER appear here. The whole point of
|
|
9
|
+
; the e2e simulation is to model real internet connectivity, where
|
|
10
|
+
; private IPs are unreachable from outside. All public-facing names
|
|
11
|
+
; resolve to the firewall's external IP (100.100.0.100), which DNATs
|
|
12
|
+
; inbound to the right internal host.
|
|
13
|
+
@ IN NS ns1.iamtheinternet.org.
|
|
14
|
+
ns1 IN A 100.64.0.55
|
|
15
|
+
@ IN A 0.0.0.0
|
|
16
|
+
www IN A 100.100.0.100
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
$ORIGIN lunacycle.net.
|
|
2
|
+
$TTL 300
|
|
3
|
+
|
|
4
|
+
@ IN SOA ns1.lunacycle.net. admin.lunacycle.net. (
|
|
5
|
+
2024020108 300 60 604800 300
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
; Public DNS — RFC 1918 addresses NEVER appear here. The whole point of
|
|
9
|
+
; the e2e simulation is to model real internet connectivity, where
|
|
10
|
+
; private IPs are unreachable from outside. All public-facing names
|
|
11
|
+
; resolve to the firewall's external IP (100.100.0.100), which DNATs
|
|
12
|
+
; inbound to the right internal host.
|
|
13
|
+
@ IN NS ns1.lunacycle.net.
|
|
14
|
+
ns1 IN A 100.64.0.55
|
|
15
|
+
@ IN A 100.100.0.100
|
|
16
|
+
www IN A 100.100.0.100
|
|
@@ -11,12 +11,22 @@ server:
|
|
|
11
11
|
# Let's Encrypt ACME endpoints → Pebble
|
|
12
12
|
local-data: "acme-v02.api.letsencrypt.org. IN A 100.64.0.100"
|
|
13
13
|
local-data: "acme-staging-v02.api.letsencrypt.org. IN A 100.64.0.100"
|
|
14
|
+
# E2E registry container
|
|
15
|
+
local-data: "e2e-registry.lab. IN A 100.64.0.56"
|
|
16
|
+
# External website prober simulator — without this override, isitup.org
|
|
17
|
+
# falls through the "forward-zone: ." below and resolves to the REAL
|
|
18
|
+
# Namecheap-parked domain on the public internet.
|
|
19
|
+
local-data: "isitup.org. IN A 100.64.0.57"
|
|
14
20
|
|
|
15
21
|
# Simulated domains → namecheap-dns (authoritative)
|
|
16
22
|
stub-zone:
|
|
17
23
|
name: "iamtheinternet.org."
|
|
18
24
|
stub-addr: 100.64.0.55
|
|
19
25
|
|
|
26
|
+
stub-zone:
|
|
27
|
+
name: "lunacycle.net."
|
|
28
|
+
stub-addr: 100.64.0.55
|
|
29
|
+
|
|
20
30
|
stub-zone:
|
|
21
31
|
name: "park-your-domain.com."
|
|
22
32
|
stub-addr: 100.64.0.55
|
|
@@ -34,6 +34,25 @@ ip route add 10.0.30.0/24 via 192.168.0.254 # secure via fw-main
|
|
|
34
34
|
# Fall back to the simulated public resolver if Technitium is not yet up.
|
|
35
35
|
printf 'nameserver 192.168.0.10\nnameserver 100.100.0.1\n' > /etc/resolv.conf
|
|
36
36
|
|
|
37
|
+
# Fetch Pebble's runtime ACME root CA (Pebble generates a fresh root at each
|
|
38
|
+
# startup, unlike the static /config/pebble/pebble-ca.crt baked in at build
|
|
39
|
+
# time which only signs Pebble's own API endpoint). Without this, management
|
|
40
|
+
# can't verify certs that Caddy obtained from Pebble (iamtheinternet.org,
|
|
41
|
+
# isitup.org, etc.). Mirrors the equivalent block in fw-ext-routes.sh.
|
|
42
|
+
echo "Waiting for Pebble ACME root CA..."
|
|
43
|
+
(
|
|
44
|
+
for i in $(seq 1 60); do
|
|
45
|
+
if curl -sk https://100.64.0.100:15000/roots/0 -o /usr/local/share/ca-certificates/pebble-acme-root.crt 2>/dev/null; then
|
|
46
|
+
if [ -s /usr/local/share/ca-certificates/pebble-acme-root.crt ]; then
|
|
47
|
+
update-ca-certificates 2>/dev/null
|
|
48
|
+
echo "Pebble ACME root CA installed"
|
|
49
|
+
break
|
|
50
|
+
fi
|
|
51
|
+
fi
|
|
52
|
+
sleep 2
|
|
53
|
+
done
|
|
54
|
+
) &
|
|
55
|
+
|
|
37
56
|
# Create celilo data directory (Linux default: /var/lib/celilo/)
|
|
38
57
|
mkdir -p /var/lib/celilo/modules
|
|
39
58
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# CADDY_IP: IP of the caddy container in the DMZ (default 10.0.10.10).
|
|
5
|
+
# All test-domain queries resolve to this IP so management can reach
|
|
6
|
+
# caddy directly without hairpin NAT through the external interface.
|
|
7
|
+
CADDY_IP="${CADDY_IP:-10.0.10.10}"
|
|
8
|
+
|
|
9
|
+
# DOMAINS: comma-separated list of e2e test domains that should be
|
|
10
|
+
# split-horizon redirected to caddy. Default covers the canonical
|
|
11
|
+
# (iamtheinternet.org) plus the cross-domain test's second domain
|
|
12
|
+
# (lunacycle.net). Tests can override via DEFAULT_GATEWAY/DOMAINS env
|
|
13
|
+
# vars in docker-compose.
|
|
14
|
+
DOMAINS="${DOMAINS:-iamtheinternet.org,lunacycle.net}"
|
|
15
|
+
|
|
16
|
+
# Routing — reach comcast-resolver (100.100.0.1) via fw-main or fw-isp.
|
|
17
|
+
# DEFAULT_GATEWAY is set by docker-compose from the topology config.
|
|
18
|
+
GATEWAY="${DEFAULT_GATEWAY:-192.168.0.254}"
|
|
19
|
+
ip route del default 2>/dev/null || true
|
|
20
|
+
ip route add default via "$GATEWAY"
|
|
21
|
+
|
|
22
|
+
# Build per-domain split-horizon blocks.
|
|
23
|
+
# local-zone "redirect" returns the single A record for ALL names under
|
|
24
|
+
# the zone (including subdomains), so every hostname in the test domain
|
|
25
|
+
# reaches caddy which then reverse-proxies to the correct backend.
|
|
26
|
+
SPLIT_HORIZON_BLOCK=""
|
|
27
|
+
IFS=',' read -ra DOMAIN_LIST <<< "${DOMAINS}"
|
|
28
|
+
for d in "${DOMAIN_LIST[@]}"; do
|
|
29
|
+
SPLIT_HORIZON_BLOCK="${SPLIT_HORIZON_BLOCK} local-zone: \"${d}.\" redirect\n local-data: \"${d}. IN A ${CADDY_IP}\"\n"
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
# Generate unbound config with split-horizon for each test domain plus
|
|
33
|
+
# the standard simulator overrides (Pebble, registry).
|
|
34
|
+
cat > /etc/unbound/unbound.conf << EOF
|
|
35
|
+
server:
|
|
36
|
+
interface: 0.0.0.0
|
|
37
|
+
access-control: 0.0.0.0/0 allow
|
|
38
|
+
cache-min-ttl: 0
|
|
39
|
+
cache-max-ttl: 60
|
|
40
|
+
verbosity: 1
|
|
41
|
+
do-daemonize: no
|
|
42
|
+
do-not-query-localhost: no
|
|
43
|
+
|
|
44
|
+
$(printf "${SPLIT_HORIZON_BLOCK}")
|
|
45
|
+
# Mirror the public resolver's simulated endpoint overrides so that
|
|
46
|
+
# management uses Pebble for ACME and the e2e registry for modules.
|
|
47
|
+
local-data: "acme-v02.api.letsencrypt.org. IN A 100.64.0.100"
|
|
48
|
+
local-data: "acme-staging-v02.api.letsencrypt.org. IN A 100.64.0.100"
|
|
49
|
+
local-data: "e2e-registry.lab. IN A 100.64.0.56"
|
|
50
|
+
|
|
51
|
+
forward-zone:
|
|
52
|
+
name: "."
|
|
53
|
+
forward-addr: 100.100.0.1
|
|
54
|
+
EOF
|
|
55
|
+
|
|
56
|
+
/usr/sbin/unbound -d &
|
|
57
|
+
|
|
58
|
+
echo "internal resolver ready (split-horizon: ${DOMAINS} -> ${CADDY_IP})"
|
|
59
|
+
sleep infinity
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
FROM ubuntu:22.04
|
|
2
|
+
|
|
3
|
+
RUN apt-get update && apt-get install -y \
|
|
4
|
+
unbound \
|
|
5
|
+
iproute2 \
|
|
6
|
+
iputils-ping \
|
|
7
|
+
net-tools \
|
|
8
|
+
dnsutils \
|
|
9
|
+
procps \
|
|
10
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
11
|
+
|
|
12
|
+
COPY config/routing/resolver-internal-routes.sh /startup.sh
|
|
13
|
+
RUN chmod +x /startup.sh
|
|
14
|
+
|
|
15
|
+
EXPOSE 53/tcp 53/udp
|
|
16
|
+
CMD ["/startup.sh"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
FROM oven/bun:latest
|
|
2
|
+
|
|
3
|
+
# Caddy (for ACME/TLS termination) + tools needed by entrypoint.sh.
|
|
4
|
+
# iproute2 provides `ip` so the entrypoint can rewrite the default route
|
|
5
|
+
# through fw-ext (same pattern as Dockerfile.pebble); without it DNS for
|
|
6
|
+
# simulated public hostnames fails.
|
|
7
|
+
RUN apt-get update && apt-get install -y \
|
|
8
|
+
ca-certificates \
|
|
9
|
+
curl \
|
|
10
|
+
iproute2 \
|
|
11
|
+
debian-keyring \
|
|
12
|
+
debian-archive-keyring \
|
|
13
|
+
apt-transport-https \
|
|
14
|
+
gnupg \
|
|
15
|
+
&& curl -fsSL 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
|
|
16
|
+
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg \
|
|
17
|
+
&& curl -fsSL 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
|
|
18
|
+
> /etc/apt/sources.list.d/caddy-stable.list \
|
|
19
|
+
&& apt-get update && apt-get install -y caddy \
|
|
20
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
21
|
+
|
|
22
|
+
# Trust Pebble CA so Caddy's ACME client accepts the directory endpoint
|
|
23
|
+
COPY config/pebble/pebble-ca.crt /usr/local/share/ca-certificates/pebble-ca.crt
|
|
24
|
+
RUN update-ca-certificates
|
|
25
|
+
|
|
26
|
+
# Simulator app
|
|
27
|
+
COPY simulators/isitup/server.ts /simulator/server.ts
|
|
28
|
+
|
|
29
|
+
# Caddy config
|
|
30
|
+
COPY simulators/isitup/Caddyfile /etc/caddy/Caddyfile
|
|
31
|
+
|
|
32
|
+
# Entrypoint
|
|
33
|
+
COPY simulators/isitup/entrypoint.sh /entrypoint.sh
|
|
34
|
+
RUN chmod +x /entrypoint.sh
|
|
35
|
+
|
|
36
|
+
EXPOSE 80 443
|
|
37
|
+
CMD ["/entrypoint.sh"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
FROM oven/bun:1
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install the shared @celilo/registry-server package.
|
|
6
|
+
# Both this e2e stand-in and the celilo-registry module run the same code —
|
|
7
|
+
# packages/registry-server is the single source of truth.
|
|
8
|
+
COPY registry-server/package.json ./package.json
|
|
9
|
+
COPY registry-server/tsconfig.json ./tsconfig.json
|
|
10
|
+
COPY registry-server/src ./src
|
|
11
|
+
|
|
12
|
+
RUN bun install --production
|
|
13
|
+
|
|
14
|
+
# Bootstrap mode: the shared-infra stand-in serves modules directly from the
|
|
15
|
+
# repo's modules/ directory (volume-mounted read-only). This preserves the
|
|
16
|
+
# pre-unification convenience where tests can `celilo module import
|
|
17
|
+
# public-registry namecheap` without having to publish namecheap first.
|
|
18
|
+
#
|
|
19
|
+
# The production celilo-registry module does NOT set BOOTSTRAP_MODULES_DIR
|
|
20
|
+
# and only serves pre-published packages.
|
|
21
|
+
ENV DATA_DIR=/var/lib/celilo-registry \
|
|
22
|
+
BOOTSTRAP_MODULES_DIR=/modules \
|
|
23
|
+
BOOTSTRAP_UPLOADS_DIR=/uploads \
|
|
24
|
+
BOOTSTRAP_CACHE_DIR=/cache \
|
|
25
|
+
PATH_PREFIX= \
|
|
26
|
+
PORT=80
|
|
27
|
+
|
|
28
|
+
# /uploads is the drop-zone for .netapps that tests push via
|
|
29
|
+
# `docker compose cp`. Create the dir at build time so `docker cp` has
|
|
30
|
+
# somewhere to put files before the server runs.
|
|
31
|
+
RUN mkdir -p /uploads
|
|
32
|
+
|
|
33
|
+
EXPOSE 80
|
|
34
|
+
CMD ["bun", "run", "src/index.ts"]
|
package/netapps/authentik.netapp
CHANGED
|
Binary file
|
package/netapps/caddy.netapp
CHANGED
|
Binary file
|
package/netapps/greenwave.netapp
CHANGED
|
Binary file
|
|
Binary file
|
package/netapps/iptables.netapp
CHANGED
|
Binary file
|
|
Binary file
|
package/netapps/namecheap.netapp
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/e2e",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
+
"@celilo/cli-display": "0.1.0",
|
|
34
35
|
"yaml": "^2.8.0"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
email admin@isitup.org
|
|
3
|
+
# Pebble listens on 443 with a TLS cert whose SAN is
|
|
4
|
+
# acme-v02.api.letsencrypt.org (not an IP), so we use the hostname and
|
|
5
|
+
# rely on comcast-resolver's local-data override to reach 100.64.0.100.
|
|
6
|
+
acme_ca https://acme-v02.api.letsencrypt.org/dir
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
isitup.org {
|
|
10
|
+
reverse_proxy localhost:8080
|
|
11
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Start the Bun API server in the background, then Caddy in the foreground.
|
|
3
|
+
# Caddy terminates TLS (with a cert issued by Pebble) and reverse-proxies
|
|
4
|
+
# /api.json to Bun on localhost:8080.
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
# Route through fw-ext so we can reach comcast-resolver on isp-external
|
|
9
|
+
# (Docker's default bridge gateway has no path across networks). Without
|
|
10
|
+
# this, DNS for acme-v02.api.letsencrypt.org fails, Caddy never gets its
|
|
11
|
+
# cert, and port 443 never opens. Mirrors config/routing/pebble-startup.sh.
|
|
12
|
+
# fw-ext is a per-test container so this route is unreachable when shared
|
|
13
|
+
# infra runs alone — harmless; the test brings fw-ext up before the prober
|
|
14
|
+
# is exercised.
|
|
15
|
+
ip route del default 2>/dev/null || true
|
|
16
|
+
ip route add default via 100.64.0.1
|
|
17
|
+
|
|
18
|
+
# Fetch Pebble's runtime ACME root CA so the simulator trusts certs presented
|
|
19
|
+
# by the targets it probes (e.g. Caddy on iamtheinternet.org uses a Pebble-
|
|
20
|
+
# issued cert). Pebble regenerates its root on every startup, so the static
|
|
21
|
+
# pebble-ca.crt baked in at build time isn't enough.
|
|
22
|
+
echo "Waiting for Pebble ACME root CA..."
|
|
23
|
+
for i in $(seq 1 60); do
|
|
24
|
+
if curl -sk https://100.64.0.100:15000/roots/0 -o /usr/local/share/ca-certificates/pebble-acme-root.crt 2>/dev/null; then
|
|
25
|
+
if [ -s /usr/local/share/ca-certificates/pebble-acme-root.crt ]; then
|
|
26
|
+
update-ca-certificates 2>/dev/null
|
|
27
|
+
echo "Pebble ACME root CA installed"
|
|
28
|
+
break
|
|
29
|
+
fi
|
|
30
|
+
fi
|
|
31
|
+
sleep 2
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
cd /simulator
|
|
35
|
+
bun run server.ts &
|
|
36
|
+
BUN_PID=$!
|
|
37
|
+
|
|
38
|
+
# Wait briefly for Bun to come up before Caddy starts proxying
|
|
39
|
+
for i in $(seq 1 10); do
|
|
40
|
+
if curl -sf http://127.0.0.1:8080/health >/dev/null 2>&1; then
|
|
41
|
+
break
|
|
42
|
+
fi
|
|
43
|
+
sleep 0.3
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
# Caddy runs in foreground so the container lifecycle follows it
|
|
47
|
+
exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* isitup.org Simulator
|
|
3
|
+
*
|
|
4
|
+
* Mimics the isitup.org JSON API:
|
|
5
|
+
* GET /api.json?url=<domain>
|
|
6
|
+
*
|
|
7
|
+
* status_code values match the real API:
|
|
8
|
+
* 1 = site is up
|
|
9
|
+
* 2 = site is down (connection failed / non-2xx/3xx)
|
|
10
|
+
* 3 = bad URL (malformed)
|
|
11
|
+
*
|
|
12
|
+
* The simulator is deployed on internet-external and probes the target
|
|
13
|
+
* from that network position — so it exercises the same public-facing
|
|
14
|
+
* path a real external prober would.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const PORT = Number(process.env.PORT) || 8080;
|
|
18
|
+
|
|
19
|
+
interface ProbeResponse {
|
|
20
|
+
domain: string;
|
|
21
|
+
port: number;
|
|
22
|
+
status_code: 1 | 2 | 3;
|
|
23
|
+
response_ip: string;
|
|
24
|
+
response_code: number;
|
|
25
|
+
response_time: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function probe(targetUrl: string): Promise<ProbeResponse> {
|
|
29
|
+
let parsed: URL;
|
|
30
|
+
const withScheme = targetUrl.includes('://') ? targetUrl : `https://${targetUrl}`;
|
|
31
|
+
try {
|
|
32
|
+
parsed = new URL(withScheme);
|
|
33
|
+
} catch {
|
|
34
|
+
return {
|
|
35
|
+
domain: targetUrl,
|
|
36
|
+
port: 443,
|
|
37
|
+
status_code: 3,
|
|
38
|
+
response_ip: '',
|
|
39
|
+
response_code: 0,
|
|
40
|
+
response_time: 0,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const port = parsed.port
|
|
45
|
+
? Number(parsed.port)
|
|
46
|
+
: parsed.protocol === 'https:'
|
|
47
|
+
? 443
|
|
48
|
+
: 80;
|
|
49
|
+
|
|
50
|
+
const start = Date.now();
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(withScheme, {
|
|
53
|
+
signal: AbortSignal.timeout(10_000),
|
|
54
|
+
redirect: 'follow',
|
|
55
|
+
});
|
|
56
|
+
const responseTime = (Date.now() - start) / 1000;
|
|
57
|
+
// Real isitup treats 2xx/3xx as up. Anything else (or unreachable) is down.
|
|
58
|
+
const isUp = response.status >= 200 && response.status < 400;
|
|
59
|
+
console.log(`[isitup] ${withScheme} → HTTP ${response.status} in ${responseTime.toFixed(2)}s`);
|
|
60
|
+
return {
|
|
61
|
+
domain: parsed.hostname,
|
|
62
|
+
port,
|
|
63
|
+
status_code: isUp ? 1 : 2,
|
|
64
|
+
response_ip: '',
|
|
65
|
+
response_code: response.status,
|
|
66
|
+
response_time: responseTime,
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
console.log(`[isitup] ${withScheme} → probe error: ${message}`);
|
|
71
|
+
return {
|
|
72
|
+
domain: parsed.hostname,
|
|
73
|
+
port,
|
|
74
|
+
status_code: 2,
|
|
75
|
+
response_ip: '',
|
|
76
|
+
response_code: 0,
|
|
77
|
+
response_time: (Date.now() - start) / 1000,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const server = Bun.serve({
|
|
83
|
+
port: PORT,
|
|
84
|
+
async fetch(req) {
|
|
85
|
+
const url = new URL(req.url);
|
|
86
|
+
|
|
87
|
+
if (url.pathname === '/api.json') {
|
|
88
|
+
const target = url.searchParams.get('url');
|
|
89
|
+
if (!target) {
|
|
90
|
+
return Response.json({
|
|
91
|
+
domain: '',
|
|
92
|
+
port: 443,
|
|
93
|
+
status_code: 3,
|
|
94
|
+
response_ip: '',
|
|
95
|
+
response_code: 0,
|
|
96
|
+
response_time: 0,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const result = await probe(target);
|
|
100
|
+
console.log(`[isitup] ${target} → ${result.status_code} (${result.response_code})`);
|
|
101
|
+
return Response.json(result);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (url.pathname === '/' || url.pathname === '/health') {
|
|
105
|
+
return Response.json({ ok: true, service: 'isitup.org simulator' });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return new Response('Not Found', { status: 404 });
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
console.log(`[isitup] simulator listening on port ${server.port}`);
|
|
@@ -11,9 +11,38 @@
|
|
|
11
11
|
import { updateZone } from './zone-updater';
|
|
12
12
|
import type { DdnsRecord } from './types';
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// Real Namecheap DDNS uses a separate password per domain. The simulator
|
|
15
|
+
// supports two modes:
|
|
16
|
+
// 1. DDNS_PASSWORDS (JSON object mapping domain→password). Per-domain
|
|
17
|
+
// lookup; password must match the entry for the requested domain.
|
|
18
|
+
// 2. DDNS_PASSWORD (single string fallback). Accepted for any domain.
|
|
19
|
+
// Default `test123` keeps single-domain tests working unchanged.
|
|
20
|
+
// Mode 1 wins when set; the cross-domain e2e test exercises it via
|
|
21
|
+
// docker-compose env to verify per-domain password routing.
|
|
22
|
+
const PASSWORD_MAP: Record<string, string> = (() => {
|
|
23
|
+
const raw = process.env.DDNS_PASSWORDS;
|
|
24
|
+
if (!raw) return {};
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
28
|
+
const out: Record<string, string> = {};
|
|
29
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
30
|
+
if (typeof v === 'string') out[k] = v;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
console.warn(`[ddns] Failed to parse DDNS_PASSWORDS env var: ${raw}`);
|
|
36
|
+
}
|
|
37
|
+
return {};
|
|
38
|
+
})();
|
|
39
|
+
const FALLBACK_PASSWORD = process.env.DDNS_PASSWORD || 'test123';
|
|
15
40
|
const PORT = Number(process.env.DDNS_PORT) || 8080;
|
|
16
41
|
|
|
42
|
+
function expectedPasswordFor(domain: string): string {
|
|
43
|
+
return PASSWORD_MAP[domain] ?? FALLBACK_PASSWORD;
|
|
44
|
+
}
|
|
45
|
+
|
|
17
46
|
// Track registered records for debugging
|
|
18
47
|
const records: Map<string, DdnsRecord> = new Map();
|
|
19
48
|
|
|
@@ -66,7 +95,7 @@ const server = Bun.serve({
|
|
|
66
95
|
return xmlResponse(1, '', ['Missing required parameters: host, domain']);
|
|
67
96
|
}
|
|
68
97
|
|
|
69
|
-
if (password !==
|
|
98
|
+
if (password !== expectedPasswordFor(domain)) {
|
|
70
99
|
return xmlResponse(1, ip, ['Passwords do not match']);
|
|
71
100
|
}
|
|
72
101
|
|
package/src/cli/build.ts
CHANGED
|
@@ -110,17 +110,27 @@ function buildDockerImages(pkgDir: string): void {
|
|
|
110
110
|
const total = dockerfiles.length;
|
|
111
111
|
const buildStart = Date.now();
|
|
112
112
|
|
|
113
|
+
// Per-Dockerfile build-context overrides. Default context is the e2e
|
|
114
|
+
// package root. Dockerfiles that need access to sibling packages (e.g.
|
|
115
|
+
// Dockerfile.registry COPYs from packages/registry-server) get `..`
|
|
116
|
+
// (= packages/). Must match the compose-time context in
|
|
117
|
+
// docker-compose-generator.ts.
|
|
118
|
+
const contextOverrides: Record<string, string> = {
|
|
119
|
+
'Dockerfile.registry': '..',
|
|
120
|
+
};
|
|
121
|
+
|
|
113
122
|
for (let i = 0; i < total; i++) {
|
|
114
123
|
const file = dockerfiles[i];
|
|
115
124
|
const name = file.replace('Dockerfile.', '');
|
|
116
125
|
const tag = `celilo-e2e/${name}`;
|
|
126
|
+
const context = contextOverrides[file] ?? '.';
|
|
117
127
|
|
|
118
128
|
process.stdout.write(` [${i + 1}/${total}] ${String(name).padEnd(25)} `);
|
|
119
129
|
|
|
120
130
|
const start = Date.now();
|
|
121
131
|
const result = spawnSync(
|
|
122
132
|
'docker',
|
|
123
|
-
['build', '-t', tag, '-f', join(dockerDir, file),
|
|
133
|
+
['build', '-t', tag, '-f', join(dockerDir, file), context],
|
|
124
134
|
{ cwd: pkgDir, stdio: 'pipe' },
|
|
125
135
|
);
|
|
126
136
|
const elapsed = Math.round((Date.now() - start) / 1000);
|