Ground-Zerro / Phobos Public
Code Issues Pull requests Actions Releases View on GitHub ↗
10.5 KB typescript
import fs from 'node:fs/promises';
import debug from 'debug';

import { warpApi } from '../phobos/warp/client';
import type { WarpManualType, WarpType } from '#db/repositories/warp/types';

const WARP_DEBUG = debug('Warp');

const WARP_INTERFACE = 'warp0';
const WARP_TABLE = 200;
export const WARP_FWMARK = 9224;
const WARP_CONFIG_PATH = `/etc/wireguard/${WARP_INTERFACE}.conf`;
const HANDSHAKE_RETRIES = 10;
const HANDSHAKE_DELAY_MS = 1000;
const WARP_ONLINE_FLOOR_S = 135;
const CONNECTIVITY_TIMEOUT_S = 5;
const CONNECTIVITY_PROBE_URL = 'http://cp.cloudflare.com/generate_204';

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

class WarpInterfaceService {
  buildConfig(
    warp: WarpType,
    ipv4Cidr: string,
    ipv6Cidr: string,
    enableIpv6: boolean
  ): string {
    const address =
      `${warp.addressV4}/32` +
      (enableIpv6 && warp.addressV6 ? `, ${warp.addressV6}/128` : '');

    const postUp = joinShell([
      `ip route replace ${ipv4Cidr} dev wg0 table ${WARP_TABLE}`,
      `ip rule del from ${ipv4Cidr} lookup ${WARP_TABLE} 2>/dev/null || true`,
      `ip rule add from ${ipv4Cidr} lookup ${WARP_TABLE}`,
      `iptables -t nat -A POSTROUTING -s ${ipv4Cidr} -o %i -j MASQUERADE`,
      `ip rule del fwmark ${WARP_FWMARK} lookup ${WARP_TABLE} 2>/dev/null || true`,
      `ip rule add fwmark ${WARP_FWMARK} lookup ${WARP_TABLE}`,
      `iptables -t nat -A POSTROUTING -m mark --mark ${WARP_FWMARK} -o %i -j MASQUERADE`,
      ...(enableIpv6
        ? [
            `ip -6 route replace ${ipv6Cidr} dev wg0 table ${WARP_TABLE}`,
            `ip -6 rule del from ${ipv6Cidr} lookup ${WARP_TABLE} 2>/dev/null || true`,
            `ip -6 rule add from ${ipv6Cidr} lookup ${WARP_TABLE}`,
            `ip6tables -t nat -A POSTROUTING -s ${ipv6Cidr} -o %i -j MASQUERADE`,
            `ip -6 rule del fwmark ${WARP_FWMARK} lookup ${WARP_TABLE} 2>/dev/null || true`,
            `ip -6 rule add fwmark ${WARP_FWMARK} lookup ${WARP_TABLE}`,
            `ip6tables -t nat -A POSTROUTING -m mark --mark ${WARP_FWMARK} -o %i -j MASQUERADE`,
          ]
        : []),
    ]);

    const postDown = joinShell([
      `ip rule del from ${ipv4Cidr} lookup ${WARP_TABLE} 2>/dev/null || true`,
      `iptables -t nat -D POSTROUTING -s ${ipv4Cidr} -o %i -j MASQUERADE 2>/dev/null || true`,
      `ip rule del fwmark ${WARP_FWMARK} lookup ${WARP_TABLE} 2>/dev/null || true`,
      `iptables -t nat -D POSTROUTING -m mark --mark ${WARP_FWMARK} -o %i -j MASQUERADE 2>/dev/null || true`,
      ...(enableIpv6
        ? [
            `ip -6 rule del from ${ipv6Cidr} lookup ${WARP_TABLE} 2>/dev/null || true`,
            `ip6tables -t nat -D POSTROUTING -s ${ipv6Cidr} -o %i -j MASQUERADE 2>/dev/null || true`,
            `ip -6 rule del fwmark ${WARP_FWMARK} lookup ${WARP_TABLE} 2>/dev/null || true`,
            `ip6tables -t nat -D POSTROUTING -m mark --mark ${WARP_FWMARK} -o %i -j MASQUERADE 2>/dev/null || true`,
          ]
        : []),
    ]);

    const ifaceLines = [
      `PrivateKey = ${warp.privateKey}`,
      `Address = ${address}`,
      `MTU = ${warp.mtu}`,
      `Table = ${WARP_TABLE}`,
      `PostUp = ${postUp}`,
      `PostDown = ${postDown}`,
    ];

    const peerLines = [
      `PublicKey = ${warp.peerPublicKey}`,
      ...(warp.presharedKey ? [`PresharedKey = ${warp.presharedKey}`] : []),
      `Endpoint = ${warp.endpoint}`,
      `AllowedIPs = 0.0.0.0/0${enableIpv6 ? ', ::/0' : ''}`,
      `PersistentKeepalive = ${warp.persistentKeepalive}`,
    ];

    return `[Interface]\n${ifaceLines.join('\n')}\n\n[Peer]\n${peerLines.join('\n')}\n`;
  }

  buildUserConfig(warp: WarpType, enableIpv6: boolean): string {
    const address =
      `${warp.addressV4}/32` +
      (enableIpv6 && warp.addressV6 ? `, ${warp.addressV6}/128` : '');

    const ifaceLines = [
      `PrivateKey = ${warp.privateKey}`,
      `Address = ${address}`,
      `MTU = ${warp.mtu}`,
      ...(warp.dns ? [`DNS = ${warp.dns}`] : []),
    ];

    const peerLines = [
      `PublicKey = ${warp.peerPublicKey}`,
      ...(warp.presharedKey ? [`PresharedKey = ${warp.presharedKey}`] : []),
      `Endpoint = ${warp.endpoint}`,
      `AllowedIPs = 0.0.0.0/0${enableIpv6 ? ', ::/0' : ''}`,
      `PersistentKeepalive = ${warp.persistentKeepalive}`,
    ];

    return `[Interface]\n${ifaceLines.join('\n')}\n\n[Peer]\n${peerLines.join('\n')}\n`;
  }

  async #writeConfig() {
    const [warp, wgInterface] = await Promise.all([
      Database.warp.get(),
      Database.interfaces.get(),
    ]);

    const config = this.buildConfig(
      warp,
      wgInterface.ipv4Cidr,
      wgInterface.ipv6Cidr,
      !WG_ENV.DISABLE_IPV6
    );

    await fs.writeFile(WARP_CONFIG_PATH, config, { mode: 0o600 });
  }

  async #up() {
    await exec(`wg-quick up ${WARP_INTERFACE}`);
  }

  async #down() {
    await exec(`wg-quick down ${WARP_INTERFACE}`).catch(() => {});
  }

  async #latestHandshakeTs(): Promise<number> {
    const out = await exec(`wg show ${WARP_INTERFACE} latest-handshakes`, {
      log: false,
    }).catch(() => '');

    return out
      .trim()
      .split('\n')
      .filter(Boolean)
      .reduce((max, line) => {
        const ts = Number.parseInt(line.split('\t')[1] ?? '0', 10);
        return ts > max ? ts : max;
      }, 0);
  }

  async #handshakeEstablished(): Promise<boolean> {
    return (await this.#latestHandshakeTs()) > 0;
  }

  async status(): Promise<{ online: boolean; lastHandshakeAt: string | null }> {
    const wgInterface = await Database.interfaces.get();
    if (wgInterface.egressMode !== 'warp' || !wgInterface.enabled) {
      return { online: false, lastHandshakeAt: null };
    }

    const ts = await this.#latestHandshakeTs();
    if (ts <= 0) {
      return { online: false, lastHandshakeAt: null };
    }

    const warp = await Database.warp.get();
    const stalenessMs =
      Math.max(warp.persistentKeepalive * 3, WARP_ONLINE_FLOOR_S) * 1000;
    const ageMs = Date.now() - ts * 1000;

    return {
      online: ageMs < stalenessMs,
      lastHandshakeAt: new Date(ts * 1000).toISOString(),
    };
  }

  async connectivityCheck(): Promise<boolean> {
    const code = await exec(
      `curl --interface ${WARP_INTERFACE} -s -o /dev/null -w '%{http_code}' --max-time ${CONNECTIVITY_TIMEOUT_S} ${CONNECTIVITY_PROBE_URL}`,
      { log: false }
    ).catch(() => '');

    return code.trim() === '204' || code.trim() === '200';
  }

  async #healthCheck(): Promise<boolean> {
    for (let attempt = 0; attempt < HANDSHAKE_RETRIES; attempt += 1) {
      if (await this.#handshakeEstablished()) {
        return true;
      }
      await new Promise((resolve) => setTimeout(resolve, HANDSHAKE_DELAY_MS));
    }
    return false;
  }

  /**
   * Brings warp0 up with a health check. On failure, tears it down and resets
   * egressMode to 'wan' so clients keep WAN connectivity.
   * @throws when the tunnel cannot be established
   */
  async enable() {
    if (!(await Database.warp.isRegistered())) {
      throw new Error('WARP is not registered');
    }

    await this.#down();
    await this.#writeConfig();
    await this.#up();

    const healthy = await this.#healthCheck();
    if (!healthy) {
      await this.#down();
      await Database.interfaces.update({ egressMode: 'wan' });
      throw new Error(
        'WARP tunnel did not establish: endpoint UDP 2408 may be blocked by your provider'
      );
    }

    WARP_DEBUG('WARP interface is up and healthy');
  }

  async disable() {
    await this.#down();
    WARP_DEBUG('WARP interface is down');
  }

  /** Re-applies the interface only when WARP egress is currently active. */
  async reapply() {
    const wgInterface = await Database.interfaces.get();
    if (wgInterface.egressMode === 'warp' && wgInterface.enabled) {
      await this.enable();
    }
  }

  async register(): Promise<void> {
    const privateKey = await wg.generatePrivateKey();
    const publicKey = await wg.getPublicKey(privateKey);
    const registration = await warpApi.register(privateKey, publicKey);
    await Database.warp.setRegistration(registration);
    await this.reapply();
  }

  async changeIp(): Promise<void> {
    const previous = await Database.warp.get();
    await this.register();
    if (previous.licenseKey.length >= 26) {
      await this.setLicense(previous.licenseKey).catch((err) => {
        WARP_DEBUG('Failed to re-apply WARP license after IP change:', err);
      });
    }
    await Database.warp.setLastUpdateAt(new Date().toISOString());
  }

  async setLicense(license: string): Promise<void> {
    const warp = await Database.warp.get();
    await warpApi.setLicense(warp.deviceId, warp.accessToken, license);
    await Database.warp.setLicenseKey(license);
  }

  async remoteConfig(): Promise<unknown> {
    const warp = await Database.warp.get();
    return warpApi.getConfig(warp.deviceId, warp.accessToken);
  }

  async delete(): Promise<void> {
    await this.disable();
    await Database.warp.clear();
    await Database.interfaces.update({ egressMode: 'wan' });
    await fs.rm(WARP_CONFIG_PATH, { force: true });
  }

  async importConfig(data: WarpManualType): Promise<void> {
    const [warpRow, wgInterface] = await Promise.all([
      Database.warp.get(),
      Database.interfaces.get(),
    ]);

    const wasActive = wgInterface.egressMode === 'warp' && wgInterface.enabled;

    const enableIpv6 = !WG_ENV.DISABLE_IPV6;
    const testConfig = this.buildConfig(
      { ...warpRow, ...data },
      wgInterface.ipv4Cidr,
      wgInterface.ipv6Cidr,
      enableIpv6
    );

    await this.#down();
    await fs.writeFile(WARP_CONFIG_PATH, testConfig, { mode: 0o600 });
    await this.#up();

    const healthy = await this.#healthCheck();
    if (!healthy) {
      await this.#down();
      if (wasActive) {
        await this.#writeConfig().catch(() => {});
        await this.#up().catch(() => {});
      }
      throw new Error(
        'WARP handshake failed: endpoint may be unreachable or blocked'
      );
    }

    await Database.warp.update(data);
    WARP_DEBUG('WARP config imported and interface is up');
  }

  async Startup() {
    const wgInterface = await Database.interfaces.get();
    if (
      wgInterface.egressMode !== 'warp' ||
      !wgInterface.enabled ||
      !(await Database.warp.isRegistered())
    ) {
      return;
    }

    WARP_DEBUG('Restoring WARP egress on startup...');
    await this.enable().catch((err) => {
      console.warn(`WARNING: failed to restore WARP egress: ${err.message}`);
    });
  }

  async Shutdown() {
    await this.#down();
  }
}

export const WarpInterface = new WarpInterfaceService();

export default WarpInterface;