diff --git a/README.md b/README.md index c70c7b2..069fa92 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,11 @@ interface in the firewall (`ufw`), so the box is reachable over your tailnet onl not the open internet. Non-interactive installs (e.g. piped `curl | bash`) default to local; force a choice with `OMARCHY_SEND_MODE=local` or `OMARCHY_SEND_MODE=remote`. +If you install in local mode but the box has a **public IP**, the installer detects +it and prints a warning with the exact commands to lock the port down — it never +changes your firewall without remote mode. See +[Public-IP boxes](#public-ip-boxes-firewall-the-port) below. + > The installer is a short shell script fetched over HTTPS; read it first if you > prefer — it lives at [`install.sh`](install.sh) in this repo. @@ -140,8 +145,47 @@ The receiver already listens on all interfaces, so it's reachable at its Tailsca IP with nothing else to configure. Sending and receiving both work, because the probe is a two-way handshake (each side learns the other). -> On a box with a public IP, don't leave `53317` open to the internet — install in -> **remote** mode (above) to firewall it to the tailnet, and/or set a `--pin`. +#### Public-IP boxes: firewall the port + +The receiver binds **all interfaces**, so on a box with a public IP, port `53317` +is reachable from the open internet while the TUI is running. Don't leave it that +way. Three ways to handle it: + +- **Easiest:** install in **remote** mode — `OMARCHY_SEND_MODE=remote bash install.sh` + — and the installer applies the `ufw` rules for you (when a real `tailscale0` + interface is present). +- **Manually**, restrict the port to the tailnet: + + ```sh + ufw allow in on tailscale0 to any port 53317 # tailnet only + ufw deny 53317 # everything else + ``` + + Or the `nftables` equivalent (inet filter, input chain): + + ``` + iifname "tailscale0" tcp dport 53317 accept + tcp dport 53317 drop + ``` + +- **Inside a container** (e.g. Docker `--network host` with userspace-networking + Tailscale, where there's no `tailscale0` and no `CAP_NET_ADMIN`): you can't + firewall from in there — apply it on the **host**. If the host already + default-denies inbound (only opens e.g. 22/80/443), `53317` is already blocked + from the internet yet still reachable over the tailnet (tailscaled delivers it + via loopback) — nothing more to do. + +Always set a **`--pin`** as a second layer regardless. + +> **Verifying** the port is closed: don't trust `nc -z`, `telnet`, or +> `/dev/tcp` — some hosting providers (Hostinger, DigitalOcean, …) answer the TCP +> handshake (SYN/ACK) for *every* port at their network edge, so those tools +> report a firewalled port as "open". Only an **app-layer** probe is truthful: +> +> ```sh +> curl -sk https://:53317/api/localsend/v2/info # should time out / hang +> curl -sk https://:53317/api/localsend/v2/info # returns device info +> ``` ### Right-click send (Nautilus) diff --git a/install.sh b/install.sh index 8b215cb..528555f 100755 --- a/install.sh +++ b/install.sh @@ -270,6 +270,13 @@ if [ -f /.dockerenv ] || grep -qaE 'docker|containerd|kubepods' /proc/1/cgroup 2 IN_CONTAINER=1 fi +# A routable public IPv4 means $PORT is reachable from the internet unless +# firewalled. Excludes loopback, link-local, RFC1918 and CGNAT/Tailscale +# (100.64.0.0/10). Empty when the box is purely on private/tailnet addresses. +PUBLIC_IP="$(ip -o -4 addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 \ + | grep -vE '^(10\.|127\.|169\.254\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.)' \ + | head -n1 || true)" + # ---- remote server: restrict the port to the Tailscale network ----------- # On a public-IP box, port 53317 would otherwise be reachable from the internet # (the receiver binds all interfaces). Lock it to the Tailscale interface so it @@ -323,6 +330,37 @@ if [ "$MODE" = "remote" ]; then echo " Tip: a PIN adds a second layer — run with --pin (or set it in Settings)." fi +# ---- local mode on a public-IP box: inform, don't touch the firewall ----- +# We never change the firewall outside remote mode, but a public IP means the +# port is internet-exposed while the TUI is open — so surface it with the exact +# commands to lock it down. (Covers the silent `curl | bash` default-to-local +# case, where the interactive remote prompt never ran.) +if [ "$MODE" != "remote" ] && [ -n "$PUBLIC_IP" ]; then + iface="${TS_IFACE:-tailscale0}" + echo + echo "⚠ Heads up: this machine has a public IP ($PUBLIC_IP) and was installed in" + echo " LOCAL mode, so port $PORT was NOT firewalled. The receiver binds all" + echo " interfaces, so $PORT is reachable from the internet while the TUI is open." + echo " The installer won't change your firewall without remote mode — lock it to" + echo " your tailnet yourself (recommended):" + if [ "$IN_CONTAINER" = "1" ]; then + echo " • You're in a container — apply on the HOST, not in here: ufw deny $PORT" + echo " (if the host already default-denies inbound, $PORT is already blocked" + echo " from the internet yet still reachable over the tailnet via loopback)." + elif command -v ufw >/dev/null 2>&1; then + echo " • ufw allow in on $iface to any port $PORT" + echo " • ufw deny $PORT" + else + echo " • nftables (inet filter, input chain):" + echo " iifname \"$iface\" tcp dport $PORT accept" + echo " tcp dport $PORT drop" + fi + echo " Or re-run to firewall it automatically: OMARCHY_SEND_MODE=remote bash install.sh" + echo " And/or set a PIN: omarchy-send --pin " + echo " Verify with an app-layer probe (raw TCP/nc lie behind some providers):" + echo " curl -sk https://:$PORT/api/localsend/v2/info # should time out" +fi + echo case ":$PATH:" in *":$BIN_DIR:"*) : ;;