Skip to content

Commit 9602efe

Browse files
committed
feat(templates): caddy-proxy puts a Moshpit name in front of a local service
1 parent aa63af8 commit 9602efe

4 files changed

Lines changed: 187 additions & 0 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Caddy in front of a service already running on this box, published at a
2+
# Moshpit name.
3+
#
4+
# The `http://` is required and is not a style choice. A Moshpit ending is not
5+
# in the public DNS root, so no certificate authority will issue for it — leave
6+
# the scheme off and Caddy will try to provision a certificate, fail, and never
7+
# bring the site up. Everything served at a Moshpit name is plain HTTP.
8+
#
9+
# Nothing here resolves the name. The visitor's resolver did that; by the time a
10+
# request arrives Caddy has only a Host header to match on, which is why the
11+
# site address must be the name exactly as it is registered.
12+
13+
http://{$MOSHPIT_NAME:foo.whatever} {
14+
reverse_proxy {$APP_ADDR:127.0.0.1:8080}
15+
16+
log {
17+
output file /var/log/caddy/moshpit-service.log
18+
}
19+
}
20+
21+
# Every subdomain too. This block answers only once the name publishes a
22+
# wildcard record — DNS Records tab in the Pit, the `*.` option with an AAAA
23+
# at this box — because until then `anything.foo.whatever` resolves to nothing
24+
# and no request reaches Caddy to match. Uncomment both together.
25+
#
26+
# The app sees which subdomain was asked for in the Host header, and Caddy
27+
# matches exactly one label deep: `api.foo.whatever` answers,
28+
# `a.b.foo.whatever` does not.
29+
#
30+
# http://*.{$MOSHPIT_NAME:foo.whatever} {
31+
# reverse_proxy {$APP_ADDR:127.0.0.1:8080}
32+
#
33+
# log {
34+
# output file /var/log/caddy/moshpit-service.log
35+
# }
36+
# }
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# caddy-proxy
2+
3+
A name in front of something that is already running. No app, no database, no
4+
runtime to keep alive — Caddy answers the Moshpit name and hands every request
5+
to a local service on `127.0.0.1:8080` (or wherever `APP_ADDR` says).
6+
7+
This is the template for "I have a thing on this box, put a name on it": a
8+
dev server, a dashboard, grafana, a game panel, anything that already listens
9+
on loopback.
10+
11+
## The part that surprises people
12+
13+
Three machines' worth of concerns, and they fail independently:
14+
15+
| | needs the resolver? | what it does |
16+
|---|---|---|
17+
| the box serving the name | **no** | Caddy matches a `Host` header, nothing more |
18+
| the registry || holds the address the name points at |
19+
| every visitor | **yes** | `sudo moshcode dns enable`, or the name resolves to nothing |
20+
21+
Nothing on the server ever resolves its own name. That is why there is no DNS
22+
software in this template.
23+
24+
## Deploying
25+
26+
1. **Point the name at the box.** In the Pit, set `points at` to its public
27+
IPv6 address — bare, no scheme, no brackets, no port:
28+
29+
```sh
30+
ip -6 addr show scope global | grep inet6
31+
```
32+
33+
Pick the globally routable one. An `fd..`/`fc..` address is unique-local
34+
(Tailscale and friends live there) and the registry refuses it, because a
35+
name pointed at one resolves somewhere only you can reach.
36+
37+
2. **Serve it.** The service stays bound to loopback — Caddy is its only
38+
client, and binding it publicly publishes it on a port nothing
39+
virtual-hosts.
40+
41+
```sh
42+
export MOSHPIT_NAME=foo.whatever
43+
export APP_ADDR=127.0.0.1:8080 # the default; change only if the service differs
44+
sudo cp Caddyfile /etc/caddy/Caddyfile
45+
sudo systemctl reload caddy
46+
sudo ufw allow 80/tcp
47+
```
48+
49+
3. **Reach it,** on any machine that should see the name:
50+
51+
```sh
52+
sudo moshcode dns enable
53+
sudo cp deploy/moshcode-dns.service /etc/systemd/system/ # survives reboot
54+
sudo systemctl enable --now moshcode-dns
55+
```
56+
57+
## Every subdomain at once
58+
59+
One name covers one hostname. To answer `anything.foo.whatever` too — one
60+
service per subdomain, or a wildcard tenant app — do both halves, in either
61+
order, because neither works without the other:
62+
63+
1. In the Pit's **DNS Records** tab, publish an **AAAA** record on the
64+
`*.foo.whatever` option pointing at the same box. Until that exists the
65+
subdomains resolve to nothing and no request ever reaches Caddy.
66+
2. Uncomment the wildcard block at the bottom of the Caddyfile and reload.
67+
Caddy matches exactly one label deep, and the app reads which subdomain was
68+
asked for from the `Host` header.
69+
70+
`foo.whatever` itself is not covered by a wildcard — keep the apex block (and
71+
its own AAAA or `points at`) for that. This is how DNS wildcards work, not a
72+
choice Caddy made.
73+
74+
## Verifying, one layer at a time
75+
76+
A failure at any layer looks identical in a browser, so do not start there.
77+
78+
```sh
79+
# Server only — no DNS involved. Proves Caddy, the firewall, and the service.
80+
curl -6 -H "Host: foo.whatever" http://[YOUR:V6:ADDR]/
81+
82+
# Resolver only. Proves the registry and the bridge.
83+
moshcode dns resolve foo.whatever
84+
85+
# Both.
86+
curl -6 http://foo.whatever/
87+
```
88+
89+
If the first works and the last does not, it is DNS. If the first fails, stop
90+
looking at DNS.
91+
92+
## Known limits
93+
94+
- **No HTTPS, ever.** No CA will issue for an ending outside the DNS root. That
95+
rules out secure cookies, service workers, and WebCrypto in the browser. The
96+
`http://` in the Caddyfile is what stops Caddy trying and failing.
97+
- **Only machines running the resolver can reach the name.** Not phones, not a
98+
colleague who has not installed it, not webhooks.
99+
`pit.moshcode.sh/n/foo.whatever` is the URL for people who installed nothing.
100+
- **Subdomains are opt-in.** `foo.whatever` works out of the box;
101+
`www.foo.whatever` works only with the wildcard record described above.
102+
- **Port 80 only** on the resolver path. A DNS record carries an address and
103+
has nowhere to put a port, which is why Caddy listens on 80 and the
104+
`host:port` part lives here, not in the registry.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# The Moshpit resolver, kept running across reboots.
2+
#
3+
# `moshcode dns enable` sets up two halves: a systemd-resolved drop-in that
4+
# routes Moshpit endings at the bridge, and the bridge process itself. The
5+
# drop-in is a file and survives a reboot on its own. The process does not —
6+
# so after a restart the routing still points at a port with nothing behind it,
7+
# and every Moshpit name stops resolving with no obvious cause. This unit is
8+
# the missing half.
9+
#
10+
# sudo cp deploy/moshcode-dns.service /etc/systemd/system/
11+
# sudo systemctl enable --now moshcode-dns
12+
#
13+
# Install this on machines that need to REACH Moshpit names. A box that only
14+
# serves one does not need it — Caddy answers whatever Host header arrives and
15+
# never resolves its own name.
16+
17+
[Unit]
18+
Description=Moshpit DNS bridge
19+
After=network-online.target
20+
Wants=network-online.target
21+
Before=systemd-resolved.service
22+
23+
[Service]
24+
Type=simple
25+
# Port 5354 is unprivileged, so this does not need root. The trade-off is that
26+
# the parking responder cannot take port 80 and falls back to the public
27+
# parking address — which only affects names that point nowhere yet.
28+
ExecStart=/usr/bin/env moshcode dns start --port 5354
29+
Restart=always
30+
RestartSec=2
31+
32+
DynamicUser=yes
33+
NoNewPrivileges=yes
34+
PrivateTmp=yes
35+
ProtectSystem=strict
36+
ProtectHome=yes
37+
38+
[Install]
39+
WantedBy=multi-user.target
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "caddy-proxy",
3+
"description": "Caddy proxying a Moshpit name to a service already running on the box — no app, no database, just the name in front",
4+
"vars": {
5+
"MOSHPIT_NAME": "the registered name to serve, e.g. foo.whatever",
6+
"APP_ADDR": "where the local service listens, loopback only (default 127.0.0.1:8080)"
7+
}
8+
}

0 commit comments

Comments
 (0)