Ground-Zerro / Phobos Public
Code Issues Pull requests Actions Releases View on GitHub ↗
6.4 KB typescript
// ! Auto Imports are not supported in this file

import { parseCidr } from 'cidr-tools';
import { stringifyIp } from 'ip-bigint';
import { removeNewlines } from './template';

import type { ClientType } from '#db/repositories/client/types';
import type { InterfaceType } from '#db/repositories/interface/types';
import type { UserConfigType } from '#db/repositories/userConfig/types';
import type { HooksType } from '#db/repositories/hooks/types';

type Options = {
  enableIpv6?: boolean;
  /** Local WG port on the client side, from the preset bound to this client. */
  clientWgLocalPort?: number;
};

const DEFAULT_CLIENT_WG_LOCAL_PORT = 13255;

const wgExecutable =
  typeof WG_ENV !== 'undefined' ? WG_ENV.WG_EXECUTABLE : 'wg';

export const wg = {
  generateServerPeer: (
    client: Omit<ClientType, 'createdAt' | 'updatedAt'>,
    options: Options = {}
  ) => {
    const { enableIpv6 = true } = options;

    const allowedIps = [
      `${client.ipv4Address}/32`,
      ...(enableIpv6 ? [`${client.ipv6Address}/128`] : []),
      ...(client.serverAllowedIps ?? []),
    ];

    const extraLines = [];
    if (client.serverEndpoint) {
      extraLines.push(`Endpoint = ${client.serverEndpoint}`);
    }

    return `# Client: ${client.name} (${client.id})
[Peer]
PublicKey = ${client.publicKey}
PresharedKey = ${client.preSharedKey}
AllowedIPs = ${allowedIps.join(', ')}${extraLines.length ? `\n${extraLines.join('\n')}` : ''}`;
  },

  generateServerInterface: (
    wgInterface: InterfaceType,
    hooks: HooksType,
    options: Options = {}
  ) => {
    const { enableIpv6 = true } = options;

    const cidr4 = parseCidr(wgInterface.ipv4Cidr);
    const cidr6 = parseCidr(wgInterface.ipv6Cidr);
    const ipv4Addr = stringifyIp({ number: cidr4.start + 1n, version: 4 });
    const ipv6Addr = stringifyIp({ number: cidr6.start + 1n, version: 6 });

    const address =
      `${ipv4Addr}/${cidr4.prefix}` +
      (enableIpv6 ? `, ${ipv6Addr}/${cidr6.prefix}` : '');

    const loopbackDrop4 = `iptables -I INPUT 1 -p udp --dport ${wgInterface.port} ! -s 127.0.0.1 -j DROP;`;
    const loopbackDrop6 = `ip6tables -I INPUT 1 -p udp --dport ${wgInterface.port} ! -s ::1 -j DROP;`;
    const loopbackDel4 = `iptables -D INPUT -p udp --dport ${wgInterface.port} ! -s 127.0.0.1 -j DROP;`;
    const loopbackDel6 = `ip6tables -D INPUT -p udp --dport ${wgInterface.port} ! -s ::1 -j DROP;`;

    const joinShell = (parts: string[]) =>
      parts
        .map((p) => p.trim())
        .filter(Boolean)
        .map((p) => (p.endsWith(';') ? p : `${p};`))
        .join(' ');

    const postUpCombined = joinShell([
      iptablesTemplate(hooks.postUp, wgInterface),
      loopbackDrop4,
      loopbackDrop6,
    ]);

    const preDownCombined = joinShell([
      loopbackDel4,
      loopbackDel6,
      iptablesTemplate(hooks.preDown, wgInterface),
    ]);

    return `# Note: Do not edit this file directly.
# Your changes will be overwritten!

# Server
[Interface]
PrivateKey = ${wgInterface.privateKey}
Address = ${address}
ListenPort = ${wgInterface.port}
MTU = ${wgInterface.mtu}

PreUp = ${iptablesTemplate(hooks.preUp, wgInterface)}
PostUp = ${postUpCombined}
PreDown = ${preDownCombined}
PostDown = ${iptablesTemplate(hooks.postDown, wgInterface)}`;
  },

  generateClientConfig: (
    wgInterface: InterfaceType,
    userConfig: UserConfigType,
    client: ClientType,
    options: Options = {}
  ) => {
    const {
      enableIpv6 = true,
      clientWgLocalPort = DEFAULT_CLIENT_WG_LOCAL_PORT,
    } = options;

    const address =
      `${client.ipv4Address}/32` +
      (enableIpv6 ? `, ${client.ipv6Address}/128` : '');

    const hookLines = [
      client.preUp ? `PreUp = ${removeNewlines(client.preUp)}` : null,
      client.postUp ? `PostUp = ${removeNewlines(client.postUp)}` : null,
      client.preDown ? `PreDown = ${removeNewlines(client.preDown)}` : null,
      client.postDown ? `PostDown = ${removeNewlines(client.postDown)}` : null,
    ];

    const dnsServers = client.dns ?? userConfig.defaultDns;
    const dnsLine =
      dnsServers.length > 0 ? `DNS = ${dnsServers.join(', ')}` : null;

    const extraLines = [dnsLine, ...hookLines].filter(
      (v) => v !== null
    );

    return `[Interface]
PrivateKey = ${client.privateKey}
Address = ${address}
MTU = ${client.mtu}
${extraLines.length ? `${extraLines.join('\n')}\n` : ''}
[Peer]
PublicKey = ${wgInterface.publicKey}
PresharedKey = ${client.preSharedKey}
AllowedIPs = ${(client.allowedIps ?? userConfig.defaultAllowedIps).join(', ')}
PersistentKeepalive = ${client.persistentKeepalive}
Endpoint = 127.0.0.1:${clientWgLocalPort}`;
  },

  generatePrivateKey: () => {
    return exec(`${wgExecutable} genkey`);
  },

  getPublicKey: (privateKey: string) => {
    return exec(`echo ${privateKey} | ${wgExecutable} pubkey`, {
      log: `echo ***hidden*** | ${wgExecutable} pubkey`,
    });
  },

  generatePreSharedKey: () => {
    return exec(`${wgExecutable} genpsk`);
  },

  up: (infName: string) => {
    return exec(`${wgExecutable}-quick up ${infName}`);
  },

  down: (infName: string) => {
    return exec(`${wgExecutable}-quick down ${infName}`);
  },

  restart: (infName: string) => {
    return exec(
      `${wgExecutable}-quick down ${infName}; ${wgExecutable}-quick up ${infName}`
    );
  },

  sync: (infName: string) => {
    return exec(
      `${wgExecutable} syncconf ${infName} <(${wgExecutable}-quick strip ${infName})`
    );
  },

  dump: async (infName: string) => {
    const rawDump = await exec(`${wgExecutable} show ${infName} dump`, {
      log: false,
    });

    type wgDumpLine = [
      string,
      string,
      string,
      string,
      string,
      string,
      string,
      string,
    ];

    return rawDump
      .trim()
      .split('\n')
      .slice(1)
      .map((line) => {
        const splitLines = line.split('\t');
        const [
          publicKey,
          preSharedKey,
          endpoint,
          allowedIps,
          latestHandshakeAt,
          transferRx,
          transferTx,
          persistentKeepalive,
        ] = splitLines as wgDumpLine;

        return {
          publicKey,
          preSharedKey,
          endpoint: endpoint === '(none)' ? null : endpoint,
          allowedIps,
          latestHandshakeAt:
            latestHandshakeAt === '0'
              ? null
              : new Date(Number.parseInt(`${latestHandshakeAt}000`)),
          transferRx: Number.parseInt(transferRx),
          transferTx: Number.parseInt(transferTx),
          persistentKeepalive: persistentKeepalive,
        };
      });
  },
};