#!/usr/bin/env bash # ============================================ # Lifecycle # ============================================ function cmd::add::on_load() { flag::register --name flag::register --type flag::register --ip flag::register --preset flag::register --guest flag::register --tunnel flag::register --show-config flag::register --show-qr } # ============================================ # Help # ============================================ function cmd::add::help() { cat < --type [options] Add a new WireGuard client. Options: --name Client name (e.g. nuno) --type Device type: desktop, laptop, phone, tablet, guest --ip Override auto-assigned IP (optional) --preset Apply a firewall preset (repeatable) --guest Shorthand for --type guest --tunnel Tunnel mode: split (default) or full --show-config Shows the WireGuard peer config --show-qr Shows the WireGuard config in a QR Code Device Types and Subnets: desktop 10.1.1.x laptop 10.1.2.x phone 10.1.3.x tablet 10.1.4.x guest 10.1.100.x Tunnel Modes: split Route only VPN subnet + LAN through WireGuard (default) full Route all traffic through WireGuard Examples: wgctl add --name nuno --type phone wgctl add --name nuno --type laptop --ip 10.1.2.5 wgctl add --name nuno --type phone --tunnel full wgctl add --name guest1 --type phone --guest wgctl add --name restricted --type desktop --preset no-docker --preset no-proxmox EOF } # ============================================ # Validation # ============================================ function cmd::add::validate() { local name="$1" local type="$2" local ip="$3" local tunnel="$4" if [[ -z "$name" ]]; then log::error "Missing required flag: --name" return 1 fi if [[ -z "$type" ]]; then log::error "Missing required flag: --type" return 1 fi if ! config::is_valid_type "$type"; then log::error "Invalid device type: ${type}" log::info "Valid types: $(config::device_types | tr ' ' ', ')" return 1 fi if [[ -n "$tunnel" && "$tunnel" != "split" && "$tunnel" != "full" ]]; then log::error "Invalid tunnel mode: ${tunnel} (use 'split' or 'full')" return 1 fi if [[ -n "$ip" ]]; then ip::require_valid "$ip" fi local full_name="${type}-${name}" if [[ -f "$(ctx::clients)/${full_name}.conf" ]]; then log::error "Client already exists: ${full_name}" return 1 fi } # ============================================ # Display helpers # ============================================ function cmd::add::is_mobile() { local type="$1" [[ "$type" == "phone" || "$type" == "tablet" ]] } # ============================================ # Run # ============================================ function cmd::add::run() { local name="" local type="" local ip="" local tunnel="" local guest=false local presets=() local show_config=false local show_qr=false # Parse flags while [[ $# -gt 0 ]]; do case "$1" in --name) name="$2"; shift 2 ;; --type) type="$2"; shift 2 ;; --ip) ip="$2"; shift 2 ;; --preset) presets+=("$2"); shift 2 ;; --guest) guest=true; shift ;; --tunnel) tunnel="$2"; shift 2 ;; --show-config) show_config=true; shift ;; --show-qr) show_qr=true; shift ;; --help) cmd::add::help; return ;; *) log::error "Unknown flag: $1" cmd::add::help return 1 ;; esac done # --guest shorthand if $guest; then type="guest" fi # Build full client name local full_name="${type}-${name}" # Validate cmd::add::validate "$name" "$type" "$ip" "$tunnel" || return 1 # Resolve tunnel mode — flag > device default if [[ -z "$tunnel" ]]; then tunnel=$(config::default_tunnel_for "$type") fi local allowed_ips allowed_ips=$(config::allowed_ips_for "$type" "$tunnel") || return 1 log::section "Adding client: ${full_name}" # Auto-assign IP if not provided if [[ -z "$ip" ]]; then ip=$(ip::next_for_type "$type") || return 1 fi log::wg_add "Name: ${full_name}" log::wg_add "Type: ${type}" log::wg_add "IP: ${ip}" log::wg_add "Tunnel: ${tunnel} (${allowed_ips})" # Generate keys keys::generate_pair "$full_name" || return 1 # Create client config peers::create_client_config "$full_name" "$type" "$ip" "$allowed_ips" || return 1 # Add peer to server config local public_key public_key=$(keys::public "$full_name") || return 1 peers::add_to_server "$full_name" "$public_key" "$ip" || return 1 # Apply presets for preset in "${presets[@]}"; do firewall::apply_preset "$preset" "${ip}" || return 1 done # Apply guest rules if guest type if [[ "$type" == "guest" ]]; then firewall::apply_guest_rules fi # Reload WireGuard peers::reload || return 1 log::wg_success "Client added successfully: ${full_name} (${ip}) [${tunnel} tunnel]" # Show QR for mobile by default, config for desktop/laptop # --show-config overrides to always show config if $show_qr; then keys::qr "$full_name" elif $show_config || ! cmd::add::is_mobile "$type"; then log::section "Client Config" cat "$(ctx::clients)/${full_name}.conf" else keys::qr "$full_name" fi }