wgctl/commands/add.command.sh

313 lines
9.6 KiB
Bash

#!/usr/bin/env bash
# ============================================
# Lifecycle
# ============================================
function cmd::add::on_load() {
load_module subnet
load_module identity
load_module policy
flag::register --name
flag::register --identity
flag::register --type
flag::register --subnet
flag::register --rule
flag::register --group
flag::register --ip
flag::register --tunnel
flag::register --show-qr
# Dynamically register --<subnet_name> as shorthand flags
local subnet_name
while IFS= read -r subnet_name; do
[[ -n "$subnet_name" ]] && flag::register "--${subnet_name}"
done < <(subnet::list_names)
}
# ============================================
# Help
# ============================================
function cmd::add::help() {
cat <<EOF
Usage: wgctl add --name <name> --type <type> [options]
or: wgctl add --identity <identity> --type <type> [options]
Add a new WireGuard client.
Options:
--name <name> Client name (e.g. nuno) — combined with type: phone-nuno
--identity <name> Identity name — auto-names peer with next available index
--type <type> Device type: desktop, laptop, phone, tablet, server, iot
--subnet <subnet> Subnet to allocate from (default: type-native)
--ip <ip> Override auto-assigned IP (optional)
--tunnel <mode> Tunnel mode: split|full (overrides policy)
--rule <rule> Peer rule (default: from policy default_rule or none)
--group <group> Add to group on creation (group must exist)
--show-qr Show the WireGuard config as a QR code after creation
Subnet shorthands (equivalent to --subnet <name>):
--guests, --servers, --iot, ... (see: wgctl subnet list)
Examples:
wgctl add --name nuno --type phone
wgctl add --identity nuno --type phone
wgctl add --name zephyr --type desktop --guests
wgctl add --identity zephyr --type desktop --guests
wgctl add --name visitor --type phone --guests --show-qr
wgctl add --name dev --type laptop --rule dev-01
EOF
}
# ============================================
# Validation
# ============================================
function cmd::add::_validate() {
local name="$1" identity="$2" type="$3" ip="$4" tunnel="$5"
if [[ -z "$name" && -z "$identity" ]]; then
log::error "Missing required flag: --name or --identity"
return 1
fi
if [[ -z "$type" ]]; then
log::error "Missing required flag: --type"
return 1
fi
if ! json::subnet_exists "$(ctx::subnets)" "$type" 2>/dev/null; then
log::error "Unknown device type: '${type}'"
log::info "Use 'wgctl subnet list' to see valid types and subnets"
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
}
function cmd::add::_validate_not_exists() {
local full_name="$1"
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="" identity="" type="" subnet_name="" rule="" \
group="" ip="" tunnel="" show_qr=false
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--identity) identity="$2"; shift 2 ;;
--type) type="$2"; shift 2 ;;
--subnet) subnet_name="$2"; shift 2 ;;
--rule) rule="$2"; shift 2 ;;
--group) group="$2"; shift 2 ;;
--ip) ip="$2"; shift 2 ;;
--tunnel) tunnel="$2"; shift 2 ;;
--show-qr) show_qr=true; shift ;;
--help) cmd::add::help; return ;;
--*)
local flag_name="${1#--}"
if subnet::exists "$flag_name" 2>/dev/null; then
subnet_name="$flag_name"
shift
else
log::error "Unknown flag: $1"
cmd::add::help
return 1
fi
;;
*)
log::error "Unknown flag: $1"
cmd::add::help
return 1
;;
esac
done
cmd::add::_validate "$name" "$identity" "$type" "$ip" "$tunnel" || return 1
# Resolve full peer name
local full_name
if [[ -n "$identity" ]]; then
full_name=$(identity::next_peer_name "$identity" "$type") || return 1
log::info "Auto-named: ${full_name}"
else
full_name="${type}-${name}"
fi
cmd::add::_validate_not_exists "$full_name" || return 1
# Resolve subnet CIDR and canonical type
local resolved_cidr resolved_type
resolved_cidr=$(subnet::resolve_for_add "$type" "$subnet_name") || return 1
resolved_type=$(subnet::type_for_add "$type" "$subnet_name") || return 1
# Resolve effective policy
local identity_name="${identity:-$(identity::get_name "$full_name")}"
local effective_policy
effective_policy=$(policy::effective "$subnet_name" "$resolved_type" "$identity_name")
# Resolve tunnel mode — flag overrides policy
if [[ -z "$tunnel" ]]; then
tunnel=$(policy::tunnel_mode "$effective_policy")
fi
# Resolve peer rule — explicit flag overrides policy default_rule
if [[ -z "$rule" ]]; then
rule=$(policy::default_rule "$effective_policy")
fi
# Validate rule if set
if [[ -n "$rule" ]]; then
rule::exists "$rule" || { log::error "Rule not found: ${rule}"; return 1; }
fi
local allowed_ips
allowed_ips=$(config::allowed_ips_for "$tunnel") || return 1
log::section "Adding client: ${full_name}"
# Allocate IP
if [[ -n "$ip" ]]; then
subnet::require_ip_valid_for "$resolved_cidr" "$ip" || return 1
else
ip=$(ip::next_for_subnet "$resolved_cidr") || return 1
fi
cmd::add::_log_plan "$full_name" "$type" "$resolved_type" \
"$subnet_name" "$resolved_cidr" "$ip" "$tunnel" \
"$allowed_ips" "${rule:---}" "$effective_policy"
keys::generate_pair "$full_name" || return 1
peers::create_client_config "$full_name" "$resolved_type" "$ip" "$allowed_ips" || return 1
# Write meta — type, subnet, rule (if set)
peers::set_meta "$full_name" "type" "$resolved_type"
if [[ -n "$subnet_name" ]]; then
peers::set_meta "$full_name" "subnet" "$subnet_name"
fi
if [[ -n "$rule" ]]; then
peers::set_meta "$full_name" "rule" "$rule"
fi
cmd::add::_assign_group "$full_name" "$group"
local public_key
public_key=$(keys::public "$full_name") || return 1
peers::add_to_server "$full_name" "$public_key" "$ip" || return 1
# Apply peer rule if set
if [[ -n "$rule" ]]; then
rule::apply "$rule" "$ip" "$full_name" || return 1
fi
# Auto-attach to identity and apply identity rule if set
identity::auto_attach "$full_name" "$resolved_type"
cmd::add::_apply_identity_rule "$full_name" "$ip" "$identity_name" "$effective_policy" "$rule"
peers::reload || return 1
log::wg_success "Client added: ${full_name} (${ip}) [${tunnel} tunnel]"
cmd::add::_show_result "$full_name" "$resolved_type" "$show_qr"
}
# ============================================
# Internal helpers
# ============================================
function cmd::add::_log_plan() {
local full_name="${1:-}" type="${2:-}" resolved_type="${3:-}" \
subnet_name="${4:-}" resolved_cidr="${5:-}" ip="${6:-}" \
tunnel="${7:-}" allowed_ips="${8:-}" rule="${9:-}" policy="${10:-}"
log::wg_add "Name: ${full_name}"
log::wg_add "Type: ${resolved_type}"
[[ -n "$subnet_name" ]] && log::wg_add "Subnet: ${subnet_name} (${resolved_cidr})"
log::wg_add "IP: ${ip}"
log::wg_add "Tunnel: ${tunnel} (${allowed_ips})"
log::wg_add "Endpoint: $(config::endpoint)"
log::wg_add "Rule: ${rule}"
log::wg_add "Policy: ${policy}"
}
function cmd::add::_assign_group() {
local full_name="${1:-}" group="${2:-}"
[[ -z "$group" ]] && return 0
if ! group::exists "$group"; then
log::wg_warning "Group '${group}' not found — skipping group assignment"
return 0
fi
group::add_peer "$group" "$full_name"
log::wg "Added to group: ${group}"
}
function cmd::add::_apply_identity_rule() {
local full_name="${1:-}" ip="${2:-}" identity_name="${3:-}" \
effective_policy="${4:-}" peer_rule="${5:-}"
[[ -z "$identity_name" ]] && return 0
local rules
rules=$(identity::rules "$identity_name")
if [[ -z "$rules" ]]; then
# No identity rules — warn if no peer rule either
if [[ -z "$peer_rule" ]]; then
policy::warn_no_rule "$full_name"
fi
return 0
fi
# Apply all identity rules
rule::_apply_identity_rule "$full_name" "$ip"
# Warn based on strict_rule
local strict
strict=$(identity::rule_flags "$identity_name" "strict_rule")
if [[ "$strict" == "true" ]]; then
local rule_list
rule_list=$(echo "$rules" | tr '\n' ',' | sed 's/,$//')
policy::warn_strict_rule "$identity_name" "$effective_policy" "$rule_list"
elif [[ -n "$peer_rule" ]]; then
local rule_list
rule_list=$(echo "$rules" | tr '\n' ',' | sed 's/,$//')
policy::warn_additive_rule "$identity_name" "$rule_list" "$peer_rule"
fi
}
function cmd::add::_show_result() {
local full_name="${1:-}" type="${2:-}" show_qr="${3:-false}"
if $show_qr || cmd::add::is_mobile "$type"; then
log::section "Client QR"
keys::qr "$full_name"
else
log::section "Client Config"
cat "$(ctx::clients)/${full_name}.conf"
fi
}