hub: serve the Hub on its own port instead of through Caddy
Caddy fronted the Hub at http://sovransystemsos.local, but the Hub already listens on 0.0.0.0:8937 itself, and nothing Caddy added is something it needs: - Not the name. That is avahi's: mDNS advertises a hostname, not a port, so the name resolves wherever the Hub listens. - Not TLS (the site was plain http), not authentication, not cache headers. The header block duplicated NoCacheMiddleware, and its Clear-Site-Data ("cache") overrode the app's stronger ("cache", "storage"). - Not access control, and this is the point. With ports 80/443 forwarded for public services, a Host header on those ports reached the Hub. That second door is how the reported bug happened, andc33457fguards it with an address check instead of closing it. The Hub is now served on port 8937 only, at http://sovransystemsos.local:8937, and Caddy has no site for it. The only thing Caddy answers on 80/443 is the public sites. Caddy keeps Ride The Lightning (:3051) and Mempool (:60847), because those do need it: Sovran_Bitcoin binds both to 127.0.0.1 and RTL's unit is sandboxed to loopback besides, so Caddy is how the local network reaches them. - caddy.nix: no Hub site. Caddy runs wherever RTL and Mempool do, which includes Bitcoin Node Only. There it did not run at all (enable was needsHttpsPorts || extraVhosts != ""), so :3051 and :60847 were open in the firewall with nothing listening. Ports 80/443 still follow needsHttpsPorts alone, so Node Only does not open them. The two sites are written only where their service exists; they were unconditional. - sovran-hub.nix: 8937 follows the new hub.directPort, 60847 follows Mempool. It used to be `[ 8937 60847 ]` on every role, Desktop Only included. - roles.nix: hub.directPort defaults to !roles.desktop: open on Server + Desktop and Bitcoin Node Only, closed on Desktop Only, where the Hub is reached from the machine itself through the desktop window on localhost. - The bind stays 0.0.0.0, which is IPv4 only: with that bind [::1]:8937 is refused and "localhost" falls back to 127.0.0.1. That is on purpose and is now said in the comment. An IPv6 listener would let in clients whose global addresses the Hub cannot tell from a stranger's, which is the question the previous commit declines to answer by guessing. - README, SECURITY.md and two strings in index.html give the new URL. Behaviour changes: the Hub's address gains :8937, and http://sovransystemsos.local on port 80 no longer reaches it. Bitcoin Node Only now runs Caddy. Evaluated with nix eval (nixpkgs as flake.lock pins it, Sovran_Bitcoin at the locked revision), firewall TCP ports per role:c33457fthis commit Server + Desktop 22 80 443 3051 8937 60847 22 80 443 3051 8937 Bitcoin Node Only 22 3051 8937 60847 22 3051 8937 60847 (Caddy now runs) Desktop Only 22 8937 60847 22 Port 22 is open on every role although sshd listens on loopback only; the last commit of this series deals with that. The Caddyfile the module really generates (the evaluated generator script, run, then `caddy validate` with Caddy 2.9.1): Node Only gets the two sites and nothing else; Server + Desktop with every domain configured gets the seven domain sites plus :3051 and :60847 and no mention of the Hub; with Bitcoin off there are no local-network sites; Node Only with Bitcoin off and no domains leaves Caddy off. Add tests/test_hub_direct.py and keep tests/test_caddy_lan_only.py for the two sites it still covers.
This commit is contained in:
@@ -316,8 +316,8 @@ the tools of your selected mode already in place.
|
|||||||
Prefer to keep using Windows, macOS, Linux, Android, or iOS? Install
|
Prefer to keep using Windows, macOS, Linux, Android, or iOS? Install
|
||||||
Sovran_SystemsOS on a separate computer and let it run quietly on your local
|
Sovran_SystemsOS on a separate computer and let it run quietly on your local
|
||||||
network, with or without a monitor. From any other device on the same network,
|
network, with or without a monitor. From any other device on the same network,
|
||||||
open a browser, visit `http://sovransystemsos.local`, and manage everything
|
open a browser, visit `http://sovransystemsos.local:8937`, and manage
|
||||||
from [The Sovran Hub](#the-sovran-hub).
|
everything from [The Sovran Hub](#the-sovran-hub).
|
||||||
|
|
||||||
Your existing devices stay familiar. Sovran_SystemsOS provides the independent
|
Your existing devices stay familiar. Sovran_SystemsOS provides the independent
|
||||||
infrastructure behind them.
|
infrastructure behind them.
|
||||||
@@ -354,7 +354,7 @@ From one place, the Hub helps you:
|
|||||||
│ │ │
|
│ │ │
|
||||||
Windows laptop Phone or tablet Mac or Linux
|
Windows laptop Phone or tablet Mac or Linux
|
||||||
│ │ │
|
│ │ │
|
||||||
└──────── Browser: sovransystemsos.local ────┘
|
└─────── Browser: sovransystemsos.local:8937 ─┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
┌──────────────────────────┐
|
┌──────────────────────────┐
|
||||||
@@ -376,9 +376,10 @@ Keep using the devices you already own. Sovran_SystemsOS becomes the private
|
|||||||
Bitcoin and digital infrastructure behind them.
|
Bitcoin and digital infrastructure behind them.
|
||||||
|
|
||||||
> **Local access:** the Hub is available at
|
> **Local access:** the Hub is available at
|
||||||
> `http://sovransystemsos.local` to devices connected to the same local
|
> `http://sovransystemsos.local:8937` to devices connected to the same local
|
||||||
> network. It is protected by authentication and is not automatically exposed
|
> network (not on Desktop, which publishes nothing). It is protected by
|
||||||
> to the public internet.
|
> authentication, answers only your local network, and is not automatically
|
||||||
|
> exposed to the public internet.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -536,18 +537,20 @@ Open the Hub directly from the Sovran_SystemsOS desktop, or from any other
|
|||||||
device on the same local network at:
|
device on the same local network at:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
http://sovransystemsos.local
|
http://sovransystemsos.local:8937
|
||||||
```
|
```
|
||||||
|
|
||||||
Sign in with your Sovran_SystemsOS credentials.
|
Sign in with your Sovran_SystemsOS credentials. Desktop does not publish the Hub
|
||||||
|
on the network, so in that mode open it from the desktop.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>If sovransystemsos.local does not open</strong></summary>
|
<summary><strong>If sovransystemsos.local:8937 does not open</strong></summary>
|
||||||
|
|
||||||
1. Make sure the Sovran_SystemsOS machine is powered on, and allow it a few
|
1. Make sure the Sovran_SystemsOS machine is powered on, and allow it a few
|
||||||
minutes to finish starting.
|
minutes to finish starting.
|
||||||
2. Make sure both devices are connected to the same local network, and that
|
2. Make sure both devices are connected to the same local network, and that
|
||||||
you entered the full address `http://sovransystemsos.local`.
|
you entered the full address `http://sovransystemsos.local:8937`,
|
||||||
|
including the `:8937`.
|
||||||
3. Avoid guest Wi-Fi networks, which may prevent devices from seeing one
|
3. Avoid guest Wi-Fi networks, which may prevent devices from seeing one
|
||||||
another.
|
another.
|
||||||
4. Temporarily disconnect any VPN that may interfere with local-network
|
4. Temporarily disconnect any VPN that may interfere with local-network
|
||||||
|
|||||||
+28
-9
@@ -35,21 +35,40 @@ external networks and are outside a “fully offline” model.
|
|||||||
The local Hub currently uses HTTP. Authentication does not encrypt local network
|
The local Hub currently uses HTTP. Authentication does not encrypt local network
|
||||||
traffic, so use a trusted LAN and avoid public or guest Wi-Fi.
|
traffic, so use a trusted LAN and avoid public or guest Wi-Fi.
|
||||||
|
|
||||||
Caddy serves the Hub (`sovransystemsos.local`), Ride The Lightning (port 3051),
|
The Hub is served on port 8937, on its own: Caddy does not front it. Server +
|
||||||
and Mempool (port 60847) only to this computer and to clients on your local
|
Desktop and Bitcoin Node Only open that port in the firewall, so other devices
|
||||||
network (private, link-local, and VPN addresses), even when ports 80 and 443 are
|
on your local network reach the Hub at `http://sovransystemsos.local:8937`.
|
||||||
forwarded to this computer for public services. Other IPv4 clients get the
|
Forwarding ports 80 and 443 for public services does not put the Hub in front of
|
||||||
connection closed. IPv6 global addresses are not filtered.
|
the internet, because the only thing Caddy answers on those ports is the public
|
||||||
|
sites.
|
||||||
|
|
||||||
|
On Desktop Only the Hub is not published at all. It is reachable only from the
|
||||||
|
machine itself, through the desktop application window on localhost. Desktop
|
||||||
|
Only is the role most likely to be used away from home, and a root-capable admin
|
||||||
|
UI has no business listening on a coffee-shop network.
|
||||||
|
`sovran_systemsOS.hub.directPort = true` in `custom.nix` opens port 8937 if you
|
||||||
|
do want to reach a Desktop Only Hub from another device.
|
||||||
|
|
||||||
The Hub also checks every client itself, before it shows a login page. It runs
|
The Hub also checks every client itself, before it shows a login page. It runs
|
||||||
as root, so it answers only this computer and the local network (loopback,
|
as root, so it answers only this computer and the local network (loopback,
|
||||||
private, VPN and link-local addresses) and turns everyone else away, however
|
private, VPN and link-local addresses) and turns everyone else away, however
|
||||||
they reached it. Global IPv6 addresses are turned away too: a laptop on your
|
they reached it. The Hub listens on IPv4 only, so IPv6 clients do not reach it
|
||||||
network and a stranger on the internet look the same by address alone. If your
|
at all; if that ever changes, global IPv6 addresses would be turned away,
|
||||||
devices use addresses outside the local ranges, list their networks in
|
because a laptop on your network and a stranger on the internet look the same
|
||||||
`sovran_systemsOS.hub.extraLanNetworks` in `custom.nix`;
|
by address alone. If your devices use addresses outside the local ranges, list
|
||||||
|
their networks in `sovran_systemsOS.hub.extraLanNetworks` in `custom.nix`;
|
||||||
`sovran_systemsOS.hub.lanOnly = false` turns the check off.
|
`sovran_systemsOS.hub.lanOnly = false` turns the check off.
|
||||||
|
|
||||||
|
The check goes by the address a connection comes from. A router that rewrites
|
||||||
|
that address when it forwards a port makes an outsider look local, so the check
|
||||||
|
is a second lock and not a reason to forward port 8937: don't.
|
||||||
|
|
||||||
|
Caddy serves Ride The Lightning (port 3051) and Mempool (port 60847) only to
|
||||||
|
this computer and to clients on your local network (private, link-local, and VPN
|
||||||
|
addresses), even when ports 80 and 443 are forwarded to this computer for public
|
||||||
|
services. Other IPv4 clients get the connection closed. IPv6 global addresses
|
||||||
|
are not filtered.
|
||||||
|
|
||||||
### Public services and your home IP address
|
### Public services and your home IP address
|
||||||
|
|
||||||
Server + Desktop publishes services under your own domain. The Dynamic DNS
|
Server + Desktop publishes services under your own domain. The Dynamic DNS
|
||||||
|
|||||||
@@ -264,7 +264,7 @@
|
|||||||
<h3>Network</h3>
|
<h3>Network</h3>
|
||||||
<div class="net-row"><span class="net-k">LAN</span><span class="ip-value" id="ip-internal">…</span></div>
|
<div class="net-row"><span class="net-k">LAN</span><span class="ip-value" id="ip-internal">…</span></div>
|
||||||
<div class="net-row"><span class="net-k">WAN</span><span class="ip-value" id="ip-external">…</span></div>
|
<div class="net-row"><span class="net-k">WAN</span><span class="ip-value" id="ip-external">…</span></div>
|
||||||
<div class="sub">sovransystemsos.local</div>
|
<div class="sub">sovransystemsos.local:8937</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="welcome-dyn" id="wc-more"></div>
|
<div class="welcome-dyn" id="wc-more"></div>
|
||||||
@@ -520,7 +520,7 @@
|
|||||||
<div class="security-reset-password-box" id="security-reset-new-password"> </div>
|
<div class="security-reset-password-box" id="security-reset-new-password"> </div>
|
||||||
<p class="security-reset-password-warning">
|
<p class="security-reset-password-warning">
|
||||||
✍️ <strong>Write this down now.</strong><br />
|
✍️ <strong>Write this down now.</strong><br />
|
||||||
You will need it to log in to your computer<br />and the Sovran Hub at <em>sovransystemsos.local</em>.
|
You will need it to log in to your computer<br />and the Sovran Hub at <em>sovransystemsos.local:8937</em>.
|
||||||
</p>
|
</p>
|
||||||
<button class="security-reset-reboot-btn" id="security-reset-reboot-btn" disabled>
|
<button class="security-reset-reboot-btn" id="security-reset-reboot-btn" disabled>
|
||||||
I have written down my new password — Restart Entire System
|
I have written down my new password — Restart Entire System
|
||||||
|
|||||||
+45
-39
@@ -14,13 +14,48 @@ let
|
|||||||
|| config.sovran_systemsOS.features.haven
|
|| config.sovran_systemsOS.features.haven
|
||||||
|| config.sovran_systemsOS.features."nwc-wallets"
|
|| config.sovran_systemsOS.features."nwc-wallets"
|
||||||
|| config.sovran_systemsOS.features.element-calling;
|
|| config.sovran_systemsOS.features.element-calling;
|
||||||
|
|
||||||
|
# RTL and Mempool listen on loopback only: Sovran_Bitcoin binds them to
|
||||||
|
# 127.0.0.1, and RTL's unit is sandboxed to loopback besides. Caddy is how
|
||||||
|
# the local network reaches them (:3051 and :60847), so it has to run
|
||||||
|
# wherever they do. That includes Bitcoin Node Only, which has no
|
||||||
|
# domain-based service and so no other reason to run Caddy.
|
||||||
|
#
|
||||||
|
# The Hub is not one of these. It listens on 0.0.0.0:8937 itself, so it is
|
||||||
|
# served on its own port rather than through Caddy: the one service that
|
||||||
|
# runs as root has nothing in front of it that it does not need, and the
|
||||||
|
# public sites on ports 80/443 cannot be asked for it by Host header.
|
||||||
|
servesRtl = config.sovran_systemsOS.services.bitcoin;
|
||||||
|
servesMempool = servesRtl && config.sovran_systemsOS.features.mempool;
|
||||||
|
|
||||||
|
caddyEnabled = needsHttpsPorts || extraVhosts != "" || servesRtl;
|
||||||
|
|
||||||
|
# Sites for the local network, one per loopback-only service. Written after
|
||||||
|
# the public domain sites; each exists only where its service does.
|
||||||
|
bitcoinUiSites =
|
||||||
|
lib.optionalString servesRtl ''
|
||||||
|
|
||||||
|
:3051 {
|
||||||
|
import sovran_lan_only
|
||||||
|
reverse_proxy :3050
|
||||||
|
encode gzip zstd
|
||||||
|
}
|
||||||
|
''
|
||||||
|
+ lib.optionalString servesMempool ''
|
||||||
|
|
||||||
|
:60847 {
|
||||||
|
import sovran_lan_only
|
||||||
|
reverse_proxy :60845
|
||||||
|
encode gzip zstd
|
||||||
|
}
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
services.caddy = {
|
services.caddy = {
|
||||||
# Only enable Caddy when at least one domain-based service needs it or
|
# Caddy runs when a domain-based service needs it, when the operator has
|
||||||
# the operator has defined custom vhosts. This prevents Caddy from
|
# defined custom vhosts, or when it is the way to reach RTL and Mempool.
|
||||||
# running on Desktop Only installs that have no web services configured.
|
# Desktop Only has none of those, so Caddy stays off there.
|
||||||
enable = needsHttpsPorts || extraVhosts != "";
|
enable = caddyEnabled;
|
||||||
user = "caddy";
|
user = "caddy";
|
||||||
group = "root";
|
group = "root";
|
||||||
};
|
};
|
||||||
@@ -90,7 +125,7 @@ EOF
|
|||||||
''}
|
''}
|
||||||
|
|
||||||
# ── LAN-only guard ──────────────────────────────
|
# ── LAN-only guard ──────────────────────────────
|
||||||
# The Hub, RTL and Mempool sites below are meant for this home network
|
# The RTL and Mempool sites below are meant for this home network
|
||||||
# only. Forwarding ports 80/443 on the router also lets other clients
|
# only. Forwarding ports 80/443 on the router also lets other clients
|
||||||
# reach Caddy, so these sites check where a request comes from, not just
|
# reach Caddy, so these sites check where a request comes from, not just
|
||||||
# which Host it asks for. Anyone else gets the connection closed.
|
# which Host it asks for. Anyone else gets the connection closed.
|
||||||
@@ -222,40 +257,11 @@ $LIGHTNING {
|
|||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Sovran Hub (LAN access via mDNS) ────────────
|
# ── RTL and Mempool (local network) ─────────────
|
||||||
cat >> /run/caddy/Caddyfile <<EOF
|
# Only where those services run; see bitcoinUiSites above.
|
||||||
|
cat >> /run/caddy/Caddyfile <<'LAN_SITES_EOF'
|
||||||
http://sovransystemsos.local {
|
${bitcoinUiSites}
|
||||||
import sovran_lan_only
|
LAN_SITES_EOF
|
||||||
reverse_proxy localhost:8937
|
|
||||||
header {
|
|
||||||
Clear-Site-Data "\"cache\""
|
|
||||||
Cache-Control "no-store, no-cache, must-revalidate, max-age=0"
|
|
||||||
Pragma "no-cache"
|
|
||||||
Expires "0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# ── RTL (LAN access) ────────────────────────────
|
|
||||||
cat >> /run/caddy/Caddyfile <<EOF
|
|
||||||
|
|
||||||
:3051 {
|
|
||||||
import sovran_lan_only
|
|
||||||
reverse_proxy :3050
|
|
||||||
encode gzip zstd
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# ── Mempool (LAN access) ────────────────────────
|
|
||||||
cat >> /run/caddy/Caddyfile <<EOF
|
|
||||||
|
|
||||||
:60847 {
|
|
||||||
import sovran_lan_only
|
|
||||||
reverse_proxy :60845
|
|
||||||
encode gzip zstd
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# ── Custom vhosts from custom.nix ──────────────
|
# ── Custom vhosts from custom.nix ──────────────
|
||||||
cat >> /run/caddy/Caddyfile <<'CUSTOM_VHOSTS_EOF'
|
cat >> /run/caddy/Caddyfile <<'CUSTOM_VHOSTS_EOF'
|
||||||
|
|||||||
@@ -84,6 +84,27 @@
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
directPort = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
default = !config.sovran_systemsOS.roles.desktop;
|
||||||
|
defaultText = lib.literalExpression "!config.sovran_systemsOS.roles.desktop";
|
||||||
|
description = ''
|
||||||
|
Open port 8937 on the firewall, so that other devices on the local
|
||||||
|
network can reach the Hub at http://sovransystemsos.local:8937.
|
||||||
|
|
||||||
|
On by default for Server + Desktop and Bitcoin Node Only. Off on
|
||||||
|
Desktop Only, the role most likely to be used away from home: there
|
||||||
|
nothing is published, and the Hub is reachable only from this
|
||||||
|
computer, through the desktop application window on localhost. Set it
|
||||||
|
to true in custom.nix if you do want to reach a Desktop Only Hub from
|
||||||
|
another device.
|
||||||
|
|
||||||
|
The Hub runs as root, so it checks every client itself (see
|
||||||
|
sovran_systemsOS.hub.lanOnly); the firewall opening only decides
|
||||||
|
whether a packet may reach it at all.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
extraLanNetworks = lib.mkOption {
|
extraLanNetworks = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
|
|||||||
@@ -487,6 +487,12 @@ os.environ["SOVRAN_HUB_ICONS"] = os.path.join("$out", "share", "sovran-hub", "i
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"sovran_systemsos_web.server:app",
|
"sovran_systemsos_web.server:app",
|
||||||
|
# IPv4 only, on purpose. The desktop launcher uses "localhost", which
|
||||||
|
# falls back to 127.0.0.1, and other devices reach the Hub over IPv4 too.
|
||||||
|
# An IPv6 listener would admit clients whose global addresses the Hub's own
|
||||||
|
# check cannot tell from a stranger's (see LanPolicy). Which devices may
|
||||||
|
# connect is up to the firewall (hub.directPort) and that check
|
||||||
|
# (hub.lanOnly), not this bind.
|
||||||
host="0.0.0.0",
|
host="0.0.0.0",
|
||||||
port=8937,
|
port=8937,
|
||||||
log_level="info",
|
log_level="info",
|
||||||
@@ -599,7 +605,14 @@ in
|
|||||||
|
|
||||||
environment.systemPackages = [ sovran-hub-web ];
|
environment.systemPackages = [ sovran-hub-web ];
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 8937 60847 ];
|
# The Hub is served on its own port, not through Caddy (see caddy.nix).
|
||||||
|
# Nothing here filters by client address: that is the Hub's own check
|
||||||
|
# (sovran_systemsOS.hub.lanOnly), and which networks can route to this
|
||||||
|
# computer at all is the router's call.
|
||||||
|
# 60847 is where Caddy serves Mempool, so it is open only when Mempool is.
|
||||||
|
networking.firewall.allowedTCPPorts =
|
||||||
|
lib.optionals cfg.hub.directPort [ 8937 ]
|
||||||
|
++ lib.optionals (cfg.services.bitcoin && cfg.features.mempool) [ 60847 ];
|
||||||
|
|
||||||
# ── Auto-launch Hub in browser on login ───────────────────────
|
# ── Auto-launch Hub in browser on login ───────────────────────
|
||||||
environment.etc."xdg/autostart/sovran-hub-autolaunch.desktop".text = ''
|
environment.etc."xdg/autostart/sovran-hub-autolaunch.desktop".text = ''
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"""Guards for the LAN-only Caddy sites.
|
"""Guards for the LAN-only Caddy sites.
|
||||||
|
|
||||||
The Hub (http://sovransystemsos.local), Ride The Lightning (:3051) and Mempool
|
Ride The Lightning (:3051) and Mempool (:60847) sites are for the home network.
|
||||||
(:60847) sites are for the home network. Caddy has to check where a request comes
|
Caddy has to check where a request comes from because forwarding ports 80/443 on
|
||||||
from because forwarding ports 80/443 on the router lets other clients reach it too.
|
the router lets other clients reach it too. The domain sites for the operator's
|
||||||
The domain sites for the operator's own public services must stay public.
|
own public services must stay public. (The Hub is not a Caddy site: it is served
|
||||||
|
on its own port and checks its own clients.)
|
||||||
|
|
||||||
These read modules/core/caddy.nix like the nix-file checks in test_security.py:
|
These read modules/core/caddy.nix like the nix-file checks in test_security.py:
|
||||||
nothing is run and nothing touches the network.
|
nothing is run and nothing touches the network.
|
||||||
@@ -20,7 +21,7 @@ _ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
|
|||||||
_PRIVATE_RANGES = ["192.168.0.0/16", "172.16.0.0/12", "10.0.0.0/8",
|
_PRIVATE_RANGES = ["192.168.0.0/16", "172.16.0.0/12", "10.0.0.0/8",
|
||||||
"127.0.0.1/8", "fd00::/8", "::1"]
|
"127.0.0.1/8", "fd00::/8", "::1"]
|
||||||
|
|
||||||
_LAN_SITES = ("http://sovransystemsos.local", ":3051", ":60847")
|
_LAN_SITES = (":3051", ":60847")
|
||||||
|
|
||||||
|
|
||||||
def _read(*parts):
|
def _read(*parts):
|
||||||
@@ -81,9 +82,10 @@ class LanOnlySites(unittest.TestCase):
|
|||||||
snippet = _snippet(self.src)
|
snippet = _snippet(self.src)
|
||||||
self.assertIsNotNone(snippet, "(sovran_lan_only) snippet not found")
|
self.assertIsNotNone(snippet, "(sovran_lan_only) snippet not found")
|
||||||
self.assertRegex(snippet, r"(?m)^\s*abort @outside\s*$")
|
self.assertRegex(snippet, r"(?m)^\s*abort @outside\s*$")
|
||||||
# defined before the first site that imports it
|
# defined before the sites that import it are written (the sites come
|
||||||
|
# from bitcoinUiSites, which the generator appends after the snippet)
|
||||||
self.assertLess(self.src.index("(sovran_lan_only) {"),
|
self.assertLess(self.src.index("(sovran_lan_only) {"),
|
||||||
self.src.index("import sovran_lan_only"))
|
self.src.index("${bitcoinUiSites}"))
|
||||||
|
|
||||||
def test_guard_ranges(self):
|
def test_guard_ranges(self):
|
||||||
nets = _allowed_networks(_snippet(self.src))
|
nets = _allowed_networks(_snippet(self.src))
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
"""Guards for serving the Hub on its own port instead of through Caddy.
|
||||||
|
|
||||||
|
The Hub is the one service that runs as root. It listens on 0.0.0.0:8937
|
||||||
|
itself, so Caddy adds nothing it needs: not TLS (the site was plain http), not
|
||||||
|
authentication, not cache headers (the app sets its own). What it did add was a
|
||||||
|
second door: with ports 80/443 forwarded for public services, a Host header on
|
||||||
|
those ports reached the Hub. The Hub is therefore served on port 8937 only, and
|
||||||
|
Caddy keeps the two services it is actually needed for, because they listen on
|
||||||
|
loopback only: Ride The Lightning (:3051) and Mempool (:60847).
|
||||||
|
|
||||||
|
Like the other nix-file checks these read the modules as text: nothing is run
|
||||||
|
and nothing touches the network.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), ".."))
|
||||||
|
|
||||||
|
|
||||||
|
def _read(*parts):
|
||||||
|
with open(os.path.join(_ROOT, *parts), encoding="utf-8") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def _without_comments(src):
|
||||||
|
"""The Nix source with `#` comment lines removed."""
|
||||||
|
return "\n".join(l for l in src.splitlines() if not l.lstrip().startswith("#"))
|
||||||
|
|
||||||
|
|
||||||
|
def _binding(src, name):
|
||||||
|
"""The right-hand side of a top-level `name = ...;` binding in a let/attrset."""
|
||||||
|
m = re.search(r"^\s*" + re.escape(name) + r"\s*=\s*(?P<v>.*?);\s*$", src, re.M | re.S)
|
||||||
|
assert m, f"{name} not found"
|
||||||
|
return m.group("v")
|
||||||
|
|
||||||
|
|
||||||
|
class HubIsNotACaddySite(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.caddy = _read("modules", "core", "caddy.nix")
|
||||||
|
cls.code = _without_comments(cls.caddy)
|
||||||
|
|
||||||
|
def test_there_is_no_site_for_the_hub(self):
|
||||||
|
self.assertNotRegex(self.code, r"sovransystemsos\.local")
|
||||||
|
self.assertNotIn("8937", self.code)
|
||||||
|
|
||||||
|
def test_the_public_ports_still_belong_to_the_public_sites(self):
|
||||||
|
# Caddy now also runs to bridge RTL and Mempool (even on Node Only),
|
||||||
|
# which must not open 80/443 by itself: those follow the domain-based
|
||||||
|
# services and nothing else.
|
||||||
|
self.assertRegex(
|
||||||
|
self.code,
|
||||||
|
r"networking\.firewall\.allowedTCPPorts\s*=\s*lib\.mkIf\s+needsHttpsPorts\s*\[\s*80\s+443\s*\]",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CaddyRunsWhereItIsNeeded(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.caddy = _read("modules", "core", "caddy.nix")
|
||||||
|
cls.code = _without_comments(cls.caddy)
|
||||||
|
|
||||||
|
def test_it_runs_for_domains_vhosts_or_the_bitcoin_uis(self):
|
||||||
|
self.assertRegex(
|
||||||
|
self.code,
|
||||||
|
r"caddyEnabled\s*=\s*needsHttpsPorts\s*\|\|\s*extraVhosts\s*!=\s*\"\"\s*\|\|\s*servesRtl\s*;",
|
||||||
|
)
|
||||||
|
self.assertRegex(self.code, r"enable\s*=\s*caddyEnabled\s*;")
|
||||||
|
|
||||||
|
def test_rtl_and_mempool_follow_their_services(self):
|
||||||
|
self.assertRegex(self.code,
|
||||||
|
r"servesRtl\s*=\s*config\.sovran_systemsOS\.services\.bitcoin\s*;")
|
||||||
|
self.assertRegex(
|
||||||
|
self.code,
|
||||||
|
r"servesMempool\s*=\s*servesRtl\s*&&\s*config\.sovran_systemsOS\.features\.mempool\s*;",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_each_site_exists_only_where_its_service_does(self):
|
||||||
|
self.assertRegex(self.code, r"lib\.optionalString\s+servesRtl\s*''\s*\n+:3051 \{")
|
||||||
|
self.assertRegex(self.code, r"lib\.optionalString\s+servesMempool\s*''\s*\n+:60847 \{")
|
||||||
|
# ... and they are written into the Caddyfile from that one place
|
||||||
|
self.assertIn("${bitcoinUiSites}", self.caddy)
|
||||||
|
|
||||||
|
def test_rtl_and_mempool_still_proxy_to_their_loopback_ports(self):
|
||||||
|
self.assertRegex(self.code, r":3051 \{[^}]*reverse_proxy :3050")
|
||||||
|
self.assertRegex(self.code, r":60847 \{[^}]*reverse_proxy :60845")
|
||||||
|
|
||||||
|
|
||||||
|
class HubPortExposure(unittest.TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.hub = _read("modules", "core", "sovran-hub.nix")
|
||||||
|
cls.roles = _read("modules", "core", "roles.nix")
|
||||||
|
|
||||||
|
def _firewall_value(self):
|
||||||
|
m = re.search(
|
||||||
|
r"^ networking\.firewall\.allowedTCPPorts =\s*(?P<value>.*?);\s*$",
|
||||||
|
self.hub, re.S | re.M,
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(m, "networking.firewall.allowedTCPPorts not found")
|
||||||
|
return m.group("value")
|
||||||
|
|
||||||
|
def test_the_hub_port_is_never_opened_unconditionally(self):
|
||||||
|
# Regression: this used to be `allowedTCPPorts = [ 8937 60847 ]` with
|
||||||
|
# no mkIf and no option gate, on every role, Desktop Only included.
|
||||||
|
value = self._firewall_value()
|
||||||
|
self.assertNotRegex(value, r"^\s*\[")
|
||||||
|
self.assertRegex(value, r"lib\.optionals\s+cfg\.hub\.directPort\s+\[\s*8937\s*\]")
|
||||||
|
|
||||||
|
def test_the_mempool_port_follows_mempool(self):
|
||||||
|
self.assertRegex(
|
||||||
|
self._firewall_value(),
|
||||||
|
r"lib\.optionals\s+\(cfg\.services\.bitcoin\s*&&\s*cfg\.features\.mempool\)\s+\[\s*60847\s*\]",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_nothing_else_is_opened_here(self):
|
||||||
|
ports = re.findall(r"\[\s*(\d+)\s*\]", self._firewall_value())
|
||||||
|
self.assertEqual(sorted(ports), ["60847", "8937"])
|
||||||
|
|
||||||
|
def test_direct_port_is_on_for_the_server_roles_and_off_for_desktop_only(self):
|
||||||
|
m = re.search(r"directPort\s*=\s*lib\.mkOption\s*\{(.*?)\n \};", self.roles, re.S)
|
||||||
|
self.assertIsNotNone(m, "hub.directPort option not found")
|
||||||
|
self.assertRegex(m.group(1), r"default\s*=\s*!config\.sovran_systemsOS\.roles\.desktop\s*;")
|
||||||
|
|
||||||
|
def test_the_bind_is_ipv4_only_on_purpose(self):
|
||||||
|
# IPv6 clients cannot reach the Hub, so the question of which IPv6
|
||||||
|
# addresses are "local" never comes up. Widening the bind reopens it.
|
||||||
|
self.assertIn('host="0.0.0.0"', self.hub)
|
||||||
|
self.assertNotRegex(self.hub, r'host="::"')
|
||||||
|
self.assertNotRegex(self.hub, r"both IPv4 and IPv6")
|
||||||
|
|
||||||
|
|
||||||
|
class TheHubIsDocumentedAtItsPort(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_no_document_still_sends_people_to_port_80(self):
|
||||||
|
for name in (("README.md",), ("SECURITY.md",),
|
||||||
|
("app", "sovran_systemsos_web", "templates", "index.html")):
|
||||||
|
with self.subTest(file=name[-1]):
|
||||||
|
text = _read(*name)
|
||||||
|
self.assertNotRegex(text, r"sovransystemsos\.local(?!:8937)(?![a-z])",
|
||||||
|
f"{name[-1]} sends people to sovransystemsos.local without :8937")
|
||||||
|
|
||||||
|
def test_the_documents_name_the_port(self):
|
||||||
|
self.assertIn("http://sovransystemsos.local:8937", _read("README.md"))
|
||||||
|
self.assertIn("http://sovransystemsos.local:8937", _read("SECURITY.md"))
|
||||||
|
self.assertIn("hub.directPort", _read("SECURITY.md"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user