diff --git a/README.md b/README.md
index 1e206a1..57158d2 100644
--- a/README.md
+++ b/README.md
@@ -202,7 +202,7 @@ Bitcoin and self-hosting infrastructure runs on the machine.
|---|---|---|
| **Desktop** | Everyday users and computers with modest hardware | Sparrow, Bisq, and Bisq 2 for self-custody and peer-to-peer Bitcoin use |
| **Node** | People ready to verify and operate their own Bitcoin infrastructure | Everything in Desktop, plus the full Bitcoin stack: Bitcoin Core, Electrs, LND, Ride The Lightning, BTCPay Server, and wallet-to-node connections |
-| **Server + Desktop** | Bitcoiners who want the same sovereignty over their communications, cloud, passwords, and web services | The complete Node stack, plus the private self-hosted services |
+| **Server + Desktop** | Bitcoiners who want the same sovereignty over their communications, cloud, passwords, and web services | The complete Node stack, plus the private self-hosted services. **Makes your home IP address public:** [read this first](#server--desktop-and-your-home-ip-address) |
**Desktop: start with your keys.** Desktop is not a reduced or Bitcoin-free
edition. It is a complete, private everyday computer with a clean GNOME
@@ -237,6 +237,66 @@ communications, identity, and services.
> provider allows port forwarding. Most home routers and providers already
> support this. If you are unsure, a quick search for your router model and
> "port forwarding" will usually turn up a step-by-step guide.
+>
+> **This mode also makes your home IP address public.** Read
+> [what that means](#server--desktop-and-your-home-ip-address) before you
+> choose it.
+
+### Server + Desktop and your home IP address
+
+> **⚠️ Server + Desktop makes your home IP address public.**
+> Public services need a domain name that points at your home internet
+> connection. When you finish the guided domain setup, Sovran_SystemsOS puts
+> your home's public IP address in a Dynamic DNS record at
+> [Njal.la](https://njal.la) and keeps it up to date, and you forward ports 80
+> and 443 on your router to this computer. From then on:
+>
+> - **Anyone can look up your domain and see your home IP address.** An IP
+> address typically reveals your internet provider and your approximate
+> location, and it ties everything you publish on that domain to your home
+> connection.
+> - **Domain privacy does not hide it.** Registrar privacy protects the
+> registrant's identity, not the IP address in your DNS records.
+> - **Your connection is open to the whole internet on those ports.** Scanners
+> and bots constantly probe public IP addresses, so expect automated probing
+> and login attempts against every service you publish.
+> - **Your service names are discoverable.** Public HTTPS certificates are
+> listed in public Certificate Transparency logs, so hostnames such as
+> `vault.yourdomain.com` can be found, and then resolved to your IP address,
+> even if you never share them.
+
+Nothing is published until you finish domain setup and port forwarding, but that
+setup is the point of this mode, so assume your IP address will be public.
+**Desktop** publishes nothing. **Node** publishes nothing unless you turn on a
+feature that needs a domain: *Put BTCPay Server Online* or *Lightning Wallet
+Connections*.
+
+**If you do not want your home IP address to be public,** choose Desktop or
+Node. Advanced users can put a VPS, reverse proxy, or tunnel in front of their
+services so DNS points there instead of at their home. Sovran_SystemsOS does not
+set this up for you, and the Hub's domain checks currently expect DNS to point
+at your home IP address.
+
+What happens technically
+
+- You create a **Dynamic** DNS record at Njal.la and paste its update command
+ into the Hub. The Hub only accepts `njal.la` update URLs.
+- The `sovran-ddns-update` timer asks Njal.la to point your record at the
+ address the request came from. It does this right after you save a domain,
+ two minutes after boot, and then every 15 minutes.
+- Njal.la reports that address back, and Sovran_SystemsOS keeps it for Element
+ calling and the Hub. Nothing else looks up your public IP address: no STUN
+ server, public DNS resolver, or "what is my IP" service is involved. See
+ `modules/core/njalla.nix`.
+- Once a service that needs a domain is turned on, the firewall opens TCP and
+ UDP ports 80 and 443 for Caddy, which requests public HTTPS certificates for
+ the domains you configure. See `modules/core/caddy.nix`.
+- Optional features can need more ports. Element calling, for example, needs
+ TCP 7881 and UDP 3478, 7882, and 40000–40099. The Hub lists the ports each
+ feature needs.
+
+
⚠️ Heads-up: your domain points at your home internet connection, " + + "so anyone can look up your home IP address. Domain privacy does not hide it.
"; return html; } diff --git a/app/sovran_systemsos_web/templates/index.html b/app/sovran_systemsos_web/templates/index.html index 4aa6e24..45468d5 100644 --- a/app/sovran_systemsos_web/templates/index.html +++ b/app/sovran_systemsos_web/templates/index.html @@ -476,6 +476,10 @@+ ⚠️ Heads-up: your domain points at your home internet connection, + so anyone can look up your home IP address. Domain privacy does not hide it. +
The Hub guides you through every step.
diff --git a/iso/installer.py b/iso/installer.py index 173a6f8..adb5f79 100644 --- a/iso/installer.py +++ b/iso/installer.py @@ -471,7 +471,7 @@ class InstallerWindow(Adw.ApplicationWindow): # Role cards roles = [ ("Server + Desktop", - "Full sovereignty: host your own websites, cloud, chat, passwords, and Bitcoin services instead of relying on Big Tech. Sovran_SystemsOS walks you through getting your domain from Njal.la and connecting everything. One router task is required: forward ports 80 and 443 to this computer.", + "Full sovereignty: host your own websites, cloud, chat, passwords, and Bitcoin services instead of relying on Big Tech. Sovran_SystemsOS walks you through getting your domain from Njal.la and connecting everything. One router task is required: forward ports 80 and 443 to this computer. Heads-up: this makes your home IP address public.", "Server+Desktop"), ("Desktop Only", "A beautiful, easy-to-use desktop without the background server applications.", diff --git a/tests/test_exposure_guards.py b/tests/test_exposure_guards.py new file mode 100644 index 0000000..9aa4782 --- /dev/null +++ b/tests/test_exposure_guards.py @@ -0,0 +1,68 @@ +"""Guards for the home-IP warnings. + +Server + Desktop publishes the home IP address (the domain points at it), so every +place that offers Server + Desktop must say so, and the README section they point +at must exist. + +These read the shipped source files like the nix-file checks in test_security.py: +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__), "..")) +_PHRASE = "home ip address" + + +def _read(*parts): + with open(os.path.join(_ROOT, *parts), encoding="utf-8") as f: + return f.read() + + +def _github_slug(heading): + slug = re.sub(r"[^\w\- ]", "", heading.strip().lower()) + return slug.replace(" ", "-") + + +class HomeIpWarnings(unittest.TestCase): + + def test_installer_role_card(self): + src = _read("iso", "installer.py") + card = re.search(r'\("Server \+ Desktop",\s*"((?:[^"\\]|\\.)*)"', src, re.S) + self.assertIsNotNone(card, "Server + Desktop role card not found") + self.assertIn(_PHRASE, card.group(1).lower()) + + def test_hub_domain_setup_text(self): + # domain-prereqs.js is the single source for onboarding, feature setup + # and domain reconfiguration; the notice must follow every variant. + js = _read("app", "sovran_systemsos_web", "static", "js", "domain-prereqs.js") + body = js[js.index("function renderDomainNeedsHtml"):] + body = body[:body.index("\n}\n")] + self.assertIn(_PHRASE, body.lower()) + self.assertGreater(body.lower().index(_PHRASE), body.rindex("} else {"), + "the notice must come after the last variant, not inside one") + + def test_hub_upgrade_dialog(self): + html = _read("app", "sovran_systemsos_web", "templates", "index.html") + dialog = html[html.index('id="upgrade-modal"'):html.index("Security Reset overlay")] + self.assertIn(_PHRASE, " ".join(dialog.lower().split())) + + def test_readme_and_security_policy(self): + self.assertIn(_PHRASE, _read("README.md").lower()) + self.assertIn(_PHRASE, " ".join(_read("SECURITY.md").lower().split())) + + def test_links_to_the_readme_section_resolve(self): + readme = _read("README.md") + slugs = {_github_slug(m.group(2)) + for m in re.finditer(r"^(#{1,6})\s+(.+?)\s*$", readme, re.M)} + links = re.findall(r"\]\(#(server--desktop[^)]*)\)", readme) + links += re.findall(r"README\.md#(server--desktop[^)\s]*)", _read("SECURITY.md")) + self.assertTrue(links, "expected links to the home-IP section") + for anchor in links: + self.assertIn(anchor, slugs) + + +if __name__ == "__main__": + unittest.main()