import type { WireGuardServer } from './api.ts' /** * The WireGuard configuration of a device: its address, a placeholder for its private key, which the router never has, * the router's resolver as its DNS server while it runs, and the router as its peer, with the router's public key, * endpoint and allowed IPs. */ export function clientConfig(server: WireGuardServer, address: string): string { return [ '[Interface]', 'PrivateKey = ', `Address = ${address}`, ...(server.dns.length ? [`DNS = ${server.dns.join(', ')}`] : []), '', '[Peer]', `PublicKey = ${server.public_key}`, `Endpoint = ${server.endpoint}`, `AllowedIPs = ${server.allowed_ips.join(', ')}`, 'PersistentKeepalive = 25', '', ].join('\n') } /** An IPv4 address as a number, or null when the text is not one. */ function addressOf(text: string): number | null { const parts = text.split('.') if (parts.length !== 4 || !parts.every((part) => /^(0|[1-9]\d{0,2})$/.test(part) && Number(part) <= 255)) { return null } return parts.reduce((value, part) => value * 256 + Number(part), 0) } /** A number as an IPv4 address. */ function textOf(value: number): string { return [24, 16, 8, 0].map((shift) => Math.floor(value / 2 ** shift) % 256).join('.') } /** An IPv4 address or network, a.b.c.d or a.b.c.d/n, as its first address and size, or null when it is not one. */ function networkOf(text: string, strict: boolean): { first: number; size: number } | null { const [address, prefix, ...rest] = text.trim().split('/') const value = addressOf(address) const length = prefix === undefined ? 32 : /^\d{1,2}$/.test(prefix) ? Number(prefix) : NaN if (value === null || rest.length > 0 || !(length >= 0 && length <= 32)) { return null } const size = 2 ** (32 - length) const first = value - (value % size) return strict && first !== value ? null : { first, size } } /** * Why the router's own WireGuard address with its prefix, wireguard.address, is refused, or '' when it is an address of * its network other than the network's and the broadcast address, as the router requires. */ export function routerAddressProblem(text: string): string { const network = networkOf(text, false) const own = addressOf(text.trim().split('/')[0]) if (network === null || own === null || !text.includes('/')) { return 'An IPv4 address with a prefix length, such as 192.168.200.1/24.' } if (network.size > 2 && (own === network.first || own === network.first + network.size - 1)) { return `The network or broadcast address of its network; give the router's own address in it, such as ${textOf(network.first + 1)}/${32 - Math.log2(network.size)}.` } return '' } /** Why a WireGuard public key is refused, or '' when it is 32 bytes in base64. */ export function keyProblem(key: string): string { return /^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw048]=$/.test(key.trim()) ? '' : 'A WireGuard public key: 44 characters of base64 ending with =.' } /** Why a list of a peer's IPs, separated by commas, is refused, or '' when each is an IPv4 address or network. */ export function ipsProblem(text: string): string { const ips = ipsOf(text) if (ips.length === 0) { return 'At least one IP.' } const wrong = ips.find((ip) => networkOf(ip, true) === null) return wrong === undefined ? '' : `${wrong} is not an IPv4 address or network, such as 10.10.0.2 or 10.20.0.0/24.` } /** The IPs of a list separated by commas, a single address written as /32. */ export function ipsOf(text: string): string[] { return text.split(',').map((ip) => ip.trim()).filter((ip) => ip !== '').map((ip) => (ip.includes('/') ? ip : `${ip}/32`)) } /** * The first address of the network of the router's address (wireguard.address, such as 192.168.200.1/24) that is * neither the router's own nor covered by one of the taken IPs, as a.b.c.d/32; '' when there is none. */ export function nextFreeAddress(routerAddress: string, taken: string[]): string { const router = networkOf(routerAddress, false) const own = addressOf(routerAddress.split('/')[0]) if (router === null || own === null) { return '' } const used = taken.map((ip) => networkOf(ip, false)).filter((network) => network !== null) for (let value = router.first + 1; value < router.first + router.size - 1; value += 1) { if (value !== own && !used.some((network) => value >= network.first && value < network.first + network.size)) { return `${textOf(value)}/32` } } return '' }