Merge pull request #22 from naturallaw777/copilot/add-dynamic-port-status-detection
Add local-only dynamic port status detection and clearer port forwarding UX
This commit is contained in:
@@ -354,6 +354,142 @@ def _get_external_ip() -> str:
|
||||
return "unavailable"
|
||||
|
||||
|
||||
# ── Port status helpers (local-only, no external calls) ──────────
|
||||
|
||||
def _get_listening_ports() -> dict[str, set[int]]:
|
||||
"""Return sets of TCP and UDP ports that have services actively listening.
|
||||
|
||||
Uses ``ss -tlnp`` for TCP and ``ss -ulnp`` for UDP. Returns a dict with
|
||||
keys ``"tcp"`` and ``"udp"`` whose values are sets of integer port numbers.
|
||||
"""
|
||||
result: dict[str, set[int]] = {"tcp": set(), "udp": set()}
|
||||
for proto, flag in (("tcp", "-tlnp"), ("udp", "-ulnp")):
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["ss", flag],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
for line in proc.stdout.splitlines():
|
||||
# Column 4 is the local address:port (e.g. "0.0.0.0:443" or "[::]:443")
|
||||
parts = line.split()
|
||||
if len(parts) < 5:
|
||||
continue
|
||||
addr = parts[4]
|
||||
# strip IPv6 brackets and extract port after last ":"
|
||||
port_str = addr.rsplit(":", 1)[-1]
|
||||
try:
|
||||
result[proto].add(int(port_str))
|
||||
except ValueError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
def _get_firewall_allowed_ports() -> dict[str, set[int]]:
|
||||
"""Return sets of TCP and UDP ports that the firewall allows.
|
||||
|
||||
Tries ``nft list ruleset`` first (NixOS default), then falls back to
|
||||
``iptables -L -n``. Returns a dict with keys ``"tcp"`` and ``"udp"``.
|
||||
"""
|
||||
result: dict[str, set[int]] = {"tcp": set(), "udp": set()}
|
||||
|
||||
# ── nftables ─────────────────────────────────────────────────
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["nft", "list", "ruleset"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
text = proc.stdout
|
||||
# Match patterns like: tcp dport 443 accept or tcp dport { 80, 443 }
|
||||
for proto in ("tcp", "udp"):
|
||||
for m in re.finditer(
|
||||
rf'{proto}\s+dport\s+\{{?([^}};\n]+)\}}?', text
|
||||
):
|
||||
raw = m.group(1)
|
||||
for token in re.split(r'[\s,]+', raw):
|
||||
token = token.strip()
|
||||
if re.match(r'^\d+$', token):
|
||||
result[proto].add(int(token))
|
||||
elif re.match(r'^(\d+)-(\d+)$', token):
|
||||
lo, hi = token.split("-")
|
||||
result[proto].update(range(int(lo), int(hi) + 1))
|
||||
return result
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── iptables fallback ─────────────────────────────────────────
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
["iptables", "-L", "-n"],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
)
|
||||
if proc.returncode == 0:
|
||||
for line in proc.stdout.splitlines():
|
||||
# e.g. ACCEPT tcp -- ... dpt:443 or dpts:7882:7894
|
||||
m = re.search(r'(tcp|udp).*dpts?:(\d+)(?::(\d+))?', line)
|
||||
if m:
|
||||
proto_match = m.group(1)
|
||||
lo = int(m.group(2))
|
||||
hi = int(m.group(3)) if m.group(3) else lo
|
||||
result[proto_match].update(range(lo, hi + 1))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _port_range_to_ints(port_str: str) -> list[int]:
|
||||
"""Convert a port string like ``"443"``, ``"7882-7894"`` to a list of ints."""
|
||||
port_str = port_str.strip()
|
||||
if re.match(r'^\d+$', port_str):
|
||||
return [int(port_str)]
|
||||
m = re.match(r'^(\d+)-(\d+)$', port_str)
|
||||
if m:
|
||||
return list(range(int(m.group(1)), int(m.group(2)) + 1))
|
||||
return []
|
||||
|
||||
|
||||
def _check_port_status(
|
||||
port_str: str,
|
||||
protocol: str,
|
||||
listening: dict[str, set[int]],
|
||||
allowed: dict[str, set[int]],
|
||||
) -> str:
|
||||
"""Return ``"listening"``, ``"firewall_open"``, ``"closed"``, or ``"unknown"``."""
|
||||
protos = []
|
||||
p = protocol.upper()
|
||||
if "TCP" in p:
|
||||
protos.append("tcp")
|
||||
if "UDP" in p:
|
||||
protos.append("udp")
|
||||
if not protos:
|
||||
protos = ["tcp"]
|
||||
|
||||
ports = _port_range_to_ints(port_str)
|
||||
if not ports:
|
||||
return "unknown"
|
||||
|
||||
ports_set = set(ports)
|
||||
is_listening = any(
|
||||
pt in ports_set
|
||||
for proto_key in protos
|
||||
for pt in listening.get(proto_key, set())
|
||||
)
|
||||
is_allowed = any(
|
||||
pt in allowed.get(proto_key, set())
|
||||
for proto_key in protos
|
||||
for pt in ports_set
|
||||
)
|
||||
|
||||
if is_listening and is_allowed:
|
||||
return "listening"
|
||||
if is_allowed:
|
||||
return "firewall_open"
|
||||
return "closed"
|
||||
|
||||
|
||||
# ── QR code helper ────────────────────────────────────────────────
|
||||
|
||||
def _generate_qr_base64(data: str) -> str | None:
|
||||
@@ -796,6 +932,34 @@ async def api_network():
|
||||
return {"internal_ip": internal, "external_ip": external}
|
||||
|
||||
|
||||
class PortCheckRequest(BaseModel):
|
||||
ports: list[dict]
|
||||
|
||||
|
||||
@app.post("/api/ports/status")
|
||||
async def api_ports_status(req: PortCheckRequest):
|
||||
"""Check port status locally using ss and firewall rules — no external calls."""
|
||||
loop = asyncio.get_event_loop()
|
||||
internal_ip, listening, allowed = await asyncio.gather(
|
||||
loop.run_in_executor(None, _get_internal_ip),
|
||||
loop.run_in_executor(None, _get_listening_ports),
|
||||
loop.run_in_executor(None, _get_firewall_allowed_ports),
|
||||
)
|
||||
|
||||
port_results = []
|
||||
for p in req.ports:
|
||||
port_str = str(p.get("port", ""))
|
||||
protocol = str(p.get("protocol", "TCP"))
|
||||
status = _check_port_status(port_str, protocol, listening, allowed)
|
||||
port_results.append({
|
||||
"port": port_str,
|
||||
"protocol": protocol,
|
||||
"status": status,
|
||||
})
|
||||
|
||||
return {"internal_ip": internal_ip, "ports": port_results}
|
||||
|
||||
|
||||
@app.get("/api/updates/check")
|
||||
async def api_updates_check():
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
@@ -228,8 +228,7 @@ function buildTile(svc) {
|
||||
var ports = svc.port_requirements || [];
|
||||
var portsHtml = "";
|
||||
if (ports.length > 0) {
|
||||
var portLabels = ports.map(function(p) { return escHtml(p.port) + ' (' + escHtml(p.protocol) + ')'; });
|
||||
portsHtml = '<div class="tile-ports" title="Click to view required router ports"><span class="tile-ports-icon">🔌</span><span class="tile-ports-label">Ports: ' + portLabels.join(', ') + '</span></div>';
|
||||
portsHtml = '<div class="tile-ports" title="Click to view required router ports"><span class="tile-ports-icon">🔌</span><span class="tile-ports-label tile-ports-label--loading">Ports: ' + ports.length + ' required</span></div>';
|
||||
}
|
||||
|
||||
tile.innerHTML = infoBtn + '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div><div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>' + portsHtml;
|
||||
@@ -249,6 +248,40 @@ function buildTile(svc) {
|
||||
e.stopPropagation();
|
||||
openPortRequirementsModal(svc.name, ports, null);
|
||||
});
|
||||
|
||||
// Async: fetch port status and update badge summary
|
||||
if (ports.length > 0) {
|
||||
fetch("/api/ports/status", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ports: ports }),
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var listeningCount = 0;
|
||||
(data.ports || []).forEach(function(p) {
|
||||
if (p.status === "listening") listeningCount++;
|
||||
});
|
||||
var total = ports.length;
|
||||
var labelEl = portsEl.querySelector(".tile-ports-label");
|
||||
if (labelEl) {
|
||||
labelEl.classList.remove("tile-ports-label--loading");
|
||||
if (listeningCount === total) {
|
||||
labelEl.className = "tile-ports-label tile-ports-all-ready";
|
||||
labelEl.textContent = "Ports: " + total + "/" + total + " ready ✓";
|
||||
} else if (listeningCount > 0) {
|
||||
labelEl.className = "tile-ports-label tile-ports-partial";
|
||||
labelEl.textContent = "Ports: " + listeningCount + "/" + total + " ready";
|
||||
} else {
|
||||
labelEl.className = "tile-ports-label tile-ports-none-ready";
|
||||
labelEl.textContent = "Ports: " + total + " required";
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
// Leave badge as-is on error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tile;
|
||||
@@ -912,43 +945,119 @@ function closeDomainSetupModal() {
|
||||
function openPortRequirementsModal(featureName, ports, onContinue) {
|
||||
if (!$portReqModal || !$portReqBody) return;
|
||||
|
||||
var rows = ports.map(function(p) {
|
||||
return '<tr><td class="port-req-port">' + escHtml(p.port) + '</td>' +
|
||||
'<td class="port-req-proto">' + escHtml(p.protocol) + '</td>' +
|
||||
'<td class="port-req-desc">' + escHtml(p.description) + '</td></tr>';
|
||||
}).join("");
|
||||
|
||||
var continueBtn = onContinue
|
||||
? '<button class="btn btn-primary" id="port-req-continue-btn">I Understand — Continue</button>'
|
||||
: '';
|
||||
|
||||
// Show loading state while fetching port status
|
||||
$portReqBody.innerHTML =
|
||||
'<p class="port-req-intro">You have enabled <strong>' + escHtml(featureName) + '</strong>. ' +
|
||||
'For it to work with clients outside your local network you must open the following ports ' +
|
||||
'on your <strong>home router / WAN firewall</strong>:</p>' +
|
||||
'<table class="port-req-table">' +
|
||||
'<thead><tr><th>Port(s)</th><th>Protocol</th><th>Purpose</th></tr></thead>' +
|
||||
'<tbody>' + rows + '</tbody>' +
|
||||
'</table>' +
|
||||
'<p class="port-req-hint">ℹ Consult your router manual or search "<em>how to open ports on [router model]</em>" ' +
|
||||
'for instructions. Features like Element Video Calling will not work for remote users until these ports are open.</p>' +
|
||||
'<div class="domain-field-actions">' +
|
||||
'<button class="btn btn-close-modal" id="port-req-dismiss-btn">Dismiss</button>' +
|
||||
continueBtn +
|
||||
'</div>';
|
||||
|
||||
document.getElementById("port-req-dismiss-btn").addEventListener("click", function() {
|
||||
closePortRequirementsModal();
|
||||
});
|
||||
|
||||
if (onContinue) {
|
||||
document.getElementById("port-req-continue-btn").addEventListener("click", function() {
|
||||
closePortRequirementsModal();
|
||||
onContinue();
|
||||
});
|
||||
}
|
||||
'<p class="port-req-intro">Checking port status for <strong>' + escHtml(featureName) + '</strong>…</p>' +
|
||||
'<p class="port-req-hint">Detecting which ports are open on this machine…</p>';
|
||||
|
||||
$portReqModal.classList.add("open");
|
||||
|
||||
// Fetch live port status from local system commands (no external calls)
|
||||
fetch("/api/ports/status", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ports: ports }),
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
var internalIp = (data.internal_ip && data.internal_ip !== "unavailable")
|
||||
? data.internal_ip : null;
|
||||
var portStatuses = {};
|
||||
(data.ports || []).forEach(function(p) {
|
||||
portStatuses[p.port + "/" + p.protocol] = p.status;
|
||||
});
|
||||
|
||||
var rows = ports.map(function(p) {
|
||||
var key = p.port + "/" + p.protocol;
|
||||
var status = portStatuses[key] || "unknown";
|
||||
var statusHtml;
|
||||
if (status === "listening") {
|
||||
statusHtml = '<span class="port-status-listening" title="Service is running and firewall allows this port">🟢 Listening</span>';
|
||||
} else if (status === "firewall_open") {
|
||||
statusHtml = '<span class="port-status-open" title="Firewall allows this port but no service is bound yet">🟡 Open (idle)</span>';
|
||||
} else if (status === "closed") {
|
||||
statusHtml = '<span class="port-status-closed" title="Firewall blocks this port and/or nothing is listening">🔴 Closed</span>';
|
||||
} else {
|
||||
statusHtml = '<span class="port-status-unknown" title="Status could not be determined">⚪ Unknown</span>';
|
||||
}
|
||||
return '<tr>' +
|
||||
'<td class="port-req-port">' + escHtml(p.port) + '</td>' +
|
||||
'<td class="port-req-proto">' + escHtml(p.protocol) + '</td>' +
|
||||
'<td class="port-req-desc">' + escHtml(p.description) + '</td>' +
|
||||
'<td class="port-req-status">' + statusHtml + '</td>' +
|
||||
'</tr>';
|
||||
}).join("");
|
||||
|
||||
var ipLine = internalIp
|
||||
? '<p class="port-req-intro">Forward each port below <strong>to this machine\'s internal IP: <code class="port-req-internal-ip">' + escHtml(internalIp) + '</code></strong></p>'
|
||||
: "<p class=\"port-req-intro\">Forward each port below to this machine's internal LAN IP in your router's port forwarding settings.</p>";
|
||||
|
||||
$portReqBody.innerHTML =
|
||||
'<p class="port-req-intro"><strong>Port Forwarding Required</strong></p>' +
|
||||
'<p class="port-req-intro">For <strong>' + escHtml(featureName) + "</strong> to work with clients outside your local network, " +
|
||||
"you must configure <strong>port forwarding</strong> in your router's admin panel.</p>" +
|
||||
ipLine +
|
||||
'<table class="port-req-table">' +
|
||||
'<thead><tr><th>Port(s)</th><th>Protocol</th><th>Purpose</th><th>Status</th></tr></thead>' +
|
||||
'<tbody>' + rows + '</tbody>' +
|
||||
'</table>' +
|
||||
"<p class=\"port-req-hint\"><strong>How to verify:</strong> Router-side forwarding cannot be checked from inside your network. " +
|
||||
"To confirm ports are forwarded correctly, test from a device on a different network (e.g. a phone on mobile data) " +
|
||||
"or check your router's port forwarding page.</p>" +
|
||||
'<p class="port-req-hint">ℹ Search "<em>how to set up port forwarding on [your router model]</em>" for step-by-step instructions.</p>' +
|
||||
'<div class="domain-field-actions">' +
|
||||
'<button class="btn btn-close-modal" id="port-req-dismiss-btn">Dismiss</button>' +
|
||||
continueBtn +
|
||||
'</div>';
|
||||
|
||||
document.getElementById("port-req-dismiss-btn").addEventListener("click", function() {
|
||||
closePortRequirementsModal();
|
||||
});
|
||||
|
||||
if (onContinue) {
|
||||
document.getElementById("port-req-continue-btn").addEventListener("click", function() {
|
||||
closePortRequirementsModal();
|
||||
onContinue();
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
// Fallback: show static table without status column if fetch fails
|
||||
var rows = ports.map(function(p) {
|
||||
return '<tr><td class="port-req-port">' + escHtml(p.port) + '</td>' +
|
||||
'<td class="port-req-proto">' + escHtml(p.protocol) + '</td>' +
|
||||
'<td class="port-req-desc">' + escHtml(p.description) + '</td></tr>';
|
||||
}).join("");
|
||||
|
||||
$portReqBody.innerHTML =
|
||||
'<p class="port-req-intro"><strong>Port Forwarding Required</strong></p>' +
|
||||
'<p class="port-req-intro">For <strong>' + escHtml(featureName) + '</strong> to work with clients outside your local network, ' +
|
||||
'you must configure <strong>port forwarding</strong> in your router\'s admin panel and forward each port below to this machine\'s internal LAN IP.</p>' +
|
||||
'<table class="port-req-table">' +
|
||||
'<thead><tr><th>Port(s)</th><th>Protocol</th><th>Purpose</th></tr></thead>' +
|
||||
'<tbody>' + rows + '</tbody>' +
|
||||
'</table>' +
|
||||
'<p class="port-req-hint">ℹ Search "<em>how to set up port forwarding on [your router model]</em>" for step-by-step instructions.</p>' +
|
||||
'<div class="domain-field-actions">' +
|
||||
'<button class="btn btn-close-modal" id="port-req-dismiss-btn">Dismiss</button>' +
|
||||
continueBtn +
|
||||
'</div>';
|
||||
|
||||
document.getElementById("port-req-dismiss-btn").addEventListener("click", function() {
|
||||
closePortRequirementsModal();
|
||||
});
|
||||
|
||||
if (onContinue) {
|
||||
document.getElementById("port-req-continue-btn").addEventListener("click", function() {
|
||||
closePortRequirementsModal();
|
||||
onContinue();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closePortRequirementsModal() {
|
||||
|
||||
@@ -1390,3 +1390,57 @@ button.btn-reboot:hover:not(:disabled) {
|
||||
line-height: 1.5;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
/* ── Port status indicators ─────────────────────────────────────── */
|
||||
|
||||
.port-req-status {
|
||||
padding: 5px 10px;
|
||||
white-space: nowrap;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.port-status-listening {
|
||||
color: #a6e3a1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.port-status-open {
|
||||
color: #f9e2af;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.port-status-closed {
|
||||
color: #f38ba8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.port-status-unknown {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* Internal IP highlight in port modal */
|
||||
.port-req-internal-ip {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
background: rgba(137, 180, 250, 0.15);
|
||||
color: var(--accent-color);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
/* Tile port badge status colours */
|
||||
.tile-ports-all-ready {
|
||||
color: #a6e3a1;
|
||||
}
|
||||
|
||||
.tile-ports-partial {
|
||||
color: #f9e2af;
|
||||
}
|
||||
|
||||
.tile-ports-none-ready {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.tile-ports-label--loading {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
@@ -349,10 +349,22 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
"""Inform the user about required router/firewall ports before install."""
|
||||
outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||
|
||||
# Detect internal IP at install time
|
||||
internal_ip = "this machine's LAN IP"
|
||||
try:
|
||||
import subprocess as _sp
|
||||
_r = _sp.run(["hostname", "-I"], capture_output=True, text=True, timeout=5)
|
||||
if _r.returncode == 0:
|
||||
_parts = _r.stdout.strip().split()
|
||||
if _parts:
|
||||
internal_ip = _parts[0]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Warning banner
|
||||
banner = Adw.Banner()
|
||||
banner.set_title(
|
||||
"⚠ You must open these ports on your home router / WAN firewall"
|
||||
"⚠ Port Forwarding Setup Required — configure your router before install"
|
||||
)
|
||||
banner.set_revealed(True)
|
||||
banner.set_margin_top(16)
|
||||
@@ -363,10 +375,12 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
intro = Gtk.Label()
|
||||
intro.set_markup(
|
||||
"<span foreground='#a6adc8'>"
|
||||
"Many Sovran_SystemsOS features require specific ports to be forwarded "
|
||||
"through your router for remote access to work correctly. "
|
||||
"Many Sovran_SystemsOS features require <b>port forwarding</b> to be configured "
|
||||
"in your router's admin panel. This means telling your router to forward "
|
||||
"specific ports to <b>this machine's internal LAN IP</b>.\n\n"
|
||||
"Services like Element Video/Audio Calling and Matrix Federation "
|
||||
"<b>will not work for clients outside your LAN</b> unless these ports are open."
|
||||
"<b>will not work for clients outside your LAN</b> unless these ports are "
|
||||
"forwarded to this machine."
|
||||
"</span>"
|
||||
)
|
||||
intro.set_wrap(True)
|
||||
@@ -376,6 +390,17 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
intro.set_margin_end(40)
|
||||
outer.append(intro)
|
||||
|
||||
ip_label = Gtk.Label()
|
||||
ip_label.set_markup(
|
||||
f"<span foreground='#89b4fa' font_desc='monospace'>"
|
||||
f" Forward ports to this machine's internal IP: <b>{internal_ip}</b>"
|
||||
f"</span>"
|
||||
)
|
||||
ip_label.set_margin_top(10)
|
||||
ip_label.set_margin_start(40)
|
||||
ip_label.set_margin_end(40)
|
||||
outer.append(ip_label)
|
||||
|
||||
sw = Gtk.ScrolledWindow()
|
||||
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
sw.set_vexpand(True)
|
||||
@@ -425,8 +450,13 @@ class InstallerWindow(Adw.ApplicationWindow):
|
||||
note = Gtk.Label()
|
||||
note.set_markup(
|
||||
"<span foreground='#6c7086' size='small'>"
|
||||
"ℹ Search \"<i>how to open ports on [your router model]</i>\" for step-by-step instructions. "
|
||||
"Most home routers have a \"Port Forwarding\" section in their admin panel."
|
||||
"ℹ In your router's admin panel (usually at 192.168.1.1), find the "
|
||||
"\"<b>Port Forwarding</b>\" section and add a rule for each port above with "
|
||||
"the destination set to <b>this machine's internal IP</b>. "
|
||||
"These ports only need to be forwarded to this specific machine — "
|
||||
"this does <b>NOT</b> expose your entire network.\n"
|
||||
"To verify forwarding is working, test from a device on a different network "
|
||||
"(e.g. a phone on mobile data) or check your router's port forwarding page."
|
||||
"</span>"
|
||||
)
|
||||
note.set_wrap(True)
|
||||
|
||||
@@ -251,6 +251,48 @@ $PENDING_NJALLA"
|
||||
echo " DDNS script: /var/lib/njalla/njalla.sh"
|
||||
echo " DDNS cron: Every 15 minutes (already configured)"
|
||||
echo ""
|
||||
|
||||
# ── Port Forwarding Reminder ──────────────────────
|
||||
INTERNAL_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
printf "%b%s%b\n" "$YELLOW" "══════════════════════════════════════════════" "$NC"
|
||||
printf "%b ⚠ Port Forwarding Reminder%b\n" "$YELLOW" "$NC"
|
||||
printf "%b%s%b\n" "$YELLOW" "══════════════════════════════════════════════" "$NC"
|
||||
echo ""
|
||||
echo " For your services to be reachable from the internet, you must"
|
||||
echo " set up PORT FORWARDING in your router's admin panel."
|
||||
echo ""
|
||||
if [ -n "$INTERNAL_IP" ]; then
|
||||
printf " Forward these ports to this machine's internal IP: %b%s%b\n" "$CYAN" "$INTERNAL_IP" "$NC"
|
||||
else
|
||||
echo " Forward these ports to this machine's internal LAN IP."
|
||||
fi
|
||||
echo ""
|
||||
echo " 80/TCP — HTTP (redirects to HTTPS)"
|
||||
echo " 443/TCP — HTTPS (all domain-based services)"
|
||||
echo " 8448/TCP — Matrix federation (server-to-server)"
|
||||
echo ""
|
||||
echo " If you enabled Element Calling, also forward:"
|
||||
echo " 7881/TCP — LiveKit WebRTC signalling"
|
||||
echo " 7882-7894/UDP — LiveKit media streams"
|
||||
echo " 5349/TCP — TURN over TLS"
|
||||
echo " 3478/UDP — TURN (STUN/relay)"
|
||||
echo " 30000-40000/TCP+UDP — TURN relay"
|
||||
echo ""
|
||||
echo " How: Log into your router (usually 192.168.1.1), find the"
|
||||
echo " \"Port Forwarding\" section, and add rules for each port above"
|
||||
if [ -n "$INTERNAL_IP" ]; then
|
||||
printf " with the destination set to %b%s%b.\n" "$CYAN" "$INTERNAL_IP" "$NC"
|
||||
else
|
||||
echo " with the destination set to this machine's IP."
|
||||
fi
|
||||
echo ""
|
||||
echo " These ports only need to be forwarded to this specific machine —"
|
||||
echo " this does NOT expose your entire network."
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$YELLOW" "══════════════════════════════════════════════" "$NC"
|
||||
echo ""
|
||||
read -p "Press Enter to continue with the rebuild..."
|
||||
|
||||
printf "%b%s%b\n" "$YELLOW" " Rebuilding to activate services with new domains..." "$NC"
|
||||
echo ""
|
||||
nixos-rebuild switch --flake /etc/nixos#nixos
|
||||
@@ -335,6 +377,48 @@ $PENDING_NJALLA"
|
||||
echo " All configured domains:"
|
||||
${domainSummary}
|
||||
echo ""
|
||||
|
||||
# ── Port Forwarding Reminder ──────────────────────
|
||||
INTERNAL_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
printf "%b%s%b\n" "$YELLOW" "══════════════════════════════════════════════" "$NC"
|
||||
printf "%b ⚠ Port Forwarding Reminder%b\n" "$YELLOW" "$NC"
|
||||
printf "%b%s%b\n" "$YELLOW" "══════════════════════════════════════════════" "$NC"
|
||||
echo ""
|
||||
echo " For your services to be reachable from the internet, you must"
|
||||
echo " set up PORT FORWARDING in your router's admin panel."
|
||||
echo ""
|
||||
if [ -n "$INTERNAL_IP" ]; then
|
||||
printf " Forward these ports to this machine's internal IP: %b%s%b\n" "$CYAN" "$INTERNAL_IP" "$NC"
|
||||
else
|
||||
echo " Forward these ports to this machine's internal LAN IP."
|
||||
fi
|
||||
echo ""
|
||||
echo " 80/TCP — HTTP (redirects to HTTPS)"
|
||||
echo " 443/TCP — HTTPS (all domain-based services)"
|
||||
echo " 8448/TCP — Matrix federation (server-to-server)"
|
||||
echo ""
|
||||
echo " If you enabled Element Calling, also forward:"
|
||||
echo " 7881/TCP — LiveKit WebRTC signalling"
|
||||
echo " 7882-7894/UDP — LiveKit media streams"
|
||||
echo " 5349/TCP — TURN over TLS"
|
||||
echo " 3478/UDP — TURN (STUN/relay)"
|
||||
echo " 30000-40000/TCP+UDP — TURN relay"
|
||||
echo ""
|
||||
echo " How: Log into your router (usually 192.168.1.1), find the"
|
||||
echo " \"Port Forwarding\" section, and add rules for each port above"
|
||||
if [ -n "$INTERNAL_IP" ]; then
|
||||
printf " with the destination set to %b%s%b.\n" "$CYAN" "$INTERNAL_IP" "$NC"
|
||||
else
|
||||
echo " with the destination set to this machine's IP."
|
||||
fi
|
||||
echo ""
|
||||
echo " These ports only need to be forwarded to this specific machine —"
|
||||
echo " this does NOT expose your entire network."
|
||||
echo ""
|
||||
printf "%b%s%b\n" "$YELLOW" "══════════════════════════════════════════════" "$NC"
|
||||
echo ""
|
||||
read -p "Press Enter to continue with the rebuild..."
|
||||
|
||||
printf "%b%s%b\n" "$YELLOW" " Rebuilding to activate services with new domains..." "$NC"
|
||||
echo ""
|
||||
nixos-rebuild switch --impure
|
||||
|
||||
Reference in New Issue
Block a user