Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions web-admin/src/components/Admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { api, changes, GIB, provisioningSite, trafficCorrection, upload, type Node, type PingTask } from "@/lib/api"
import { addresses, api, changes, GIB, provisioningSite, trafficCorrection, upload, type Node, type PingTask } from "@/lib/api"
import { bytes, CYCLES, FOREVER, money, monthUsage, uptime } from "@/lib/format"

// Counters the panel can correct after migration or an accounting error.
Expand Down Expand Up @@ -46,10 +46,7 @@ function copy(text: string) {
// Every address a node has, each click-to-copy: pasting one into an ssh command
// is why they are shown.
function Addresses({ node }: { node: Node }) {
const reported = [node.ipv4, node.ipv6].filter(Boolean) as string[]
// `ip` is merely where the agent's connection originated: the fallback for an
// agent too old to report its own interfaces.
const list = reported.length ? reported : ([node.ip].filter(Boolean) as string[])
const list = addresses(node)
if (!list.length) return <span className="text-sm text-muted-foreground">—</span>
return (
<div className="flex flex-col items-start gap-y-0.5">
Expand Down
17 changes: 15 additions & 2 deletions web-admin/src/lib/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="node" />
import assert from "node:assert/strict"
import { changes, GIB, provisioningSite, trafficCorrection } from "./api.ts"
import { addresses, changes, GIB, provisioningSite, trafficCorrection } from "./api.ts"

assert.deepEqual(changes({ public: true, price: 5 }, { price: 20 }), { price: 20 })
assert.deepEqual(changes({ total_rx: "100", month_tx: "2" }, { total_rx: "100", month_tx: "3" }), { month_tx: "3" })
Expand All @@ -17,4 +17,17 @@ assert.deepEqual(trafficCorrection(shown, { ...shown, total_rx: " " }), {})
assert.deepEqual(trafficCorrection(shown, { ...shown, total_rx: "0" }), { total_rx: 0 })
assert.deepEqual(trafficCorrection(shown, { ...shown, total_tx: "3" }), { total_tx: 3 * GIB })
assert.deepEqual(trafficCorrection(shown, shown), {})
console.log("partial edits, traffic corrections and provisioning checks passed")
// NAT: the public address the connection arrived from leads the private interface.
assert.deepEqual(addresses({ ip: "203.0.113.7", ipv4: "10.10.2.250", ipv6: "2001:db8::1" }), ["203.0.113.7", "10.10.2.250", "2001:db8::1"])
assert.deepEqual(addresses({ ip: "203.0.113.7", ipv4: "100.64.0.9" }), ["203.0.113.7", "100.64.0.9"])
// Hub on the same network, or on the same machine: the connection says nothing more.
assert.deepEqual(addresses({ ip: "192.168.1.2", ipv4: "192.168.1.5" }), ["192.168.1.5"])
assert.deepEqual(addresses({ ip: "127.0.0.1", ipv4: "172.16.0.5" }), ["172.16.0.5"])
// A public interface, or a connection over IPv6, stays as reported.
assert.deepEqual(addresses({ ip: "198.51.100.1", ipv4: "203.0.113.7" }), ["203.0.113.7"])
assert.deepEqual(addresses({ ip: "2001:db8::2", ipv4: "10.0.0.2", ipv6: "2001:db8::2" }), ["10.0.0.2", "2001:db8::2"])
assert.deepEqual(addresses({ ip: "203.0.113.7" }), ["203.0.113.7"])
// Without a recorded connection the list holds only what the agent reported.
assert.deepEqual(addresses({ ipv4: "10.0.0.2" }), ["10.0.0.2"])
assert.deepEqual(addresses({}), [])
console.log("partial edits, traffic corrections, provisioning and address checks passed")
22 changes: 22 additions & 0 deletions web-admin/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,28 @@ export function trafficCorrection(
)
}

/** Private, carrier-grade NAT, loopback or link-local: unreachable from outside the machine's own network. */
function isLocalV4(ip: string): boolean {
const [a, b] = ip.split(".").map(Number)
return a === 10 || a === 127 || (a === 172 && b >= 16 && b < 32) || (a === 192 && b === 168) ||
(a === 100 && b >= 64 && b < 128) || (a === 169 && b === 254)
}

/**
* The addresses shown for a node. The agent reports its interfaces; `ip` is
* where its connection arrived from, which the hub canonicalizes to dotted form
* for IPv4. Behind NAT the interface holds only a private IPv4 while the
* connection arrives from the public one, so that address leads. `ip` alone is
* also the fallback for an agent too old to report its interfaces.
*/
export function addresses(node: Pick<Node, "ip" | "ipv4" | "ipv6">): string[] {
const reported = [node.ipv4, node.ipv6].filter(Boolean) as string[]
const { ip } = node
if (!ip) return reported
if (node.ipv4 && isLocalV4(node.ipv4) && ip.includes(".") && !isLocalV4(ip)) return [ip, ...reported]
return reported.length ? reported : [ip]
}

/** Installation commands require a TLS origin with a domain, never an IP. */
export function provisioningSite(site: string): string {
try {
Expand Down