tv-anarchy/Sources/TVAnarchyCore/Mesh/MeshJoin.swift
Natalie 4a2ceb9781 feat(offline): inline star-to-keep and trash-to-cull on cache rows
Surface the existing pin (keep-from-cull) and per-file delete actions as
visible inline buttons on each offline cache row instead of context-menu-only:
a star toggles protection from auto-cull (and restore-if-missing), a trash
culls that file early. Aligns wording/icons to the star metaphor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:12:41 -04:00

133 lines
6.3 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import CryptoKit
/// The wg1 device mesh, as the app knows it. This is the **device-to-device
/// fabric** (Plane 1 of the networking design see
/// `.project/history/20260608_fleet-manager-mesh-design.md`), distinct from the
/// public-swarm OpenVPN exit managed by `VPNConfigStore`. Source of truth for
/// the topology is `net-tools/data/mesh-hosts.json`; these constants mirror its
/// `mesh` block and the enrollment contract of `session-tools/bin/wg-phone-add`
/// (same hub, same client store, same conf shape the app and the script are
/// interchangeable front-ends for one enrollment).
public enum MeshDefaults {
/// The hub's wg endpoint (yuzu / quinn-vps, 1984 Hosting Iceland).
public static let hubEndpoint = "89.127.233.145:51820"
/// The hub's wg1 public key.
public static let hubPublicKey = "t6LT6Ff4AxcGCd9Zug7dxkT7tXhkMV+fd28UCY/h8Xw="
/// SSH endpoints for editing the hub config, tried in order: the
/// `~/.ssh/config` alias first (carries the 1984 identity file), bare
/// root@public-IP as fallback.
public static let hubSSHEndpoints = ["quinn-vps", "root@89.127.233.145"]
/// Path of the hub-side wg config the enrollment appends to.
public static let hubConfigPath = "/etc/wireguard/wg1.conf"
public static let meshSubnet = "10.9.0.0/24"
public static let lanSubnet = "10.0.0.0/24"
/// Mesh DNS (apricot's dnsmasq) gives joined devices `.wg`/`.lan` names.
public static let meshDNS = "10.9.0.2"
}
/// A WireGuard Curve25519 keypair in wg's base64 wire format. Generated with
/// CryptoKit (X25519 per RFC 7748 byte-compatible with `wg genkey`/`wg pubkey`,
/// verified by test vector), so enrollment needs no `wg` binary on this Mac.
public struct WireGuardKeypair: Sendable, Equatable {
public let privateKey: String
public let publicKey: String
/// Fresh random keypair.
public init() {
let priv = Curve25519.KeyAgreement.PrivateKey()
privateKey = priv.rawRepresentation.base64EncodedString()
publicKey = priv.publicKey.rawRepresentation.base64EncodedString()
}
/// Rebuild from a stored base64 private key (re-derives the public key).
/// Nil when the string isn't a valid 32-byte base64 scalar.
public init?(privateKeyBase64: String) {
let trimmed = privateKeyBase64.trimmingCharacters(in: .whitespacesAndNewlines)
guard let raw = Data(base64Encoded: trimmed), raw.count == 32,
let priv = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: raw) else { return nil }
privateKey = trimmed
publicKey = priv.publicKey.rawRepresentation.base64EncodedString()
}
}
/// Pure pieces of the enrollment: device-name validation, free-IP selection
/// against the hub config, and rendering of both sides' config text. Mirrors
/// `wg-phone-add` byte-for-byte where it matters (the hub `[Peer]` block probe
/// is an exact-line `grep`), so the script and the app stay idempotent against
/// each other.
public enum MeshJoin {
/// Valid device name: non-empty, `[A-Za-z0-9_-]` only (used in filenames and
/// as the comment line in the hub conf). Same rule as `wg-phone-add`.
public static func isValidDeviceName(_ name: String) -> Bool {
!name.isEmpty && name.allSatisfy { $0.isASCII && ($0.isLetter || $0.isNumber || $0 == "_" || $0 == "-") }
}
/// Lowest free `10.9.0.n` (n in 5...254) given the hub config text `.1``.4`
/// are the fixed hosts, phones/tablets start at `.5`. Nil when the /24 is full.
public static func freeAddress(hubConfigText: String) -> String? {
var used = Set<Int>()
for match in hubConfigText.matches(of: #/10\.9\.0\.(\d+)/#) {
if let n = Int(match.1) { used.insert(n) }
}
return (5...254).first { !used.contains($0) }.map { "10.9.0.\($0)" }
}
/// The `[Peer]` block appended to the hub's wg1.conf for a new device.
public static func hubPeerBlock(device: String, publicKey: String, address: String) -> String {
"""
[Peer]
# \(device)
PublicKey = \(publicKey)
AllowedIPs = \(address)/32
PersistentKeepalive = 25
"""
}
/// The app-setup payload for the iOS app's join page the SECOND QR a
/// phone scans (after the WireGuard app takes the mesh one). Carries this
/// Mac's bridge endpoints: primary = the LAN leg, fallback = the mesh leg,
/// matching BridgeSettings' host/fallbackHost model. The iOS side parses
/// this exact shape (tvanarchy://bridge?host=&fallback=&port=); change
/// them together. Nil when this Mac has no address to advertise.
public static func phoneSetupURL(lanHost: String? = LocalNetwork.lanAddress(),
meshHost: String? = LocalNetwork.meshAddress(),
port: Int = 8787) -> URL? {
guard let primary = lanHost ?? meshHost else { return nil }
var comps = URLComponents()
comps.scheme = "tvanarchy"
comps.host = "bridge"
var items = [URLQueryItem(name: "host", value: primary),
URLQueryItem(name: "port", value: String(port))]
if let meshHost, meshHost != primary {
items.append(URLQueryItem(name: "fallback", value: meshHost))
}
comps.queryItems = items
return comps.url
}
/// The client-side wg-quick config the new device imports (QR or file).
/// Routes the mesh /24 and the home LAN /24 through the tunnel; DNS via the
/// mesh resolver so `.wg`/`.lan` names work on the device.
public static func clientConfig(device: String, privateKey: String, address: String,
generatedAt: Date = Date()) -> String {
let stamp = ISO8601DateFormatter().string(from: generatedAt)
return """
# WireGuard config for \(device)
# Generated by TVAnarchy at \(stamp)
# Hub: \(MeshDefaults.hubEndpoint)
[Interface]
PrivateKey = \(privateKey)
Address = \(address)/32
DNS = \(MeshDefaults.meshDNS)
[Peer]
PublicKey = \(MeshDefaults.hubPublicKey)
Endpoint = \(MeshDefaults.hubEndpoint)
AllowedIPs = \(MeshDefaults.meshSubnet), \(MeshDefaults.lanSubnet)
PersistentKeepalive = 25
"""
}
}