Files
linux-cloud-router/web/src/wireguard.ts
T

105 lines
4.4 KiB
TypeScript

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 = <the private key of this device>',
`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 ''
}