307 lines
9.5 KiB
Bash
307 lines
9.5 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 identity_rule
|
|
identity_rule=$(identity::rule "$identity_name") || true
|
|
|
|
[[ -z "$identity_rule" ]] && {
|
|
# No identity rule — warn if no peer rule either
|
|
if [[ -z "$peer_rule" ]]; then
|
|
policy::warn_no_rule "$full_name"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Apply identity rule
|
|
rule::exists "$identity_rule" && rule::apply "$identity_rule" "$ip" "$full_name" || true
|
|
|
|
# Warn based on strict_rule
|
|
if policy::strict_rule "$effective_policy"; then
|
|
policy::warn_strict_rule "$identity_name" "$effective_policy" "$identity_rule"
|
|
elif [[ -n "$peer_rule" && "$peer_rule" != "$identity_rule" ]]; then
|
|
policy::warn_additive_rule "$identity_name" "$identity_rule" "$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
|
|
}
|