#!/usr/bin/env bash # ============================================ # Lifecycle # ============================================ function cmd::add::on_load() { flag::register --name flag::register --type flag::register --subtype flag::register --rule flag::register --group flag::register --ip 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 --subtype Guest subtype: desktop, laptop, phone, tablet (mostly used for guest) --ip Override auto-assigned IP (optional) --guest Shorthand for --type guest --tunnel Tunnel mode: split|full (default: split) --rule Assign rule on creation (default: user, guest types: guest) --group Add to group on creation (group must exist) --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 Rules: Automatically assigned based on type (guest → guest rule, others → user rule). Override with --rule. Manage rules with: wgctl rule help 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 wgctl add --name dev --type laptop --rule dev-01 wgctl add --name visitor --type guest --show-qr 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 subtype="" local rule="" local group="" local ip="" local tunnel="" local guest=false 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 ;; --subtype) subtype="$2"; shift 2 ;; --rule) rule="$2"; shift 2 ;; --group) group="$2"; shift 2 ;; --ip) ip="$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 && type="guest" local effective_type effective_type=$(cmd::add::_resolve_type "$type" "$subtype") || return 1 local full_name="${type}-${name}" cmd::add::validate "$name" "$type" "$ip" "$tunnel" || return 1 [[ -z "$tunnel" ]] && tunnel=$(config::default_tunnel_for "$type") [[ -z "$rule" ]] && rule=$(cmd::add::_default_rule "$type") rule::exists "$rule" || { log::error "Rule not found: ${rule}"; return 1; } local allowed_ips allowed_ips=$(config::allowed_ips_for "$effective_type" "$tunnel") || return 1 log::section "Adding client: ${full_name}" [[ -z "$ip" ]] && ip=$(ip::next_for_type "$effective_type") || return 1 log::wg_add "Name: ${full_name}" log::wg_add "Type: ${type}" log::wg_add "IP: ${ip}" log::wg_add "Tunnel: ${tunnel} (${allowed_ips})" log::wg_add "Endpoint: $(config::endpoint)" log::wg_add "Rule: ${rule:-none}" keys::generate_pair "$full_name" || return 1 peers::create_client_config "$full_name" "$effective_type" "$ip" "$allowed_ips" || return 1 [[ -n "$subtype" ]] && peers::set_meta "$full_name" "subtype" "$subtype" if [[ -n "$group" ]]; then if ! group::exists "$group"; then log::wg_warning "Group '${group}' not found — skipping group assignment" else group::add_peer "$group" "$full_name" log::wg "Added to group: ${group}" fi fi local public_key public_key=$(keys::public "$full_name") || return 1 peers::add_to_server "$full_name" "$public_key" "$ip" || return 1 [[ -n "$rule" ]] && rule::apply "$rule" "$ip" || return 1 peers::reload || return 1 log::wg_success "Client added successfully: ${full_name} (${ip}) [${tunnel} tunnel]" cmd::add::_show_result "$full_name" "${subtype:-$type}" } function cmd::add::_resolve_type() { local type="$1" subtype="$2" if [[ "$type" == "guest" && -n "$subtype" ]]; then local valid_subtypes="desktop laptop phone tablet" if ! echo "$valid_subtypes" | grep -qw "$subtype"; then log::error "Invalid subtype: ${subtype} (valid: desktop, laptop, phone, tablet)" return 1 fi echo "guest-${subtype}" else echo "$type" fi } function cmd::add::_default_rule() { local type="$1" config::is_guest_type "$type" && echo "guest" || echo "user" } function cmd::add::_show_result() { local full_name="$1" display_type="$2" if cmd::add::is_mobile "$display_type"; then keys::qr "$full_name" else log::section "Client Config" cat "$(ctx::clients)/${full_name}.conf" fi }