finish base implementation
This commit is contained in:
parent
8bb1de4976
commit
de1a44a7e4
23 changed files with 958 additions and 787 deletions
|
|
@ -1,152 +1,144 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Lifecycle
|
# Lifecycle
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::add::on_load() {
|
function cmd::add::on_load() {
|
||||||
|
load_module subnet
|
||||||
|
load_module identity
|
||||||
|
|
||||||
flag::register --name
|
flag::register --name
|
||||||
|
flag::register --identity
|
||||||
flag::register --type
|
flag::register --type
|
||||||
flag::register --subtype
|
flag::register --subnet
|
||||||
flag::register --rule
|
flag::register --rule
|
||||||
flag::register --group
|
flag::register --group
|
||||||
flag::register --ip
|
flag::register --ip
|
||||||
flag::register --guest
|
|
||||||
flag::register --tunnel
|
flag::register --tunnel
|
||||||
flag::register --show-config
|
|
||||||
flag::register --show-qr
|
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
|
# Help
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::add::help() {
|
function cmd::add::help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: wgctl add --name <name> --type <type> [options]
|
Usage: wgctl add --name <name> --type <type> [options]
|
||||||
|
or: wgctl add --identity <identity> --type <type> [options]
|
||||||
|
|
||||||
Add a new WireGuard client.
|
Add a new WireGuard client.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--name <name> Client name (e.g. nuno)
|
--name <name> Client name (e.g. nuno) — combined with type: phone-nuno
|
||||||
--type <type> Device type: desktop, laptop, phone, tablet, guest
|
--identity <name> Identity name — auto-names peer with next available index
|
||||||
--subtype <subtype> Guest subtype: desktop, laptop, phone, tablet (mostly used for guest)
|
--type <type> Device type: desktop, laptop, phone, tablet, server, iot
|
||||||
--ip <ip> Override auto-assigned IP (optional)
|
--subnet <subnet> Subnet to allocate from (default: type-native)
|
||||||
--guest Shorthand for --type guest
|
--ip <ip> Override auto-assigned IP (optional)
|
||||||
--tunnel <mode> Tunnel mode: split|full (default: split)
|
--tunnel <mode> Tunnel mode: split|full (default: from subnet)
|
||||||
--rule <rule> Assign rule on creation (default: user, guest types: guest)
|
--rule <rule> Assign rule on creation (default: from subnet or user)
|
||||||
--group <group> Add to group on creation (group must exist)
|
--group <group> Add to group on creation (group must exist)
|
||||||
--show-config Shows the WireGuard peer config
|
--show-qr Show the WireGuard config as a QR code after creation
|
||||||
--show-qr Shows the WireGuard config in a QR Code
|
|
||||||
|
Subnet shorthands (equivalent to --subnet <name>):
|
||||||
Device Types and Subnets:
|
--guests, --servers, --iot, ... (see: wgctl subnet list)
|
||||||
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:
|
Examples:
|
||||||
wgctl add --name nuno --type phone
|
wgctl add --name nuno --type phone
|
||||||
wgctl add --name nuno --type laptop --ip 10.1.2.5
|
wgctl add --identity nuno --type phone # auto-names phone-nuno (or phone-nuno-2)
|
||||||
wgctl add --name nuno --type phone --tunnel full
|
wgctl add --identity nuno --type laptop
|
||||||
wgctl add --name guest1 --type phone --guest
|
wgctl add --name zephyr --type desktop --guests
|
||||||
wgctl add --name restricted --type desktop
|
wgctl add --identity zephyr --type desktop --guests
|
||||||
wgctl add --name dev --type laptop --rule dev-01
|
wgctl add --name visitor --type phone --guests --show-qr
|
||||||
wgctl add --name visitor --type guest --show-qr
|
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Validation
|
# Validation
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::add::validate() {
|
function cmd::add::_validate() {
|
||||||
local name="$1"
|
local name="$1" identity="$2" type="$3" ip="$4" tunnel="$5"
|
||||||
local type="$2"
|
|
||||||
local ip="$3"
|
if [[ -z "$name" && -z "$identity" ]]; then
|
||||||
local tunnel="$4"
|
log::error "Missing required flag: --name or --identity"
|
||||||
|
|
||||||
if [[ -z "$name" ]]; then
|
|
||||||
log::error "Missing required flag: --name"
|
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z "$type" ]]; then
|
if [[ -z "$type" ]]; then
|
||||||
log::error "Missing required flag: --type"
|
log::error "Missing required flag: --type"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! config::is_valid_type "$type"; then
|
if ! json::subnet_exists "$(ctx::subnets)" "$type" 2>/dev/null; then
|
||||||
log::error "Invalid device type: ${type}"
|
log::error "Unknown device type: '${type}'"
|
||||||
log::info "Valid types: $(config::device_types | tr ' ' ', ')"
|
log::info "Use 'wgctl subnet list' to see valid types and subnets"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$tunnel" && "$tunnel" != "split" && "$tunnel" != "full" ]]; then
|
if [[ -n "$tunnel" && "$tunnel" != "split" && "$tunnel" != "full" ]]; then
|
||||||
log::error "Invalid tunnel mode: ${tunnel} (use 'split' or 'full')"
|
log::error "Invalid tunnel mode: '${tunnel}' (use 'split' or 'full')"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$ip" ]]; then
|
if [[ -n "$ip" ]]; then
|
||||||
ip::require_valid "$ip"
|
ip::require_valid "$ip"
|
||||||
fi
|
fi
|
||||||
|
}
|
||||||
local full_name="${type}-${name}"
|
|
||||||
|
function cmd::add::_validate_not_exists() {
|
||||||
|
local full_name="$1"
|
||||||
if [[ -f "$(ctx::clients)/${full_name}.conf" ]]; then
|
if [[ -f "$(ctx::clients)/${full_name}.conf" ]]; then
|
||||||
log::error "Client already exists: ${full_name}"
|
log::error "Client already exists: ${full_name}"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Display helpers
|
# Display helpers
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::add::is_mobile() {
|
function cmd::add::is_mobile() {
|
||||||
local type="$1"
|
local type="$1"
|
||||||
[[ "$type" == "phone" || "$type" == "tablet" ]]
|
[[ "$type" == "phone" || "$type" == "tablet" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Run
|
# Run
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::add::run() {
|
function cmd::add::run() {
|
||||||
local name=""
|
local name="" identity="" type="" subnet_name="" rule="" \
|
||||||
local type=""
|
group="" ip="" tunnel="" show_qr=false
|
||||||
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
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--name) name="$2"; shift 2 ;;
|
--name) name="$2"; shift 2 ;;
|
||||||
--type) type="$2"; shift 2 ;;
|
--identity) identity="$2"; shift 2 ;;
|
||||||
--subtype) subtype="$2"; shift 2 ;;
|
--type) type="$2"; shift 2 ;;
|
||||||
--rule) rule="$2"; shift 2 ;;
|
--subnet) subnet_name="$2"; shift 2 ;;
|
||||||
--group) group="$2"; shift 2 ;;
|
--rule) rule="$2"; shift 2 ;;
|
||||||
--ip) ip="$2"; shift 2 ;;
|
--group) group="$2"; shift 2 ;;
|
||||||
--guest) guest=true; shift ;;
|
--ip) ip="$2"; shift 2 ;;
|
||||||
--tunnel) tunnel="$2"; shift 2 ;;
|
--tunnel) tunnel="$2"; shift 2 ;;
|
||||||
--show-config) show_config=true; shift ;;
|
--show-qr) show_qr=true; shift ;;
|
||||||
--show-qr) show_qr=true; shift ;;
|
--help) cmd::add::help; return ;;
|
||||||
--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"
|
log::error "Unknown flag: $1"
|
||||||
cmd::add::help
|
cmd::add::help
|
||||||
|
|
@ -154,82 +146,112 @@ function cmd::add::run() {
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
$guest && type="guest"
|
cmd::add::_validate "$name" "$identity" "$type" "$ip" "$tunnel" || return 1
|
||||||
|
|
||||||
local effective_type
|
# Resolve full peer name
|
||||||
effective_type=$(cmd::add::_resolve_type "$type" "$subtype") || return 1
|
local full_name
|
||||||
|
if [[ -n "$identity" ]]; then
|
||||||
local full_name="${type}-${name}"
|
full_name=$(identity::next_peer_name "$identity" "$type") || return 1
|
||||||
cmd::add::validate "$name" "$type" "$ip" "$tunnel" || return 1
|
log::info "Auto-named: ${full_name}"
|
||||||
|
|
||||||
[[ -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
|
else
|
||||||
echo "$type"
|
full_name="${type}-${name}"
|
||||||
fi
|
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 tunnel mode
|
||||||
|
[[ -z "$tunnel" ]] && tunnel=$(subnet::tunnel_mode "${subnet_name:-$type}" "$type")
|
||||||
|
|
||||||
|
# Resolve rule — subnet default_rule, then global default
|
||||||
|
if [[ -z "$rule" ]]; then
|
||||||
|
rule=$(subnet::default_rule "$subnet_name" "$resolved_type")
|
||||||
|
[[ -z "$rule" ]] && rule="user"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rule::exists "$rule" || { log::error "Rule not found: ${rule}"; return 1; }
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
keys::generate_pair "$full_name" || return 1
|
||||||
|
peers::create_client_config "$full_name" "$resolved_type" "$ip" "$allowed_ips" || return 1
|
||||||
|
|
||||||
|
# Write meta
|
||||||
|
peers::set_meta "$full_name" "type" "$resolved_type"
|
||||||
|
peers::set_meta "$full_name" "rule" "$rule"
|
||||||
|
if [[ -n "$subnet_name" ]]; then
|
||||||
|
peers::set_meta "$full_name" "subnet" "$subnet_name"
|
||||||
|
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
|
||||||
|
rule::apply "$rule" "$ip" || return 1
|
||||||
|
peers::reload || return 1
|
||||||
|
|
||||||
|
# Auto-attach to identity
|
||||||
|
identity::auto_attach "$full_name" "$resolved_type"
|
||||||
|
|
||||||
|
log::wg_success "Client added: ${full_name} (${ip}) [${tunnel} tunnel]"
|
||||||
|
cmd::add::_show_result "$full_name" "$resolved_type" "$show_qr"
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::add::_default_rule() {
|
# ============================================
|
||||||
local type="$1"
|
# Internal helpers
|
||||||
config::is_guest_type "$type" && echo "guest" || echo "user"
|
# ============================================
|
||||||
|
|
||||||
|
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:-}"
|
||||||
|
|
||||||
|
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}"
|
||||||
|
}
|
||||||
|
|
||||||
|
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::_show_result() {
|
function cmd::add::_show_result() {
|
||||||
local full_name="$1" display_type="$2"
|
local full_name="${1:-}" type="${2:-}" show_qr="${3:-false}"
|
||||||
if cmd::add::is_mobile "$display_type"; then
|
if $show_qr || cmd::add::is_mobile "$type"; then
|
||||||
|
log::section "Client QR"
|
||||||
keys::qr "$full_name"
|
keys::qr "$full_name"
|
||||||
else
|
else
|
||||||
log::section "Client Config"
|
log::section "Client Config"
|
||||||
cat "$(ctx::clients)/${full_name}.conf"
|
cat "$(ctx::clients)/${full_name}.conf"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
function cmd::block::on_load() {
|
function cmd::block::on_load() {
|
||||||
flag::register --name
|
flag::register --name
|
||||||
|
flag::register --identity
|
||||||
flag::register --type
|
flag::register --type
|
||||||
flag::register --force
|
flag::register --force
|
||||||
flag::register --quiet
|
flag::register --quiet
|
||||||
|
|
@ -14,8 +15,6 @@ function cmd::block::on_load() {
|
||||||
flag::register --proto
|
flag::register --proto
|
||||||
flag::register --subnet
|
flag::register --subnet
|
||||||
flag::register --block-name
|
flag::register --block-name
|
||||||
|
|
||||||
# System - NET Services
|
|
||||||
flag::register --service
|
flag::register --service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,6 +25,7 @@ function cmd::block::on_load() {
|
||||||
function cmd::block::help() {
|
function cmd::block::help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: wgctl block --name <name> [options]
|
Usage: wgctl block --name <name> [options]
|
||||||
|
or: wgctl block --identity <identity> [options]
|
||||||
|
|
||||||
Block a client entirely or restrict access to specific IPs/ports/subnets/services.
|
Block a client entirely or restrict access to specific IPs/ports/subnets/services.
|
||||||
All block rules are persisted and restored on WireGuard restart.
|
All block rules are persisted and restored on WireGuard restart.
|
||||||
|
|
@ -34,39 +34,38 @@ but have specific traffic restricted (shown as 'restricted' in list).
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--name <name> Client name (e.g. phone-nuno)
|
--name <name> Client name (e.g. phone-nuno)
|
||||||
|
--identity <name> Block all peers belonging to an identity
|
||||||
--type <type> Device type (optional, combines with --name)
|
--type <type> Device type (optional, combines with --name)
|
||||||
--ip <ip> Block access to specific IP (repeatable)
|
--ip <ip> Block access to specific IP (repeatable)
|
||||||
--subnet <cidr> Block access to subnet (repeatable)
|
--subnet <cidr> Block access to subnet (repeatable)
|
||||||
--port <ip:port:proto> Block specific port, e.g. 10.0.0.210:9000:tcp (repeatable)
|
--port <ip:port:proto> Block specific port (repeatable)
|
||||||
--service <name> Block a named service (e.g. proxmox, truenas:web-ui) (repeatable)
|
--service <name> Block a named service (repeatable)
|
||||||
--block-name <name> Optional name for this block rule
|
--block-name <name> Optional name for this block rule
|
||||||
--quiet Suppress output (used by group block)
|
--quiet Suppress output (used by group block)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
wgctl block --name phone-nuno
|
wgctl block --name phone-nuno
|
||||||
|
wgctl block --identity nuno
|
||||||
wgctl block --name nuno --type phone
|
wgctl block --name nuno --type phone
|
||||||
wgctl block --name phone-nuno --ip 10.0.0.210
|
wgctl block --name phone-nuno --ip 10.0.0.210
|
||||||
wgctl block --name phone-nuno --subnet 10.0.0.0/24
|
|
||||||
wgctl block --name phone-nuno --port 10.0.0.210:9000:tcp
|
|
||||||
wgctl block --name phone-nuno --service proxmox
|
wgctl block --name phone-nuno --service proxmox
|
||||||
wgctl block --name phone-nuno --service truenas:web-ui --block-name "no truenas ui"
|
|
||||||
wgctl ban --name phone-nuno
|
wgctl ban --name phone-nuno
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Block Run
|
# Run
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::block::run() {
|
function cmd::block::run() {
|
||||||
local name="" type="" block_name=""
|
local name="" identity="" type="" block_name=""
|
||||||
local ips=() subnets=() ports=() services=()
|
local ips=() subnets=() ports=() services=()
|
||||||
local quiet=false force=false
|
local quiet=false force=false
|
||||||
local changed=false
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--name) name="$2"; shift 2 ;;
|
--name) name="$2"; shift 2 ;;
|
||||||
|
--identity) identity="$2"; shift 2 ;;
|
||||||
--type) type="$2"; shift 2 ;;
|
--type) type="$2"; shift 2 ;;
|
||||||
--ip) ips+=("$2"); shift 2 ;;
|
--ip) ips+=("$2"); shift 2 ;;
|
||||||
--block-name) block_name="$2"; shift 2 ;;
|
--block-name) block_name="$2"; shift 2 ;;
|
||||||
|
|
@ -75,16 +74,27 @@ function cmd::block::run() {
|
||||||
--quiet) quiet=true; shift ;;
|
--quiet) quiet=true; shift ;;
|
||||||
--subnet) subnets+=("$2"); shift 2 ;;
|
--subnet) subnets+=("$2"); shift 2 ;;
|
||||||
--port) ports+=("$2"); shift 2 ;;
|
--port) ports+=("$2"); shift 2 ;;
|
||||||
--help) cmd::block::help; return ;;
|
--help) cmd::block::help; return ;;
|
||||||
*)
|
*)
|
||||||
log::error "Unknown flag: $1"
|
log::error "Unknown flag: $1"
|
||||||
cmd::block::help
|
cmd::block::help
|
||||||
return 1 ;;
|
return 1
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
[[ -z "$name" ]] && log::error "Missing required flag: --name" && \
|
# --identity: block all peers for this identity
|
||||||
cmd::block::help && return 1
|
if [[ -n "$identity" ]]; then
|
||||||
|
cmd::block::_block_identity "$identity" "$quiet" \
|
||||||
|
"${ips[@]+"${ips[@]}"}" || return 1
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -z "$name" ]] && {
|
||||||
|
log::error "Missing required flag: --name or --identity"
|
||||||
|
cmd::block::help
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||||
|
|
||||||
|
|
@ -113,6 +123,8 @@ function cmd::block::run() {
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local changed=false
|
||||||
|
|
||||||
# Block specific IPs
|
# Block specific IPs
|
||||||
for ip in "${ips[@]}"; do
|
for ip in "${ips[@]}"; do
|
||||||
ip::require_valid "$ip"
|
ip::require_valid "$ip"
|
||||||
|
|
@ -137,7 +149,7 @@ function cmd::block::run() {
|
||||||
fw::block_port "$client_ip" "$b_target" "$b_port" "${b_proto:-tcp}"
|
fw::block_port "$client_ip" "$b_target" "$b_port" "${b_proto:-tcp}"
|
||||||
block::add_rule "$name" "$client_ip" "port" "${block_name:-}" \
|
block::add_rule "$name" "$client_ip" "port" "${block_name:-}" \
|
||||||
"$b_target" "$b_port" "${b_proto:-tcp}"
|
"$b_target" "$b_port" "${b_proto:-tcp}"
|
||||||
$quiet || log::wg_success "${client_ip}:${b_port}:${b_proto:-tcp} has been blocked for ${name}"
|
$quiet || log::wg_success "${client_ip}:${b_port}:${b_proto:-tcp} has been blocked for ${name}"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Block services
|
# Block services
|
||||||
|
|
@ -149,13 +161,12 @@ function cmd::block::run() {
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if already blocked
|
|
||||||
local already_blocked=true
|
local already_blocked=true
|
||||||
for resolved in "${resolved_lines[@]}"; do
|
for resolved in "${resolved_lines[@]}"; do
|
||||||
if [[ "$resolved" == *:*:* ]]; then
|
if [[ "$resolved" == *:*:* ]]; then
|
||||||
local b_ip b_port b_proto
|
local b_ip b_port b_proto
|
||||||
IFS=":" read -r b_ip b_port b_proto <<< "$resolved"
|
IFS=":" read -r b_ip b_port b_proto <<< "$resolved"
|
||||||
fw::has_block_rule "$client_ip" "$b_ip" "$b_port" "$b_proto" 2>/dev/null || \
|
fw::has_block_rule "$client_ip" "$b_ip" "$b_port" "$b_proto" 2>/dev/null || \
|
||||||
{ already_blocked=false; break; }
|
{ already_blocked=false; break; }
|
||||||
else
|
else
|
||||||
fw::has_block_rule "$client_ip" "$resolved" 2>/dev/null || \
|
fw::has_block_rule "$client_ip" "$resolved" 2>/dev/null || \
|
||||||
|
|
@ -167,7 +178,7 @@ function cmd::block::run() {
|
||||||
$quiet || log::wg_warning "${svc} is already blocked for ${name}"
|
$quiet || log::wg_warning "${svc} is already blocked for ${name}"
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for resolved in "${resolved_lines[@]}"; do
|
for resolved in "${resolved_lines[@]}"; do
|
||||||
if [[ "$resolved" == *:*:* ]]; then
|
if [[ "$resolved" == *:*:* ]]; then
|
||||||
local b_ip b_port b_proto
|
local b_ip b_port b_proto
|
||||||
|
|
@ -186,10 +197,8 @@ function cmd::block::run() {
|
||||||
done
|
done
|
||||||
|
|
||||||
[[ ${#ips[@]} -gt 0 || ${#ports[@]} -gt 0 || \
|
[[ ${#ips[@]} -gt 0 || ${#ports[@]} -gt 0 || \
|
||||||
${#subnets[@]} -gt 0 ]] && changed=true
|
${#subnets[@]} -gt 0 ]] && changed=true
|
||||||
|
|
||||||
# Reapply in correct order: rule ACCEPT first, then peer DROP rules
|
|
||||||
# Only reorder if rules were actually added
|
|
||||||
if $changed; then
|
if $changed; then
|
||||||
local peer_rule
|
local peer_rule
|
||||||
peer_rule=$(peers::get_meta "$name" "rule")
|
peer_rule=$(peers::get_meta "$name" "rule")
|
||||||
|
|
@ -203,6 +212,45 @@ function cmd::block::run() {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Identity block
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::block::_block_identity() {
|
||||||
|
local identity_name="${1:-}" quiet="${2:-false}"
|
||||||
|
shift 2 || true
|
||||||
|
|
||||||
|
identity::require_exists "$identity_name" || return 1
|
||||||
|
identity::require_has_peers "$identity_name" || return 1
|
||||||
|
|
||||||
|
local peers blocked=0 failed=0
|
||||||
|
peers=$(identity::peers "$identity_name")
|
||||||
|
|
||||||
|
while IFS= read -r peer_name; do
|
||||||
|
[[ -z "$peer_name" ]] && continue
|
||||||
|
if peers::is_blocked "$peer_name"; then
|
||||||
|
$quiet || log::wg_warning "${peer_name} is already blocked"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
local client_ip
|
||||||
|
client_ip=$(peers::get_ip "$peer_name") || continue
|
||||||
|
monitor::update_endpoint_cache
|
||||||
|
if cmd::block::_block_all "$peer_name" "$client_ip" true; then
|
||||||
|
blocked=$(( blocked + 1 ))
|
||||||
|
else
|
||||||
|
failed=$(( failed + 1 ))
|
||||||
|
fi
|
||||||
|
done <<< "$peers"
|
||||||
|
|
||||||
|
log::ok "Blocked ${blocked} peer(s) for identity '${identity_name}'"
|
||||||
|
[[ $failed -gt 0 ]] && log::warn "${failed} peer(s) failed to block"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Helpers
|
||||||
|
# ============================================
|
||||||
|
|
||||||
function cmd::block::_get_endpoint() {
|
function cmd::block::_get_endpoint() {
|
||||||
local name="$1" public_key="$2"
|
local name="$1" public_key="$2"
|
||||||
local endpoint
|
local endpoint
|
||||||
|
|
@ -218,10 +266,7 @@ function cmd::block::_block_all() {
|
||||||
local client_ip="${2:?client_ip required}"
|
local client_ip="${2:?client_ip required}"
|
||||||
local quiet="${3:-false}"
|
local quiet="${3:-false}"
|
||||||
|
|
||||||
# Apply fw rules and remove from server
|
|
||||||
block::apply_full "$name" "$client_ip"
|
block::apply_full "$name" "$client_ip"
|
||||||
|
|
||||||
# Mark as directly blocked
|
|
||||||
block::set_direct "$name" "$client_ip" "true"
|
block::set_direct "$name" "$client_ip" "true"
|
||||||
|
|
||||||
$quiet || log::wg_success "${name} has been blocked."
|
$quiet || log::wg_success "${name} has been blocked."
|
||||||
|
|
|
||||||
|
|
@ -89,9 +89,6 @@ function cmd::inspect::_peer_info() {
|
||||||
local activity_current
|
local activity_current
|
||||||
activity_current=$(peers::format_activity_current "$public_key")
|
activity_current=$(peers::format_activity_current "$public_key")
|
||||||
|
|
||||||
local subtype
|
|
||||||
subtype=$(peers::get_meta "$name" "subtype")
|
|
||||||
|
|
||||||
local rule_extends=""
|
local rule_extends=""
|
||||||
if [[ -n "$rule" ]]; then
|
if [[ -n "$rule" ]]; then
|
||||||
local rule_file
|
local rule_file
|
||||||
|
|
@ -117,7 +114,7 @@ function cmd::inspect::_peer_info() {
|
||||||
printf "\n"
|
printf "\n"
|
||||||
ui::row "Name" "$name" "${INSPECT_LABEL_WIDTH}"
|
ui::row "Name" "$name" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "IP" "$ip" "${INSPECT_LABEL_WIDTH}"
|
ui::row "IP" "$ip" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Type" "$(peers::display_type "$type" "$subtype")" "${INSPECT_LABEL_WIDTH}"
|
ui::row "Type" "$(peers::display_type "$type")" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Rule" "$rule_display" "${INSPECT_LABEL_WIDTH}"
|
ui::row "Rule" "$rule_display" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Status" "$(echo -e "$status")" "${INSPECT_LABEL_WIDTH}"
|
ui::row "Status" "$(echo -e "$status")" "${INSPECT_LABEL_WIDTH}"
|
||||||
ui::row "Endpoint" "${endpoint:-—}" "${INSPECT_LABEL_WIDTH}"
|
ui::row "Endpoint" "${endpoint:-—}" "${INSPECT_LABEL_WIDTH}"
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ function cmd::list::on_load() {
|
||||||
flag::register --type
|
flag::register --type
|
||||||
flag::register --rule
|
flag::register --rule
|
||||||
flag::register --group
|
flag::register --group
|
||||||
|
flag::register --identity
|
||||||
flag::register --online
|
flag::register --online
|
||||||
flag::register --offline
|
flag::register --offline
|
||||||
flag::register --restricted
|
flag::register --restricted
|
||||||
|
|
@ -28,9 +29,10 @@ Usage: wgctl list [options]
|
||||||
List all WireGuard clients.
|
List all WireGuard clients.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--type <type> Filter by device type (desktop, laptop, phone, tablet, guest)
|
--type <type> Filter by device type (desktop, laptop, phone, tablet)
|
||||||
--rule <rule> Filter by assigned rule
|
--rule <rule> Filter by assigned rule
|
||||||
--group <group> Filter by group membership
|
--group <group> Filter by group membership
|
||||||
|
--identity <name> Filter by identity (show all peers for an identity)
|
||||||
--online Show only connected clients
|
--online Show only connected clients
|
||||||
--offline Show only disconnected clients
|
--offline Show only disconnected clients
|
||||||
--blocked Show only fully blocked clients (removed from WireGuard)
|
--blocked Show only fully blocked clients (removed from WireGuard)
|
||||||
|
|
@ -47,9 +49,10 @@ Status values:
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
wgctl list
|
wgctl list
|
||||||
wgctl list --type guest
|
wgctl list --type phone
|
||||||
wgctl list --rule user
|
wgctl list --rule user
|
||||||
wgctl list --group family
|
wgctl list --group family
|
||||||
|
wgctl list --identity nuno
|
||||||
wgctl list --online
|
wgctl list --online
|
||||||
wgctl list --blocked
|
wgctl list --blocked
|
||||||
wgctl list --restricted
|
wgctl list --restricted
|
||||||
|
|
@ -87,8 +90,8 @@ function cmd::list::_render_footer() {
|
||||||
function cmd::list::_render_summary() {
|
function cmd::list::_render_summary() {
|
||||||
local group_summary="${1:-}"
|
local group_summary="${1:-}"
|
||||||
local -n _rule_counts="$2"
|
local -n _rule_counts="$2"
|
||||||
|
local filter_desc="${3:-}"
|
||||||
|
|
||||||
# Count total from rule_counts (only filtered peers)
|
|
||||||
local total=0
|
local total=0
|
||||||
for r in "${!_rule_counts[@]}"; do
|
for r in "${!_rule_counts[@]}"; do
|
||||||
(( total += _rule_counts[$r] )) || true
|
(( total += _rule_counts[$r] )) || true
|
||||||
|
|
@ -108,7 +111,7 @@ function cmd::list::_render_summary() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Detail Card (show_client)
|
# Detail Card
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::list::show_client() {
|
function cmd::list::show_client() {
|
||||||
|
|
@ -130,11 +133,10 @@ function cmd::list::show_client() {
|
||||||
local public_key
|
local public_key
|
||||||
public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
|
public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
# Meta type is authoritative; IP reverse lookup is fallback for pre-migration peers
|
||||||
local type
|
local type
|
||||||
type=$(peers::get_type_from_ip "$ip")
|
type=$(peers::get_meta "$name" "type" 2>/dev/null)
|
||||||
|
[[ -z "$type" ]] && type=$(peers::get_type_from_ip "$ip")
|
||||||
local subtype
|
|
||||||
subtype=$(peers::get_meta "$name" "subtype")
|
|
||||||
|
|
||||||
local endpoint="—"
|
local endpoint="—"
|
||||||
local ep
|
local ep
|
||||||
|
|
@ -155,7 +157,7 @@ function cmd::list::show_client() {
|
||||||
last_ts=$(monitor::last_attempt "$name")
|
last_ts=$(monitor::last_attempt "$name")
|
||||||
|
|
||||||
local status
|
local status
|
||||||
status=$(peers::format_status "$name" "$public_key" \
|
status=$(peers::format_status_verbose "$name" "$public_key" \
|
||||||
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||||
|
|
||||||
local last_seen
|
local last_seen
|
||||||
|
|
@ -174,7 +176,7 @@ function cmd::list::show_client() {
|
||||||
|
|
||||||
ui::section "Client: ${name}"
|
ui::section "Client: ${name}"
|
||||||
ui::row "IP" "$ip"
|
ui::row "IP" "$ip"
|
||||||
ui::row "Type" "$(peers::display_type "$type" "$subtype")"
|
ui::row "Type" "$(peers::display_type "$type")"
|
||||||
ui::row "Status" "$(echo -e "$status")"
|
ui::row "Status" "$(echo -e "$status")"
|
||||||
ui::row "Endpoint" "$endpoint"
|
ui::row "Endpoint" "$endpoint"
|
||||||
ui::row "Last seen" "$last_seen"
|
ui::row "Last seen" "$last_seen"
|
||||||
|
|
@ -197,7 +199,7 @@ function cmd::list::show_client() {
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::list::run() {
|
function cmd::list::run() {
|
||||||
local filter_type="" filter_rule="" filter_group=""
|
local filter_type="" filter_rule="" filter_group="" filter_identity=""
|
||||||
local online_only=false offline_only=false
|
local online_only=false offline_only=false
|
||||||
local restricted_only=false blocked_only=false allowed_only=false
|
local restricted_only=false blocked_only=false allowed_only=false
|
||||||
local detailed=false single_name=""
|
local detailed=false single_name=""
|
||||||
|
|
@ -207,6 +209,7 @@ function cmd::list::run() {
|
||||||
--type) filter_type="$2"; shift 2 ;;
|
--type) filter_type="$2"; shift 2 ;;
|
||||||
--rule) filter_rule="$2"; shift 2 ;;
|
--rule) filter_rule="$2"; shift 2 ;;
|
||||||
--group) filter_group="$2"; shift 2 ;;
|
--group) filter_group="$2"; shift 2 ;;
|
||||||
|
--identity) filter_identity="$2"; shift 2 ;;
|
||||||
--online) online_only=true; shift ;;
|
--online) online_only=true; shift ;;
|
||||||
--offline) offline_only=true; shift ;;
|
--offline) offline_only=true; shift ;;
|
||||||
--restricted) restricted_only=true; shift ;;
|
--restricted) restricted_only=true; shift ;;
|
||||||
|
|
@ -223,7 +226,6 @@ function cmd::list::run() {
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# Single detail card
|
|
||||||
if [[ -n "$single_name" ]]; then
|
if [[ -n "$single_name" ]]; then
|
||||||
cmd::list::show_client "$single_name"
|
cmd::list::show_client "$single_name"
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -237,21 +239,17 @@ function cmd::list::run() {
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Precompute everything ──────────────────
|
|
||||||
cmd::list::_precompute_all
|
cmd::list::_precompute_all
|
||||||
|
|
||||||
# ── Detailed mode ──────────────────────────
|
|
||||||
if $detailed; then
|
if $detailed; then
|
||||||
log::section "WireGuard Clients"
|
log::section "WireGuard Clients"
|
||||||
cmd::list::_iter_confs "$filter_type" cmd::list::_show_client_safe
|
cmd::list::_iter_confs "$filter_type" cmd::list::_show_client_safe
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Build filter description ───────────────
|
|
||||||
local filter_desc=""
|
local filter_desc=""
|
||||||
cmd::list::_build_filter_desc
|
cmd::list::_build_filter_desc
|
||||||
|
|
||||||
# ── Table view ─────────────────────────────
|
|
||||||
declare -A rule_counts=() group_counts=()
|
declare -A rule_counts=() group_counts=()
|
||||||
_list_header_printed=false
|
_list_header_printed=false
|
||||||
|
|
||||||
|
|
@ -267,10 +265,12 @@ function cmd::list::run() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Iteration
|
||||||
|
# ============================================
|
||||||
|
|
||||||
function cmd::list::_iter_confs() {
|
function cmd::list::_iter_confs() {
|
||||||
# Usage: cmd::list::_iter_confs <filter_type> <callback>
|
local filter_type="$1" callback="$2"
|
||||||
local filter_type="$1"
|
|
||||||
local callback="$2"
|
|
||||||
local dir
|
local dir
|
||||||
dir="$(ctx::clients)"
|
dir="$(ctx::clients)"
|
||||||
|
|
||||||
|
|
@ -278,17 +278,27 @@ function cmd::list::_iter_confs() {
|
||||||
[[ -f "$conf" ]] || continue
|
[[ -f "$conf" ]] || continue
|
||||||
local client_name
|
local client_name
|
||||||
client_name=$(basename "$conf" .conf)
|
client_name=$(basename "$conf" .conf)
|
||||||
local ip="${p_ips[$client_name]:-}"
|
|
||||||
if [[ -z "$ip" ]]; then
|
# Identity filter — skip peers not in the identity set
|
||||||
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
if [[ ${#p_identity_filter[@]} -gt 0 && \
|
||||||
|
-z "${p_identity_filter[$client_name]:-}" ]]; then
|
||||||
|
continue
|
||||||
fi
|
fi
|
||||||
local type
|
|
||||||
type=$(peers::get_type_from_ip "$ip")
|
local ip="${p_ips[$client_name]:-}"
|
||||||
|
[[ -z "$ip" ]] && ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
||||||
|
|
||||||
|
# p_types is authoritative — set during precompute from meta + IP fallback
|
||||||
|
local type="${p_types[$client_name]:-unknown}"
|
||||||
[[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
|
[[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
|
||||||
|
|
||||||
"$callback" "$client_name" "$ip" "$type"
|
"$callback" "$client_name" "$ip" "$type"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Row rendering
|
||||||
|
# ============================================
|
||||||
|
|
||||||
function cmd::list::_render_row() {
|
function cmd::list::_render_row() {
|
||||||
local client_name="$1" ip="$2" type="$3"
|
local client_name="$1" ip="$2" type="$3"
|
||||||
|
|
@ -299,9 +309,8 @@ function cmd::list::_render_row() {
|
||||||
local is_restricted="${p_restricted[$client_name]:-false}"
|
local is_restricted="${p_restricted[$client_name]:-false}"
|
||||||
local last_ts="${p_last_ts[$client_name]:-}"
|
local last_ts="${p_last_ts[$client_name]:-}"
|
||||||
|
|
||||||
# Apply status filters
|
if $online_only; then peers::is_online "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
|
||||||
if $online_only; then peers::is_online "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
|
if $offline_only; then peers::is_offline "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
|
||||||
if $offline_only; then peers::is_offline "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
|
|
||||||
if $restricted_only && [[ "$is_restricted" != "true" ]]; then return 0; fi
|
if $restricted_only && [[ "$is_restricted" != "true" ]]; then return 0; fi
|
||||||
if $blocked_only && [[ "$is_blocked" != "true" ]]; then return 0; fi
|
if $blocked_only && [[ "$is_blocked" != "true" ]]; then return 0; fi
|
||||||
if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
|
if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
|
||||||
|
|
@ -312,18 +321,16 @@ function cmd::list::_render_row() {
|
||||||
[[ "$all_groups" != *"$filter_group"* ]] && return 0
|
[[ "$all_groups" != *"$filter_group"* ]] && return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Format display values
|
|
||||||
local status last_seen display_type rule group_display
|
local status last_seen display_type rule group_display
|
||||||
status=$(peers::format_status "$client_name" "$pubkey" \
|
status=$(peers::format_status_verbose "$client_name" "$pubkey" \
|
||||||
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||||
last_seen=$(peers::format_last_seen "$client_name" "$pubkey" \
|
last_seen=$(peers::format_last_seen "$client_name" "$pubkey" \
|
||||||
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
||||||
display_type=$(peers::display_type "$type" "${p_subtypes[$client_name]:-}")
|
display_type=$(peers::display_type "$type")
|
||||||
rule="${p_rules[$client_name]:-—}"
|
rule="${p_rules[$client_name]:-—}"
|
||||||
|
|
||||||
if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi
|
if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi
|
||||||
|
|
||||||
# Print header on first match
|
|
||||||
if [[ "${_list_header_printed:-false}" == "false" ]]; then
|
if [[ "${_list_header_printed:-false}" == "false" ]]; then
|
||||||
log::section "WireGuard Clients"
|
log::section "WireGuard Clients"
|
||||||
cmd::list::_render_header $has_groups
|
cmd::list::_render_header $has_groups
|
||||||
|
|
@ -336,7 +343,6 @@ function cmd::list::_render_row() {
|
||||||
padded_status=$(ui::pad_status "$status" 25)
|
padded_status=$(ui::pad_status "$status" 25)
|
||||||
|
|
||||||
if $has_groups; then
|
if $has_groups; then
|
||||||
# Use main group for display, fall back to first group, then —
|
|
||||||
local main_group="${p_main_groups[$client_name]:-}"
|
local main_group="${p_main_groups[$client_name]:-}"
|
||||||
if [[ -n "$main_group" ]]; then
|
if [[ -n "$main_group" ]]; then
|
||||||
group_display="$main_group"
|
group_display="$main_group"
|
||||||
|
|
@ -364,22 +370,28 @@ function cmd::list::_render_row() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Private helpers
|
# Precompute
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::list::_precompute_all() {
|
function cmd::list::_precompute_all() {
|
||||||
# Peer data
|
# Peer data — field 4 is 'type' from peer_data_v2
|
||||||
declare -gA p_ips=() p_rules=() p_subtypes=() p_last_ts=() p_last_evt=() p_main_groups=()
|
declare -gA p_ips=() p_rules=() p_types=() p_last_ts=() p_last_evt=() p_main_groups=()
|
||||||
while IFS="|" read -r name ip rule subtype last_ts last_evt main_group; do
|
while IFS="|" read -r name ip rule type last_ts last_evt main_group; do
|
||||||
[[ -z "$name" ]] && continue
|
[[ -z "$name" ]] && continue
|
||||||
p_ips["$name"]="$ip"
|
p_ips["$name"]="$ip"
|
||||||
p_rules["$name"]="${rule:-—}"
|
p_rules["$name"]="${rule:-—}"
|
||||||
p_subtypes["$name"]="$subtype"
|
p_types["$name"]="${type:-}"
|
||||||
p_last_ts["$name"]="$last_ts"
|
p_last_ts["$name"]="$last_ts"
|
||||||
p_last_evt["$name"]="$last_evt"
|
p_last_evt["$name"]="$last_evt"
|
||||||
p_main_groups["$name"]="${main_group:-}"
|
p_main_groups["$name"]="${main_group:-}"
|
||||||
done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)")
|
done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)")
|
||||||
|
|
||||||
|
# Fill type from IP for peers missing meta type (pre-migration peers)
|
||||||
|
for name in "${!p_ips[@]}"; do
|
||||||
|
[[ -n "${p_types[$name]:-}" ]] && continue
|
||||||
|
p_types["$name"]=$(peers::get_type_from_ip "${p_ips[$name]}")
|
||||||
|
done
|
||||||
|
|
||||||
# WireGuard handshakes + endpoints
|
# WireGuard handshakes + endpoints
|
||||||
declare -gA wg_handshakes=() wg_endpoints=()
|
declare -gA wg_handshakes=() wg_endpoints=()
|
||||||
while IFS=$'\t' read -r pubkey ts; do
|
while IFS=$'\t' read -r pubkey ts; do
|
||||||
|
|
@ -417,6 +429,19 @@ function cmd::list::_precompute_all() {
|
||||||
done < <(json::peer_group_map "$groups_dir")
|
done < <(json::peer_group_map "$groups_dir")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Resolve identity filter into a peer set
|
||||||
|
declare -gA p_identity_filter=()
|
||||||
|
if [[ -n "$filter_identity" ]]; then
|
||||||
|
identity::require_exists "$filter_identity" || return 1
|
||||||
|
while IFS= read -r peer_name; do
|
||||||
|
[[ -n "$peer_name" ]] && p_identity_filter["$peer_name"]=1
|
||||||
|
done < <(identity::peers "$filter_identity")
|
||||||
|
if [[ ${#p_identity_filter[@]} -eq 0 ]]; then
|
||||||
|
log::wg_warning "Identity '${filter_identity}' has no peers"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Transfer/activity data — keyed by pubkey
|
# Transfer/activity data — keyed by pubkey
|
||||||
declare -gA p_rx=() p_tx=() p_activity=()
|
declare -gA p_rx=() p_tx=() p_activity=()
|
||||||
while IFS="|" read -r pubkey rx tx level; do
|
while IFS="|" read -r pubkey rx tx level; do
|
||||||
|
|
@ -427,65 +452,6 @@ function cmd::list::_precompute_all() {
|
||||||
done < <(json::peer_transfer "$(config::interface)")
|
done < <(json::peer_transfer "$(config::interface)")
|
||||||
}
|
}
|
||||||
|
|
||||||
# function cmd::list::_precompute_all() {
|
|
||||||
# # Peer data
|
|
||||||
# declare -gA p_ips=() p_rules=() p_subtypes=() p_last_ts=() p_last_evt=()
|
|
||||||
# while IFS="|" read -r name ip rule subtype last_ts last_evt; do
|
|
||||||
# [[ -z "$name" ]] && continue
|
|
||||||
# p_ips["$name"]="$ip"
|
|
||||||
# p_rules["$name"]="${rule:-—}"
|
|
||||||
# p_subtypes["$name"]="$subtype"
|
|
||||||
# p_last_ts["$name"]="$last_ts"
|
|
||||||
# p_last_evt["$name"]="$last_evt"
|
|
||||||
# done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)")
|
|
||||||
|
|
||||||
# # WireGuard handshakes + endpoints
|
|
||||||
# declare -gA wg_handshakes=() wg_endpoints=()
|
|
||||||
# while IFS=$'\t' read -r pubkey ts; do
|
|
||||||
# [[ -n "$pubkey" ]] && wg_handshakes["$pubkey"]="$ts"
|
|
||||||
# done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
|
|
||||||
# while IFS=$'\t' read -r pubkey endpoint; do
|
|
||||||
# [[ -n "$pubkey" ]] && wg_endpoints["$pubkey"]="$endpoint"
|
|
||||||
# done < <(wg show "$(config::interface)" endpoints 2>/dev/null)
|
|
||||||
|
|
||||||
# # Block/restricted status
|
|
||||||
# declare -gA p_blocked=() p_restricted=()
|
|
||||||
# cmd::list::_precompute_block_status p_blocked p_restricted
|
|
||||||
|
|
||||||
# # Public keys
|
|
||||||
# declare -gA p_pubkeys=()
|
|
||||||
# local dir
|
|
||||||
# dir="$(ctx::clients)"
|
|
||||||
# for kf in "${dir}"/*_public.key; do
|
|
||||||
# [[ -f "$kf" ]] || continue
|
|
||||||
# local kname
|
|
||||||
# kname=$(basename "$kf" _public.key)
|
|
||||||
# p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "")
|
|
||||||
# done
|
|
||||||
|
|
||||||
# # Groups
|
|
||||||
# has_groups=false
|
|
||||||
# declare -gA peer_group_map=()
|
|
||||||
# local groups_dir
|
|
||||||
# groups_dir="$(ctx::groups)"
|
|
||||||
# local group_files=("${groups_dir}"/*.group)
|
|
||||||
# if [[ -f "${group_files[0]}" ]]; then
|
|
||||||
# has_groups=true
|
|
||||||
# while IFS=":" read -r peer_name group_name; do
|
|
||||||
# [[ -n "$peer_name" ]] && peer_group_map["$peer_name"]="$group_name"
|
|
||||||
# done < <(json::peer_group_map "$groups_dir")
|
|
||||||
# fi
|
|
||||||
|
|
||||||
# # Transfer/activity data — keyed by pubkey
|
|
||||||
# declare -gA p_rx=() p_tx=() p_activity=()
|
|
||||||
# while IFS="|" read -r pubkey rx tx level; do
|
|
||||||
# [[ -z "$pubkey" ]] && continue
|
|
||||||
# p_rx["$pubkey"]="$rx"
|
|
||||||
# p_tx["$pubkey"]="$tx"
|
|
||||||
# p_activity["$pubkey"]="$level"
|
|
||||||
# done < <(json::peer_transfer "$(config::interface)")
|
|
||||||
# }
|
|
||||||
|
|
||||||
function cmd::list::_precompute_block_status() {
|
function cmd::list::_precompute_block_status() {
|
||||||
local -n _blocked="$1"
|
local -n _blocked="$1"
|
||||||
local -n _restricted="$2"
|
local -n _restricted="$2"
|
||||||
|
|
@ -500,7 +466,6 @@ function cmd::list::_precompute_block_status() {
|
||||||
_restricted["$name"]=false
|
_restricted["$name"]=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Blocked = removed from WG server
|
|
||||||
local pubkey
|
local pubkey
|
||||||
pubkey=$(keys::public "$name" 2>/dev/null || echo "")
|
pubkey=$(keys::public "$name" 2>/dev/null || echo "")
|
||||||
if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then
|
if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then
|
||||||
|
|
@ -511,11 +476,16 @@ function cmd::list::_precompute_block_status() {
|
||||||
done < <(peers::all)
|
done < <(peers::all)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Filter helpers
|
||||||
|
# ============================================
|
||||||
|
|
||||||
function cmd::list::_build_filter_desc() {
|
function cmd::list::_build_filter_desc() {
|
||||||
filter_desc=""
|
filter_desc=""
|
||||||
[[ -n "$filter_type" ]] && filter_desc+="type=${filter_type} "
|
[[ -n "$filter_type" ]] && filter_desc+="type=${filter_type} "
|
||||||
[[ -n "$filter_rule" ]] && filter_desc+="rule=${filter_rule} "
|
[[ -n "$filter_rule" ]] && filter_desc+="rule=${filter_rule} "
|
||||||
[[ -n "$filter_group" ]] && filter_desc+="group=${filter_group} "
|
[[ -n "$filter_group" ]] && filter_desc+="group=${filter_group} "
|
||||||
|
[[ -n "$filter_identity" ]] && filter_desc+="identity=${filter_identity} "
|
||||||
$online_only && filter_desc+="online "
|
$online_only && filter_desc+="online "
|
||||||
$offline_only && filter_desc+="offline "
|
$offline_only && filter_desc+="offline "
|
||||||
$blocked_only && filter_desc+="blocked "
|
$blocked_only && filter_desc+="blocked "
|
||||||
|
|
|
||||||
|
|
@ -58,5 +58,6 @@ function cmd::qr::run() {
|
||||||
|
|
||||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||||
|
|
||||||
|
log::section "Client QR: ${name}"
|
||||||
keys::qr "$name"
|
keys::qr "$name"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ function cmd::rename::run() {
|
||||||
|
|
||||||
# Update identity entry after successful rename
|
# Update identity entry after successful rename
|
||||||
identity::rename_peer "$name" "$new_name"
|
identity::rename_peer "$name" "$new_name"
|
||||||
|
|
||||||
log::wg_success "Client renamed: ${name} → ${new_name}"
|
log::wg_success "Client renamed: ${name} → ${new_name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,9 +105,5 @@ function cmd::rename::_rename_files() {
|
||||||
sed -i "s/^# ${name}$/# ${new_name}/" "$(config::config_file)"
|
sed -i "s/^# ${name}$/# ${new_name}/" "$(config::config_file)"
|
||||||
|
|
||||||
block::rename "$name" "$new_name"
|
block::rename "$name" "$new_name"
|
||||||
|
peers::rename_meta "$name" "$new_name"
|
||||||
local old_meta new_meta
|
|
||||||
old_meta=$(peers::meta_path "$name")
|
|
||||||
new_meta=$(peers::meta_path "$new_name")
|
|
||||||
[[ -f "$old_meta" ]] && mv "$old_meta" "$new_meta"
|
|
||||||
}
|
}
|
||||||
|
|
@ -7,11 +7,13 @@ function cmd::test::section_destructive() {
|
||||||
test::section "Destructive (modifying state)"
|
test::section "Destructive (modifying state)"
|
||||||
|
|
||||||
# Cleanup from any previous failed run
|
# Cleanup from any previous failed run
|
||||||
"$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
|
"$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
|
||||||
|
"$WGCTL_BINARY" remove --name laptop-testunit2 --force > /dev/null 2>&1 || true
|
||||||
|
"$WGCTL_BINARY" remove --name laptop-testunit2b --force > /dev/null 2>&1 || true
|
||||||
"$WGCTL_BINARY" group remove --name testgroup --force > /dev/null 2>&1 || true
|
"$WGCTL_BINARY" group remove --name testgroup --force > /dev/null 2>&1 || true
|
||||||
"$WGCTL_BINARY" group remove --name testgroup2 --force > /dev/null 2>&1 || true
|
"$WGCTL_BINARY" group remove --name testgroup2 --force > /dev/null 2>&1 || true
|
||||||
"$WGCTL_BINARY" net rm --name test-block-svc --force > /dev/null 2>&1 || true
|
"$WGCTL_BINARY" net rm --name test-block-svc --force > /dev/null 2>&1 || true
|
||||||
"$WGCTL_BINARY" unblock --name phone-testunit > /dev/null 2>&1 || true
|
"$WGCTL_BINARY" unblock --name phone-testunit > /dev/null 2>&1 || true
|
||||||
|
|
||||||
cmd::test::_destructive_peer
|
cmd::test::_destructive_peer
|
||||||
cmd::test::_destructive_block_unblock
|
cmd::test::_destructive_block_unblock
|
||||||
|
|
@ -28,15 +30,15 @@ function cmd::test::_destructive_peer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::_destructive_block_unblock() {
|
function cmd::test::_destructive_block_unblock() {
|
||||||
cmd::test::run_cmd "block peer" "blocked" block --name phone-testunit
|
cmd::test::run_cmd "block peer" "blocked" block --name phone-testunit
|
||||||
cmd::test::run_cmd "list shows blocked" "blocked" list --blocked
|
cmd::test::run_cmd "list shows blocked" "phone-testunit" list --blocked
|
||||||
cmd::test::run_cmd "unblock peer" "unblocked" unblock --name phone-testunit
|
cmd::test::run_cmd "unblock peer" "unblocked" unblock --name phone-testunit
|
||||||
|
|
||||||
cmd::test::run_cmd "block peer --ip" "blocked for" \
|
cmd::test::run_cmd "block peer --ip" "blocked for" \
|
||||||
block --name phone-testunit --ip 10.0.0.99
|
block --name phone-testunit --ip 10.0.0.99
|
||||||
cmd::test::run_cmd "list shows restricted" "restricted" \
|
cmd::test::run_cmd "list shows restricted" "restricted" \
|
||||||
list --name phone-testunit
|
list --name phone-testunit
|
||||||
cmd::test::run_cmd "unblock peer --ip" "unblocked" \
|
cmd::test::run_cmd "unblock peer --ip" "unblocked" \
|
||||||
unblock --name phone-testunit --ip 10.0.0.99
|
unblock --name phone-testunit --ip 10.0.0.99
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,12 +65,11 @@ function cmd::test::_destructive_rule() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::_destructive_groups() {
|
function cmd::test::_destructive_groups() {
|
||||||
cmd::test::run_cmd "group add" "created" group add --name testgroup --desc "Test group"
|
cmd::test::run_cmd "group add" "created" group add --name testgroup --desc "Test group"
|
||||||
cmd::test::run_cmd "group peer add" "Added" group peer add --name testgroup --peer phone-testunit
|
cmd::test::run_cmd "group peer add" "Added" group peer add --name testgroup --peer phone-testunit
|
||||||
cmd::test::run_cmd "group block" "blocked" group block --name testgroup
|
cmd::test::run_cmd "group block" "blocked" group block --name testgroup
|
||||||
cmd::test::run_cmd "group unblock" "unblocked" group unblock --name testgroup
|
cmd::test::run_cmd "group unblock" "unblocked" group unblock --name testgroup
|
||||||
|
|
||||||
# M:N group block tracking
|
|
||||||
"$WGCTL_BINARY" group add --name testgroup2 --desc "Test group 2" > /dev/null 2>&1
|
"$WGCTL_BINARY" group add --name testgroup2 --desc "Test group 2" > /dev/null 2>&1
|
||||||
"$WGCTL_BINARY" group peer add --name testgroup2 --peer phone-testunit > /dev/null 2>&1
|
"$WGCTL_BINARY" group peer add --name testgroup2 --peer phone-testunit > /dev/null 2>&1
|
||||||
|
|
||||||
|
|
@ -76,12 +77,11 @@ function cmd::test::_destructive_groups() {
|
||||||
cmd::test::run_cmd "group block second" "blocked" group block --name testgroup2
|
cmd::test::run_cmd "group block second" "blocked" group block --name testgroup2
|
||||||
|
|
||||||
"$WGCTL_BINARY" group unblock --name testgroup > /dev/null 2>&1
|
"$WGCTL_BINARY" group unblock --name testgroup > /dev/null 2>&1
|
||||||
cmd::test::run_cmd "peer stays blocked after partial unblock" "blocked" list --blocked
|
cmd::test::run_cmd "peer stays blocked after partial unblock" "phone-testunit" list --blocked
|
||||||
|
|
||||||
"$WGCTL_BINARY" group unblock --name testgroup2 > /dev/null 2>&1
|
"$WGCTL_BINARY" group unblock --name testgroup2 > /dev/null 2>&1
|
||||||
cmd::test::run_cmd "peer unblocked after all groups unblock" "phone-testunit" list --allowed
|
cmd::test::run_cmd "peer unblocked after all groups unblock" "phone-testunit" list --allowed
|
||||||
|
|
||||||
# Direct block overrides group block
|
|
||||||
"$WGCTL_BINARY" group block --name testgroup > /dev/null 2>&1
|
"$WGCTL_BINARY" group block --name testgroup > /dev/null 2>&1
|
||||||
cmd::test::run_cmd "direct unblock overrides group block" "unblocked" unblock --name phone-testunit
|
cmd::test::run_cmd "direct unblock overrides group block" "unblocked" unblock --name phone-testunit
|
||||||
|
|
||||||
|
|
@ -92,22 +92,33 @@ function cmd::test::_destructive_groups() {
|
||||||
function cmd::test::_destructive_identity() {
|
function cmd::test::_destructive_identity() {
|
||||||
test::section "Destructive: identity auto-attach/detach"
|
test::section "Destructive: identity auto-attach/detach"
|
||||||
|
|
||||||
# Add verifies auto-attach
|
# Cleanup from any previous failed run
|
||||||
|
"$WGCTL_BINARY" remove --name laptop-testunit2 --force > /dev/null 2>&1 || true
|
||||||
|
"$WGCTL_BINARY" remove --name laptop-testunit2b --force > /dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Add — verify auto-attach to identity "testunit2"
|
||||||
cmd::test::run_cmd "add attaches to identity" "added" \
|
cmd::test::run_cmd "add attaches to identity" "added" \
|
||||||
add --name testunit2 --type laptop
|
add --name testunit2 --type laptop
|
||||||
|
|
||||||
cmd::test::run_cmd "identity shows testunit2" "testunit2" \
|
cmd::test::run_cmd "identity created for testunit2" "laptop-testunit2" \
|
||||||
identity show --name testunit
|
identity show --name testunit2
|
||||||
|
|
||||||
# Rename verifies identity::rename_peer
|
# Rename — verify identity::rename_peer moves peer to new identity "testunit2b"
|
||||||
cmd::test::run_cmd "rename updates identity" "renamed" \
|
cmd::test::run_cmd "rename peer" "renamed" \
|
||||||
rename --name laptop-testunit2 --new-name laptop-testunit2b
|
rename --name laptop-testunit2 --new-name laptop-testunit2b
|
||||||
|
|
||||||
cmd::test::run_cmd "identity reflects rename" "testunit2b" \
|
cmd::test::run_cmd "identity reflects rename (new identity)" "laptop-testunit2b" \
|
||||||
identity show --name testunit
|
identity show --name testunit2b
|
||||||
|
|
||||||
# Remove verifies auto-detach
|
cmd::test::run_cmd_fails "old identity gone after rename" \
|
||||||
"$WGCTL_BINARY" remove --name laptop-testunit2b --force > /dev/null 2>&1 || true
|
identity show --name testunit2
|
||||||
|
|
||||||
|
# Remove — verify auto-detach cleans up identity file
|
||||||
|
cmd::test::run_cmd "remove detaches from identity" "removed" \
|
||||||
|
remove --name laptop-testunit2b --force
|
||||||
|
|
||||||
|
cmd::test::run_cmd_fails "identity cleaned up after remove" \
|
||||||
|
identity show --name testunit2b
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::_destructive_cleanup() {
|
function cmd::test::_destructive_cleanup() {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,10 @@ WGCTL_BINARY="$(command -v wgctl)"
|
||||||
# Helpers
|
# Helpers
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::test::_strip_ansi() {
|
||||||
|
sed 's/\x1b\[[0-9;]*m//g'
|
||||||
|
}
|
||||||
|
|
||||||
function cmd::test::run_cmd() {
|
function cmd::test::run_cmd() {
|
||||||
local desc="$1" expected="${2:-}"
|
local desc="$1" expected="${2:-}"
|
||||||
shift 2
|
shift 2
|
||||||
|
|
@ -17,30 +21,64 @@ function cmd::test::run_cmd() {
|
||||||
tmp=$(mktemp)
|
tmp=$(mktemp)
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
timeout 30 "$WGCTL_BINARY" "$@" > "$tmp" 2>&1 &
|
timeout 30 "$WGCTL_BINARY" "$@" > "$tmp" 2>&1
|
||||||
local pid=$!
|
|
||||||
wait $pid
|
|
||||||
exit_code=$?
|
exit_code=$?
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# Reset terminal color in case command output left ANSI state dirty
|
||||||
|
printf "\033[0m" >&2
|
||||||
|
|
||||||
if [[ $exit_code -eq 124 ]]; then
|
if [[ $exit_code -eq 124 ]]; then
|
||||||
test::warn "${desc} (timed out after 30s)"
|
test::warn "${desc} (timed out after 30s)"
|
||||||
rm -f "$tmp"
|
rm -f "$tmp"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local clean
|
||||||
|
clean=$(cmd::test::_strip_ansi < "$tmp")
|
||||||
|
|
||||||
if [[ $exit_code -ne 0 ]]; then
|
if [[ $exit_code -ne 0 ]]; then
|
||||||
test::fail "${desc}"
|
local msg="${desc}"
|
||||||
|
[[ -n "$expected" ]] && msg="${desc} (expected '${expected}', command failed)"
|
||||||
|
test::fail "$msg"
|
||||||
if [[ "${WGCTL_TEST_VERBOSE:-false}" == "true" ]]; then
|
if [[ "${WGCTL_TEST_VERBOSE:-false}" == "true" ]]; then
|
||||||
printf " Output: %s\n" "$(cat "$tmp")"
|
printf " Output: %s\n" "$(echo "$clean" | head -3 | tr '\n' ' ')"
|
||||||
fi
|
fi
|
||||||
rm -f "$tmp"
|
rm -f "$tmp"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$expected" ]] && ! grep -qF "$expected" "$tmp"; then
|
if [[ -n "$expected" ]] && ! echo "$clean" | grep -qF "$expected"; then
|
||||||
local actual
|
local actual
|
||||||
actual=$(head -3 "$tmp" | tr '\n' ' ' | sed 's/ */ /g' | cut -c1-100)
|
actual=$(echo "$clean" | head -3 | tr '\n' ' ' | sed 's/ */ /g' | cut -c1-100)
|
||||||
|
test::fail "${desc} (expected '${expected}', got: '${actual}')"
|
||||||
|
rm -f "$tmp"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
test::pass "$desc"
|
||||||
|
rm -f "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::test::run_cmd_any() {
|
||||||
|
local desc="$1" expected="${2:-}"
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
|
||||||
|
set +e
|
||||||
|
timeout 30 "$WGCTL_BINARY" "$@" > "$tmp" 2>&1
|
||||||
|
set -e
|
||||||
|
|
||||||
|
printf "\033[0m" >&2
|
||||||
|
|
||||||
|
local clean
|
||||||
|
clean=$(cmd::test::_strip_ansi < "$tmp")
|
||||||
|
|
||||||
|
if [[ -n "$expected" ]] && ! echo "$clean" | grep -qF "$expected"; then
|
||||||
|
local actual
|
||||||
|
actual=$(echo "$clean" | head -3 | tr '\n' ' ' | sed 's/ */ /g' | cut -c1-100)
|
||||||
test::fail "${desc} (expected '${expected}', got: '${actual}')"
|
test::fail "${desc} (expected '${expected}', got: '${actual}')"
|
||||||
rm -f "$tmp"
|
rm -f "$tmp"
|
||||||
return 1
|
return 1
|
||||||
|
|
@ -54,13 +92,15 @@ function cmd::test::run_cmd_fails() {
|
||||||
local desc="$1"
|
local desc="$1"
|
||||||
shift
|
shift
|
||||||
|
|
||||||
set +e
|
|
||||||
local tmp exit_code
|
local tmp exit_code
|
||||||
tmp=$(mktemp)
|
tmp=$(mktemp)
|
||||||
timeout 10 setsid "$WGCTL_BINARY" "$@" > "$tmp" 2>&1
|
|
||||||
|
set +e
|
||||||
|
timeout 10 "$WGCTL_BINARY" "$@" > "$tmp" 2>&1
|
||||||
exit_code=$?
|
exit_code=$?
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
printf "\033[0m" >&2
|
||||||
rm -f "$tmp"
|
rm -f "$tmp"
|
||||||
|
|
||||||
if [[ $exit_code -eq 124 ]]; then
|
if [[ $exit_code -eq 124 ]]; then
|
||||||
|
|
@ -96,13 +136,13 @@ function cmd::test::run_all_integration_sections() {
|
||||||
|
|
||||||
function cmd::test::section_list() {
|
function cmd::test::section_list() {
|
||||||
test::section "List"
|
test::section "List"
|
||||||
cmd::test::run_cmd "list" "WireGuard Clients" list
|
cmd::test::run_cmd "list" "NAME" list
|
||||||
cmd::test::run_cmd "list --online" "" list --online
|
cmd::test::run_cmd "list --online" "" list --online
|
||||||
cmd::test::run_cmd "list --offline" "" list --offline
|
cmd::test::run_cmd "list --offline" "" list --offline
|
||||||
cmd::test::run_cmd "list --blocked" "" list --blocked
|
cmd::test::run_cmd "list --blocked" "" list --blocked
|
||||||
cmd::test::run_cmd "list --type phone" "" list --type phone
|
cmd::test::run_cmd "list --type phone" "phone" list --type phone
|
||||||
cmd::test::run_cmd "list --detailed" "Client:" list --detailed
|
cmd::test::run_cmd "list --detailed" "IP:" list --detailed
|
||||||
cmd::test::run_cmd "list --name phone-nuno" "phone-nuno" list --name phone-nuno
|
cmd::test::run_cmd "list --name phone-nuno" "phone-nuno" list --name phone-nuno
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::section_inspect() {
|
function cmd::test::section_inspect() {
|
||||||
|
|
@ -115,14 +155,14 @@ function cmd::test::section_inspect() {
|
||||||
|
|
||||||
function cmd::test::section_config() {
|
function cmd::test::section_config() {
|
||||||
test::section "Config & QR"
|
test::section "Config & QR"
|
||||||
cmd::test::run_cmd "config --name phone-nuno" "PrivateKey" config --name phone-nuno
|
cmd::test::run_cmd "config --name phone-nuno" "PrivateKey" config --name phone-nuno
|
||||||
cmd::test::run_cmd "config --name nuno --type phone" "PrivateKey" config --name nuno --type phone
|
cmd::test::run_cmd "config --name nuno --type phone" "PrivateKey" config --name nuno --type phone
|
||||||
cmd::test::run_cmd "qr --name phone-nuno" "" qr --name phone-nuno
|
cmd::test::run_cmd "qr --name phone-nuno" "" qr --name phone-nuno
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::section_rules() {
|
function cmd::test::section_rules() {
|
||||||
test::section "Rules"
|
test::section "Rules"
|
||||||
cmd::test::run_cmd "rule list" "guest" rule list
|
cmd::test::run_cmd "rule list" "user" rule list
|
||||||
cmd::test::run_cmd "rule show --name guest" "Description" rule show --name guest
|
cmd::test::run_cmd "rule show --name guest" "Description" rule show --name guest
|
||||||
cmd::test::run_cmd "rule show --name user" "Description" rule show --name user
|
cmd::test::run_cmd "rule show --name user" "Description" rule show --name user
|
||||||
cmd::test::run_cmd "rule show --name admin" "Description" rule show --name admin
|
cmd::test::run_cmd "rule show --name admin" "Description" rule show --name admin
|
||||||
|
|
@ -131,35 +171,35 @@ function cmd::test::section_rules() {
|
||||||
|
|
||||||
function cmd::test::section_groups() {
|
function cmd::test::section_groups() {
|
||||||
test::section "Groups"
|
test::section "Groups"
|
||||||
cmd::test::run_cmd "group list" "Groups" group list
|
cmd::test::run_cmd "group list" "Groups" group list
|
||||||
cmd::test::run_cmd "group show --name family" "Peers:" group show --name family
|
cmd::test::run_cmd "group show --name family" "Peers:" group show --name family
|
||||||
cmd::test::run_cmd_fails "group show nonexistent" group show --name nonexistent
|
cmd::test::run_cmd_fails "group show nonexistent" group show --name nonexistent
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::section_audit() {
|
function cmd::test::section_audit() {
|
||||||
test::section "Audit"
|
test::section "Audit"
|
||||||
cmd::test::run_cmd "audit" "passed" audit
|
cmd::test::run_cmd_any "audit" "passed" audit
|
||||||
cmd::test::run_cmd "audit --peer phone-nuno" "passed" audit --peer phone-nuno
|
cmd::test::run_cmd_any "audit --peer phone-nuno" "passed" audit --peer phone-nuno
|
||||||
cmd::test::run_cmd "audit --type phone" "passed" audit --type phone
|
cmd::test::run_cmd_any "audit --type phone" "passed" audit --type phone
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::section_logs() {
|
function cmd::test::section_logs() {
|
||||||
test::section "Logs"
|
test::section "Logs"
|
||||||
cmd::test::run_cmd "logs" "Activity" logs
|
cmd::test::run_cmd "logs" "Activity" logs
|
||||||
cmd::test::run_cmd "logs --name phone-nuno" "Activity" logs --name phone-nuno
|
cmd::test::run_cmd "logs --name phone-nuno" "Activity" logs --name phone-nuno
|
||||||
cmd::test::run_cmd "logs --fw" "Activity" logs --fw
|
cmd::test::run_cmd "logs --fw" "Activity" logs --fw
|
||||||
cmd::test::run_cmd "logs --wg" "Activity" logs --wg
|
cmd::test::run_cmd "logs --wg" "Activity" logs --wg
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::section_fw() {
|
function cmd::test::section_fw() {
|
||||||
test::section "Firewall"
|
test::section "Firewall"
|
||||||
cmd::test::run_cmd "fw list" "FORWARD" fw list
|
cmd::test::run_cmd "fw list" "FORWARD" fw list
|
||||||
cmd::test::run_cmd "fw list --peer phone-nuno" "" fw list --peer phone-nuno
|
cmd::test::run_cmd "fw list --peer phone-nuno" "" fw list --peer phone-nuno
|
||||||
cmd::test::run_cmd "fw list --no-nflog" "" fw list --no-nflog
|
cmd::test::run_cmd "fw list --no-nflog" "" fw list --no-nflog
|
||||||
cmd::test::run_cmd "fw list --no-accept" "" fw list --no-accept
|
cmd::test::run_cmd "fw list --no-accept" "" fw list --no-accept
|
||||||
cmd::test::run_cmd "fw list --no-drop" "" fw list --no-drop
|
cmd::test::run_cmd "fw list --no-drop" "" fw list --no-drop
|
||||||
cmd::test::run_cmd "fw nat" "PREROUTING" fw nat
|
cmd::test::run_cmd "fw nat" "PREROUTING" fw nat
|
||||||
cmd::test::run_cmd "fw count" "TOTAL" fw count
|
cmd::test::run_cmd "fw count" "TOTAL" fw count
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::section_net() {
|
function cmd::test::section_net() {
|
||||||
|
|
@ -175,26 +215,24 @@ function cmd::test::section_net() {
|
||||||
cmd::test::run_cmd "net add port again" "Added" net add --name test-svc:web --port 9999:tcp
|
cmd::test::run_cmd "net add port again" "Added" net add --name test-svc:web --port 9999:tcp
|
||||||
cmd::test::run_cmd "net rm all ports" "Removed" net rm --name test-svc:ports --force
|
cmd::test::run_cmd "net rm all ports" "Removed" net rm --name test-svc:ports --force
|
||||||
cmd::test::run_cmd "net rm service" "Removed" net rm --name test-svc --force
|
cmd::test::run_cmd "net rm service" "Removed" net rm --name test-svc --force
|
||||||
cmd::test::run_cmd_fails "net show nonexistent" net show --name nonexistent-svc
|
cmd::test::run_cmd_fails "net show nonexistent" net show --name nonexistent-svc
|
||||||
cmd::test::run_cmd_fails "net add port no service" net add --name nonexistent:web --port 80:tcp
|
cmd::test::run_cmd_fails "net add port no service" net add --name nonexistent:web --port 80:tcp
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::test::section_subnet() {
|
function cmd::test::section_subnet() {
|
||||||
test::section "Subnet"
|
test::section "Subnet"
|
||||||
# Cleanup from any previous failed run
|
"$WGCTL_BINARY" subnet rm --name test-subnet-2 > /dev/null 2>&1 || true
|
||||||
"$WGCTL_BINARY" subnet rm --name test-subnet-2 --force > /dev/null 2>&1 || true
|
"$WGCTL_BINARY" subnet rm --name test-subnet > /dev/null 2>&1 || true
|
||||||
"$WGCTL_BINARY" subnet rm --name test-subnet --force > /dev/null 2>&1 || true
|
|
||||||
|
|
||||||
cmd::test::run_cmd "subnet list" "desktop" subnet list
|
cmd::test::run_cmd "subnet list" "desktop" subnet list
|
||||||
cmd::test::run_cmd "subnet show desktop" "Subnet" subnet show --name desktop
|
cmd::test::run_cmd "subnet show desktop" "Subnet" subnet show --name desktop
|
||||||
cmd::test::run_cmd "subnet show guests group" "group" subnet show --name guests
|
cmd::test::run_cmd "subnet show guests group" "group" subnet show --name guests
|
||||||
cmd::test::run_cmd_fails "subnet show nonexistent" subnet show --name nonexistent
|
cmd::test::run_cmd_fails "subnet show nonexistent" subnet show --name nonexistent
|
||||||
|
|
||||||
cmd::test::run_cmd "subnet add" "added" \
|
cmd::test::run_cmd "subnet add" "added" \
|
||||||
subnet add --name test-subnet --subnet 10.1.250.0/24 --type iot --desc "Test"
|
subnet add --name test-subnet --subnet 10.1.250.0/24 --type iot --desc "Test"
|
||||||
cmd::test::run_cmd "subnet list shows new" "test-subnet" \
|
cmd::test::run_cmd "subnet list shows new" "test-subnet" \
|
||||||
subnet list
|
subnet list
|
||||||
|
|
||||||
cmd::test::run_cmd_fails "subnet rename in-use (desktop)" \
|
cmd::test::run_cmd_fails "subnet rename in-use (desktop)" \
|
||||||
subnet rename --name desktop --new-name workstation
|
subnet rename --name desktop --new-name workstation
|
||||||
cmd::test::run_cmd "subnet rename unused" "renamed" \
|
cmd::test::run_cmd "subnet rename unused" "renamed" \
|
||||||
|
|
@ -207,7 +245,7 @@ function cmd::test::section_subnet() {
|
||||||
|
|
||||||
function cmd::test::section_identity() {
|
function cmd::test::section_identity() {
|
||||||
test::section "Identity"
|
test::section "Identity"
|
||||||
cmd::test::run_cmd "identity list" "" identity list
|
cmd::test::run_cmd "identity list" "" identity list
|
||||||
cmd::test::run_cmd "identity migrate --dry-run" "Dry run" identity migrate --dry-run
|
cmd::test::run_cmd "identity migrate --dry-run" "Dry run" identity migrate --dry-run
|
||||||
cmd::test::run_cmd "identity show nuno" "nuno" identity show --name nuno
|
cmd::test::run_cmd "identity show nuno" "nuno" identity show --name nuno
|
||||||
cmd::test::run_cmd_fails "identity show nonexistent" identity show --name nonexistent
|
cmd::test::run_cmd_fails "identity show nonexistent" identity show --name nonexistent
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,6 @@ function cmd::test::assert_false() {
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::test::run_all_unit_sections() {
|
function cmd::test::run_all_unit_sections() {
|
||||||
load_module subnet
|
|
||||||
load_module ip
|
|
||||||
load_module identity
|
|
||||||
|
|
||||||
cmd::test::unit_subnet
|
cmd::test::unit_subnet
|
||||||
cmd::test::unit_ip
|
cmd::test::unit_ip
|
||||||
cmd::test::unit_identity
|
cmd::test::unit_identity
|
||||||
|
|
@ -55,16 +51,16 @@ function cmd::test::unit_subnet() {
|
||||||
load_module subnet
|
load_module subnet
|
||||||
|
|
||||||
# subnet::prefix
|
# subnet::prefix
|
||||||
cmd::test::assert "subnet::prefix /24" "$(subnet::prefix '10.1.3.0/24')" "10.1.3"
|
cmd::test::assert "subnet::prefix /24" "$(subnet::prefix '10.1.3.0/24')" "10.1.3"
|
||||||
cmd::test::assert "subnet::prefix /16" "$(subnet::prefix '10.1.0.0/16')" "10.1.0"
|
cmd::test::assert "subnet::prefix /16" "$(subnet::prefix '10.1.0.0/16')" "10.1.0"
|
||||||
cmd::test::assert "subnet::mask /24" "$(subnet::mask '10.1.3.0/24')" "24"
|
cmd::test::assert "subnet::mask /24" "$(subnet::mask '10.1.3.0/24')" "24"
|
||||||
cmd::test::assert "subnet::mask /16" "$(subnet::mask '10.1.0.0/16')" "16"
|
cmd::test::assert "subnet::mask /16" "$(subnet::mask '10.1.0.0/16')" "16"
|
||||||
cmd::test::assert "subnet::base_ip" "$(subnet::base_ip '10.1.3.0/24')" "10.1.3.0"
|
cmd::test::assert "subnet::base_ip" "$(subnet::base_ip '10.1.3.0/24')" "10.1.3.0"
|
||||||
|
|
||||||
# subnet::contains
|
# subnet::contains
|
||||||
cmd::test::assert_true "subnet::contains inside" subnet::contains "10.1.3.0/24" "10.1.3.5"
|
cmd::test::assert_true "subnet::contains inside" subnet::contains "10.1.3.0/24" "10.1.3.5"
|
||||||
cmd::test::assert_true "subnet::contains boundary" subnet::contains "10.1.3.0/24" "10.1.3.254"
|
cmd::test::assert_true "subnet::contains boundary" subnet::contains "10.1.3.0/24" "10.1.3.254"
|
||||||
cmd::test::assert_false "subnet::contains outside" subnet::contains "10.1.3.0/24" "10.1.4.1"
|
cmd::test::assert_false "subnet::contains outside" subnet::contains "10.1.3.0/24" "10.1.4.1"
|
||||||
cmd::test::assert_false "subnet::contains wrong net" subnet::contains "10.1.3.0/24" "192.168.1.1"
|
cmd::test::assert_false "subnet::contains wrong net" subnet::contains "10.1.3.0/24" "192.168.1.1"
|
||||||
|
|
||||||
# subnet::is_valid_cidr
|
# subnet::is_valid_cidr
|
||||||
|
|
@ -100,7 +96,6 @@ function cmd::test::unit_identity() {
|
||||||
test::section "Unit: identity inference"
|
test::section "Unit: identity inference"
|
||||||
load_module identity
|
load_module identity
|
||||||
|
|
||||||
# _parse_peer_name via identity::infer
|
|
||||||
cmd::test::assert "infer phone-nuno" "$(identity::infer 'phone-nuno')" "nuno|phone|1"
|
cmd::test::assert "infer phone-nuno" "$(identity::infer 'phone-nuno')" "nuno|phone|1"
|
||||||
cmd::test::assert "infer phone-nuno-2" "$(identity::infer 'phone-nuno-2')" "nuno|phone|2"
|
cmd::test::assert "infer phone-nuno-2" "$(identity::infer 'phone-nuno-2')" "nuno|phone|2"
|
||||||
cmd::test::assert "infer desktop-zephyr" "$(identity::infer 'desktop-zephyr')" "zephyr|desktop|1"
|
cmd::test::assert "infer desktop-zephyr" "$(identity::infer 'desktop-zephyr')" "zephyr|desktop|1"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
function cmd::unblock::on_load() {
|
function cmd::unblock::on_load() {
|
||||||
flag::register --name
|
flag::register --name
|
||||||
|
flag::register --identity
|
||||||
flag::register --type
|
flag::register --type
|
||||||
flag::register --force
|
flag::register --force
|
||||||
flag::register --quiet
|
flag::register --quiet
|
||||||
|
|
@ -14,8 +15,6 @@ function cmd::unblock::on_load() {
|
||||||
flag::register --proto
|
flag::register --proto
|
||||||
flag::register --subnet
|
flag::register --subnet
|
||||||
flag::register --all
|
flag::register --all
|
||||||
|
|
||||||
# System - NET Services
|
|
||||||
flag::register --service
|
flag::register --service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,12 +25,14 @@ function cmd::unblock::on_load() {
|
||||||
function cmd::unblock::help() {
|
function cmd::unblock::help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: wgctl unblock --name <name> [options]
|
Usage: wgctl unblock --name <name> [options]
|
||||||
|
or: wgctl unblock --identity <identity> [options]
|
||||||
|
|
||||||
Remove block rules for a client. Without specific flags, performs a full unblock.
|
Remove block rules for a client. Without specific flags, performs a full unblock.
|
||||||
Direct unblock overrides any group blocks.
|
Direct unblock overrides any group blocks.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--name <name> Client name (e.g. phone-nuno)
|
--name <name> Client name (e.g. phone-nuno)
|
||||||
|
--identity <name> Unblock all peers belonging to an identity
|
||||||
--type <type> Device type (optional, combines with --name)
|
--type <type> Device type (optional, combines with --name)
|
||||||
--ip <ip> Unblock specific IP (repeatable)
|
--ip <ip> Unblock specific IP (repeatable)
|
||||||
--subnet <cidr> Unblock specific subnet (repeatable)
|
--subnet <cidr> Unblock specific subnet (repeatable)
|
||||||
|
|
@ -42,40 +43,36 @@ Options:
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
wgctl unblock --name phone-nuno
|
wgctl unblock --name phone-nuno
|
||||||
|
wgctl unblock --identity nuno
|
||||||
wgctl unblock --name nuno --type phone
|
wgctl unblock --name nuno --type phone
|
||||||
wgctl unblock --name phone-nuno --ip 10.0.0.210
|
wgctl unblock --name phone-nuno --ip 10.0.0.210
|
||||||
wgctl unblock --name phone-nuno --service proxmox
|
wgctl unblock --name phone-nuno --service proxmox
|
||||||
wgctl unblock --name phone-nuno --service truenas:web-ui
|
|
||||||
wgctl unban --name phone-nuno
|
wgctl unban --name phone-nuno
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Unblock Run
|
# Run
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::unblock::run() {
|
function cmd::unblock::run() {
|
||||||
local name=""
|
local name="" identity="" type=""
|
||||||
local type=""
|
local ips=() subnets=() ports=() services=()
|
||||||
local ips=()
|
local all=false quiet=false force=false
|
||||||
local subnets=()
|
|
||||||
local ports=()
|
|
||||||
local services=()
|
|
||||||
local all=false
|
|
||||||
local quiet=false
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--name) name="$2"; shift 2 ;;
|
--name) name="$2"; shift 2 ;;
|
||||||
--type) type="$2"; shift 2 ;;
|
--identity) identity="$2"; shift 2 ;;
|
||||||
--ip) ips+=("$2"); shift 2 ;;
|
--type) type="$2"; shift 2 ;;
|
||||||
--force) force=true; shift ;;
|
--ip) ips+=("$2"); shift 2 ;;
|
||||||
--quiet) quiet=true; shift ;;
|
--force) force=true; shift ;;
|
||||||
--subnet) subnets+=("$2"); shift 2 ;;
|
--quiet) quiet=true; shift ;;
|
||||||
--port) ports+=("$2"); shift 2 ;;
|
--subnet) subnets+=("$2"); shift 2 ;;
|
||||||
--service) services+=("$2"); shift 2 ;;
|
--port) ports+=("$2"); shift 2 ;;
|
||||||
--all) all=true; shift ;;
|
--service) services+=("$2"); shift 2 ;;
|
||||||
--help) cmd::unblock::help; return ;;
|
--all) all=true; shift ;;
|
||||||
|
--help) cmd::unblock::help; return ;;
|
||||||
*)
|
*)
|
||||||
log::error "Unknown flag: $1"
|
log::error "Unknown flag: $1"
|
||||||
cmd::unblock::help
|
cmd::unblock::help
|
||||||
|
|
@ -84,30 +81,33 @@ function cmd::unblock::run() {
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# --identity: unblock all peers for this identity
|
||||||
|
if [[ -n "$identity" ]]; then
|
||||||
|
cmd::unblock::_unblock_identity "$identity" "$quiet" || return 1
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -z "$name" ]]; then
|
if [[ -z "$name" ]]; then
|
||||||
log::error "Missing required flag: --name"
|
log::error "Missing required flag: --name or --identity"
|
||||||
cmd::unblock::help
|
cmd::unblock::help
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||||
|
|
||||||
# Check if actually blocked
|
|
||||||
if ! peers::is_blocked "$name" && ! block::has_file "$name"; then
|
if ! peers::is_blocked "$name" && ! block::has_file "$name"; then
|
||||||
log::wg_warning "Client is not blocked: ${name}"
|
log::wg_warning "Client is not blocked: ${name}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Default to full unblock if no specific flags given
|
if [[ ${#ips[@]} -eq 0 && ${#subnets[@]} -eq 0 && \
|
||||||
if [[ ${#ips[@]} -eq 0 && ${#subnets[@]} -eq 0 && ${#ports[@]} -eq 0 && ${#services[@]} -eq 0 ]]; then
|
${#ports[@]} -eq 0 && ${#services[@]} -eq 0 ]]; then
|
||||||
all=true
|
all=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local client_ip
|
local client_ip
|
||||||
client_ip=$(peers::get_ip "$name") || return 1
|
client_ip=$(peers::get_ip "$name") || return 1
|
||||||
|
|
||||||
# $quiet || log::section "Unblocking client: ${name} (${client_ip})"
|
|
||||||
|
|
||||||
if $all; then
|
if $all; then
|
||||||
cmd::unblock::_unblock_all "$name" "$client_ip" "$quiet"
|
cmd::unblock::_unblock_all "$name" "$client_ip" "$quiet"
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -117,14 +117,14 @@ function cmd::unblock::run() {
|
||||||
for ip in "${ips[@]}"; do
|
for ip in "${ips[@]}"; do
|
||||||
fw::unblock_ip "$client_ip" "$ip"
|
fw::unblock_ip "$client_ip" "$ip"
|
||||||
block::remove_rule "$name" "ip" "$ip"
|
block::remove_rule "$name" "ip" "$ip"
|
||||||
$quiet || log::wg_success "${ip} has been unblocked for ${name}"
|
$quiet || log::wg_success "${ip} has been unblocked for ${name}"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Unblock specific subnets
|
# Unblock specific subnets
|
||||||
for subnet in "${subnets[@]}"; do
|
for subnet in "${subnets[@]}"; do
|
||||||
fw::unblock_subnet "$client_ip" "$subnet"
|
fw::unblock_subnet "$client_ip" "$subnet"
|
||||||
block::remove_rule "$name" "subnet" "$subnet"
|
block::remove_rule "$name" "subnet" "$subnet"
|
||||||
$quiet || log::wg_success "${subnet} has been unblocked for ${name}"
|
$quiet || log::wg_success "${subnet} has been unblocked for ${name}"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Unblock specific ports
|
# Unblock specific ports
|
||||||
|
|
@ -133,8 +133,8 @@ function cmd::unblock::run() {
|
||||||
IFS=":" read -r target port proto <<< "$entry"
|
IFS=":" read -r target port proto <<< "$entry"
|
||||||
proto="${proto:-tcp}"
|
proto="${proto:-tcp}"
|
||||||
fw::unblock_port "$client_ip" "$target" "$port" "$proto"
|
fw::unblock_port "$client_ip" "$target" "$port" "$proto"
|
||||||
block::remove_rule "$name" "port" "$b_target" "$b_port" "$b_proto"
|
block::remove_rule "$name" "port" "$target" "$port" "$proto"
|
||||||
$quiet || log::wg_success "${client_ip}:${b_port}:${b_proto:-tcp} has been unblocked for ${name}"
|
$quiet || log::wg_success "${target}:${port}:${proto} has been unblocked for ${name}"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Unblock services
|
# Unblock services
|
||||||
|
|
@ -146,7 +146,6 @@ function cmd::unblock::run() {
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if actually blocked
|
|
||||||
local is_blocked=false
|
local is_blocked=false
|
||||||
for resolved in "${resolved_lines[@]}"; do
|
for resolved in "${resolved_lines[@]}"; do
|
||||||
if [[ "$resolved" == *:*:* ]]; then
|
if [[ "$resolved" == *:*:* ]]; then
|
||||||
|
|
@ -180,27 +179,66 @@ function cmd::unblock::run() {
|
||||||
$quiet || log::wg_success "${svc} has been unblocked for ${name}"
|
$quiet || log::wg_success "${svc} has been unblocked for ${name}"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Clean up block file if now empty
|
|
||||||
block::cleanup "$name"
|
block::cleanup "$name"
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Identity unblock
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::unblock::_unblock_identity() {
|
||||||
|
local identity_name="${1:-}" quiet="${2:-false}"
|
||||||
|
|
||||||
|
identity::require_exists "$identity_name" || return 1
|
||||||
|
identity::require_has_peers "$identity_name" || return 1
|
||||||
|
|
||||||
|
local peers unblocked=0 skipped=0
|
||||||
|
peers=$(identity::peers "$identity_name")
|
||||||
|
|
||||||
|
while IFS= read -r peer_name; do
|
||||||
|
[[ -z "$peer_name" ]] && continue
|
||||||
|
if ! peers::is_blocked "$peer_name" && ! block::has_file "$peer_name"; then
|
||||||
|
skipped=$(( skipped + 1 ))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
local client_ip
|
||||||
|
client_ip=$(peers::get_ip "$peer_name") || continue
|
||||||
|
|
||||||
|
if cmd::unblock::_unblock_all "$peer_name" "$client_ip" true; then
|
||||||
|
unblocked=$(( unblocked + 1 ))
|
||||||
|
else
|
||||||
|
skipped=$(( skipped + 1 ))
|
||||||
|
fi
|
||||||
|
done <<< "$peers"
|
||||||
|
|
||||||
|
if [[ $unblocked -eq 0 ]]; then
|
||||||
|
log::wg_warning "No peers were blocked for identity '${identity_name}'"
|
||||||
|
elif [[ $skipped -gt 0 ]]; then
|
||||||
|
log::ok "Unblocked ${unblocked} peer(s) for identity '${identity_name}' (${skipped} were not blocked)"
|
||||||
|
else
|
||||||
|
log::ok "Unblocked ${unblocked} peer(s) for identity '${identity_name}'"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Helpers
|
||||||
|
# ============================================
|
||||||
|
|
||||||
function cmd::unblock::_unblock_all() {
|
function cmd::unblock::_unblock_all() {
|
||||||
local name="${1:?}" client_ip="${2:?}" quiet="${3:-false}"
|
local name="${1:?}" client_ip="${2:?}" quiet="${3:-false}"
|
||||||
|
|
||||||
# Direct unblock overrides everything — clear all block state
|
|
||||||
block::set_direct "$name" "$client_ip" "false"
|
block::set_direct "$name" "$client_ip" "false"
|
||||||
block::clear_full_block "$name"
|
block::clear_full_block "$name"
|
||||||
|
|
||||||
block::restore_peer "$name" "$client_ip"
|
block::restore_peer "$name" "$client_ip"
|
||||||
block::cleanup "$name"
|
block::cleanup "$name"
|
||||||
|
|
||||||
local rule
|
local rule
|
||||||
rule=$(peers::get_meta "$name" "rule")
|
rule=$(peers::get_meta "$name" "rule")
|
||||||
|
if [[ -n "$rule" ]] && rule::exists "$rule"; then
|
||||||
[[ -n "$rule" ]] && rule::exists "$rule" && \
|
|
||||||
rule::apply "$rule" "$client_ip" "$name"
|
rule::apply "$rule" "$client_ip" "$name"
|
||||||
|
fi
|
||||||
|
|
||||||
local groups
|
local groups
|
||||||
groups=$(block::get_groups "$name")
|
groups=$(block::get_groups "$name")
|
||||||
|
|
@ -209,6 +247,5 @@ function cmd::unblock::_unblock_all() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
$quiet || log::wg_success "${name} has been unblocked."
|
$quiet || log::wg_success "${name} has been unblocked."
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
@ -60,6 +60,7 @@ function json::net_resolve() { python3 "$JSON_HELPER" net_resolve
|
||||||
function json::net_reverse_lookup() { python3 "$JSON_HELPER" net_reverse_lookup "$@" </dev/null; }
|
function json::net_reverse_lookup() { python3 "$JSON_HELPER" net_reverse_lookup "$@" </dev/null; }
|
||||||
function json::block_is_empty() { python3 "$JSON_HELPER" block_is_empty "$@" </dev/null; }
|
function json::block_is_empty() { python3 "$JSON_HELPER" block_is_empty "$@" </dev/null; }
|
||||||
function json::group_has_peer() { python3 "$JSON_HELPER" group_has_peer "$@" </dev/null; }
|
function json::group_has_peer() { python3 "$JSON_HELPER" group_has_peer "$@" </dev/null; }
|
||||||
|
|
||||||
# Subnet wrappers
|
# Subnet wrappers
|
||||||
function json::subnet_lookup() { python3 "$JSON_HELPER" subnet_lookup "$@" </dev/null; }
|
function json::subnet_lookup() { python3 "$JSON_HELPER" subnet_lookup "$@" </dev/null; }
|
||||||
function json::subnet_type() { python3 "$JSON_HELPER" subnet_type "$@" </dev/null; }
|
function json::subnet_type() { python3 "$JSON_HELPER" subnet_type "$@" </dev/null; }
|
||||||
|
|
@ -72,6 +73,8 @@ function json::subnet_remove() { python3 "$JSON_HELPER" subnet_remove
|
||||||
function json::subnet_rename() { python3 "$JSON_HELPER" subnet_rename "$@" </dev/null; }
|
function json::subnet_rename() { python3 "$JSON_HELPER" subnet_rename "$@" </dev/null; }
|
||||||
function json::subnet_peers() { python3 "$JSON_HELPER" subnet_peers "$@" </dev/null; }
|
function json::subnet_peers() { python3 "$JSON_HELPER" subnet_peers "$@" </dev/null; }
|
||||||
function json::subnet_exists() { python3 "$JSON_HELPER" subnet_exists "$@" </dev/null; }
|
function json::subnet_exists() { python3 "$JSON_HELPER" subnet_exists "$@" </dev/null; }
|
||||||
|
function json::subnet_default_rule() { python3 "$JSON_HELPER" subnet_default_rule "$@" </dev/null; }
|
||||||
|
function json::subnet_list_names() { python3 "$JSON_HELPER" subnet_list_names "$@" </dev/null; }
|
||||||
|
|
||||||
# Identity wrappers
|
# Identity wrappers
|
||||||
function json::identity_list() { python3 "$JSON_HELPER" identity_list "$@" </dev/null; }
|
function json::identity_list() { python3 "$JSON_HELPER" identity_list "$@" </dev/null; }
|
||||||
|
|
|
||||||
|
|
@ -540,31 +540,38 @@ def count(file, key):
|
||||||
print(0)
|
print(0)
|
||||||
|
|
||||||
def audit_fw_counts(clients_dir):
|
def audit_fw_counts(clients_dir):
|
||||||
"""Return peer_name:fw_count pairs from iptables and client configs"""
|
"""Return peer_name:fw_count pairs from iptables"""
|
||||||
import glob, subprocess
|
import glob, subprocess, re
|
||||||
|
|
||||||
# Get iptables output once
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['iptables', '-L', 'FORWARD', '-n'],
|
['iptables', '-L', 'FORWARD', '-n', '-v'],
|
||||||
capture_output=True, text=True
|
capture_output=True, text=True
|
||||||
)
|
)
|
||||||
fw_output = result.stdout
|
fw_lines = result.stdout.splitlines()
|
||||||
except:
|
except Exception:
|
||||||
fw_output = ""
|
fw_lines = []
|
||||||
|
|
||||||
# Build ip->name and count rules
|
# Filter to only data lines (skip headers and blanks)
|
||||||
for conf in glob.glob(f"{clients_dir}/*.conf"):
|
# In -v output, source IP is in column 8 (0-indexed)
|
||||||
|
# Format: pkts bytes target prot opt in out source destination [options]
|
||||||
|
rule_lines = [l for l in fw_lines if l.strip() and not l.startswith('Chain') and not l.startswith(' pkts')]
|
||||||
|
|
||||||
|
for conf in sorted(glob.glob(f"{clients_dir}/*.conf")):
|
||||||
name = os.path.basename(conf).replace('.conf', '')
|
name = os.path.basename(conf).replace('.conf', '')
|
||||||
try:
|
try:
|
||||||
with open(conf) as f:
|
with open(conf) as f:
|
||||||
|
ip = ''
|
||||||
for line in f:
|
for line in f:
|
||||||
if line.startswith('Address'):
|
if line.startswith('Address'):
|
||||||
ip = line.split('=')[1].strip().split('/')[0]
|
ip = line.split('=')[1].strip().split('/')[0]
|
||||||
count = fw_output.count(ip)
|
|
||||||
print(f"{name}:{count}")
|
|
||||||
break
|
break
|
||||||
except:
|
if not ip:
|
||||||
|
continue
|
||||||
|
# Count lines where source column exactly matches the peer IP
|
||||||
|
count = sum(1 for l in rule_lines if re.search(r'\s' + re.escape(ip) + r'\s', l))
|
||||||
|
print(f"{name}:{count}")
|
||||||
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def peer_group_map(groups_dir):
|
def peer_group_map(groups_dir):
|
||||||
|
|
@ -599,55 +606,6 @@ def peer_groups(groups_dir, peer_name):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def peer_data(clients_dir, meta_dir, events_log):
|
|
||||||
import glob
|
|
||||||
|
|
||||||
meta = {}
|
|
||||||
for f in glob.glob(f"{meta_dir}/*.meta"):
|
|
||||||
name = os.path.basename(f).replace('.meta', '')
|
|
||||||
try:
|
|
||||||
with open(f) as mf:
|
|
||||||
meta[name] = json.load(mf)
|
|
||||||
except:
|
|
||||||
meta[name] = {}
|
|
||||||
|
|
||||||
last_events = {}
|
|
||||||
try:
|
|
||||||
with open(events_log) as f:
|
|
||||||
for line in f:
|
|
||||||
try:
|
|
||||||
e = json.loads(line.strip())
|
|
||||||
client = e.get('client', '')
|
|
||||||
if client:
|
|
||||||
last_events[client] = e
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for conf in sorted(glob.glob(f"{clients_dir}/*.conf")):
|
|
||||||
name = os.path.basename(conf).replace('.conf', '')
|
|
||||||
ip = ''
|
|
||||||
try:
|
|
||||||
with open(conf) as f:
|
|
||||||
for line in f:
|
|
||||||
if line.startswith('Address'):
|
|
||||||
ip = line.split('=')[1].strip().split('/')[0]
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
m = meta.get(name, {})
|
|
||||||
rule = m.get('rule', '')
|
|
||||||
subtype = m.get('subtype', '')
|
|
||||||
main_group = m.get('main_group', '')
|
|
||||||
|
|
||||||
last_event = last_events.get(name, {})
|
|
||||||
last_ts = last_event.get('timestamp', '') # raw ISO, no formatting
|
|
||||||
last_evt = last_event.get('event', '') # fixed: was last_event
|
|
||||||
|
|
||||||
print(f"{name}|{ip}|{rule}|{subtype}|{last_ts}|{last_evt}|{main_group}")
|
|
||||||
|
|
||||||
def iso_to_ts(iso_str):
|
def iso_to_ts(iso_str):
|
||||||
"""Convert ISO timestamp to unix timestamp"""
|
"""Convert ISO timestamp to unix timestamp"""
|
||||||
try:
|
try:
|
||||||
|
|
@ -1972,7 +1930,7 @@ def identity_migrate(identities_dir, clients_dir, meta_dir, dry_run):
|
||||||
peer_name = os.path.basename(conf).replace('.conf', '')
|
peer_name = os.path.basename(conf).replace('.conf', '')
|
||||||
parsed = _parse_peer_name(peer_name)
|
parsed = _parse_peer_name(peer_name)
|
||||||
if not parsed:
|
if not parsed:
|
||||||
print(f"skip|{peer_name}||0")
|
print(f"skip|{peer_name}")
|
||||||
continue
|
continue
|
||||||
peer_type, identity_name, index = parsed
|
peer_type, identity_name, index = parsed
|
||||||
if identity_name not in grouped:
|
if identity_name not in grouped:
|
||||||
|
|
@ -2005,13 +1963,12 @@ def identity_exists(file):
|
||||||
# peer_data update — adds type field from meta
|
# peer_data update — adds type field from meta
|
||||||
# ============================================
|
# ============================================
|
||||||
# NOTE: This replaces the existing peer_data function.
|
# NOTE: This replaces the existing peer_data function.
|
||||||
# The new version reads 'type' from meta directly instead of inferring from subtype.
|
# The new version reads 'type' from meta directly.
|
||||||
# Output format: name|ip|rule|type|last_ts|last_evt|main_group
|
# Output format: name|ip|rule|type|last_ts|last_evt|main_group
|
||||||
# (field 4 is now 'type' instead of 'subtype')
|
|
||||||
|
|
||||||
def peer_data_v2(clients_dir, meta_dir, events_log):
|
def peer_data(clients_dir, meta_dir, events_log):
|
||||||
"""
|
"""
|
||||||
Updated peer_data that reads 'type' from meta instead of 'subtype'.
|
Updated peer_data that reads 'type' from meta.
|
||||||
Output: name|ip|rule|type|last_ts|last_evt|main_group
|
Output: name|ip|rule|type|last_ts|last_evt|main_group
|
||||||
"""
|
"""
|
||||||
import glob
|
import glob
|
||||||
|
|
@ -2062,6 +2019,35 @@ def peer_data_v2(clients_dir, meta_dir, events_log):
|
||||||
|
|
||||||
print(f"{name}|{ip}|{rule}|{peer_type}|{last_ts}|{last_evt}|{main_group}")
|
print(f"{name}|{ip}|{rule}|{peer_type}|{last_ts}|{last_evt}|{main_group}")
|
||||||
|
|
||||||
|
def subnet_default_rule(file, name, type_key=''):
|
||||||
|
"""
|
||||||
|
Return the default_rule for a subnet entry, or empty string if none set.
|
||||||
|
For scalar: subnet_default_rule(file, "desktop") -> ""
|
||||||
|
For group: subnet_default_rule(file, "guests", "phone") -> "guest"
|
||||||
|
"""
|
||||||
|
data = _subnet_read(file)
|
||||||
|
if name not in data:
|
||||||
|
print('')
|
||||||
|
return
|
||||||
|
entry = data[name]
|
||||||
|
if _subnet_is_group(entry):
|
||||||
|
key = type_key if type_key else 'none'
|
||||||
|
child = entry.get(key, {})
|
||||||
|
print(child.get('default_rule', ''))
|
||||||
|
else:
|
||||||
|
print(entry.get('default_rule', ''))
|
||||||
|
|
||||||
|
def subnet_list_names(file):
|
||||||
|
"""
|
||||||
|
List all top-level subnet names, one per line.
|
||||||
|
Used for dynamic flag registration in commands.
|
||||||
|
Output: one name per line (e.g. desktop, laptop, guests, servers, iot)
|
||||||
|
"""
|
||||||
|
data = _subnet_read(file)
|
||||||
|
for name in data.keys():
|
||||||
|
print(name)
|
||||||
|
|
||||||
|
|
||||||
commands = {
|
commands = {
|
||||||
'get': lambda args: get(args[0], args[1]),
|
'get': lambda args: get(args[0], args[1]),
|
||||||
'set': lambda args: set_key(args[0], args[1], args[2]),
|
'set': lambda args: set_key(args[0], args[1], args[2]),
|
||||||
|
|
@ -2083,7 +2069,7 @@ commands = {
|
||||||
'audit_fw_counts': lambda args: audit_fw_counts(args[0]),
|
'audit_fw_counts': lambda args: audit_fw_counts(args[0]),
|
||||||
'peer_group_map': lambda args: peer_group_map(args[0]),
|
'peer_group_map': lambda args: peer_group_map(args[0]),
|
||||||
'peer_groups': lambda args: peer_groups(args[0], args[1]),
|
'peer_groups': lambda args: peer_groups(args[0], args[1]),
|
||||||
# 'peer_data': lambda args: peer_data(args[0], args[1], args[2]),
|
'peer_data': lambda args: peer_data(args[0], args[1], args[2]),
|
||||||
'iso_to_ts': lambda args: iso_to_ts(args[0]),
|
'iso_to_ts': lambda args: iso_to_ts(args[0]),
|
||||||
'rule_list_data': lambda args: rule_list_data(args[0], args[1]),
|
'rule_list_data': lambda args: rule_list_data(args[0], args[1]),
|
||||||
'group_list_data': lambda args: group_list_data(args[0], args[1]),
|
'group_list_data': lambda args: group_list_data(args[0], args[1]),
|
||||||
|
|
@ -2099,7 +2085,6 @@ commands = {
|
||||||
'create_group': lambda args: create_group(args[0], args[1], args[2]),
|
'create_group': lambda args: create_group(args[0], args[1], args[2]),
|
||||||
'parse_event': lambda args: parse_event(args[0]),
|
'parse_event': lambda args: parse_event(args[0]),
|
||||||
'parse_fw_event': lambda args: parse_fw_event(args[0]),
|
'parse_fw_event': lambda args: parse_fw_event(args[0]),
|
||||||
'peer_transfer': lambda args: peer_transfer(args[0]),
|
|
||||||
'remove_events_filtered': lambda args: remove_events_filtered(
|
'remove_events_filtered': lambda args: remove_events_filtered(
|
||||||
args[0], args[1], args[2], args[3], args[4]=='true', args[5]=='true', args[6] if len(args)>6 else ''),
|
args[0], args[1], args[2], args[3], args[4]=='true', args[5]=='true', args[6] if len(args)>6 else ''),
|
||||||
'peer_transfer': lambda args: peer_transfer(args[0]),
|
'peer_transfer': lambda args: peer_transfer(args[0]),
|
||||||
|
|
@ -2183,7 +2168,8 @@ commands = {
|
||||||
'identity_migrate': lambda args: identity_migrate(args[0], args[1], args[2], args[3]),
|
'identity_migrate': lambda args: identity_migrate(args[0], args[1], args[2], args[3]),
|
||||||
'identity_infer': lambda args: identity_infer(args[0]),
|
'identity_infer': lambda args: identity_infer(args[0]),
|
||||||
'identity_exists': lambda args: identity_exists(args[0]),
|
'identity_exists': lambda args: identity_exists(args[0]),
|
||||||
'peer_data': lambda args: peer_data_v2(args[0], args[1], args[2]),
|
'subnet_default_rule': lambda args: subnet_default_rule(args[0], args[1], args[2] if len(args) > 2 else ''),
|
||||||
|
'subnet_list_names': lambda args: subnet_list_names(args[0]),
|
||||||
}
|
}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ function internal::get_context_icon() {
|
||||||
function internal::log::info() { internal::log INFO "$*"; }
|
function internal::log::info() { internal::log INFO "$*"; }
|
||||||
function internal::log::warn() { internal::log WARN "$*"; }
|
function internal::log::warn() { internal::log WARN "$*"; }
|
||||||
function internal::log::error() { internal::log ERROR "$*"; }
|
function internal::log::error() { internal::log ERROR "$*"; }
|
||||||
function internal::log::success() { internal::log SUCCESS "$*"; }
|
function internal::log::success() { internal::log OK "$*"; }
|
||||||
function internal::log::debug() { internal::log DEBUG "$*"; }
|
function internal::log::debug() { internal::log DEBUG "$*"; }
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"phone-fred": "94.63.0.129",
|
"phone-fred": "176.223.61.130",
|
||||||
"phone-helena": "148.69.46.73",
|
"phone-helena": "148.69.46.73",
|
||||||
"phone-nuno": "94.63.0.129",
|
"phone-nuno": "94.63.0.129",
|
||||||
"tablet-nuno": "148.69.202.5",
|
"tablet-nuno": "148.69.202.5",
|
||||||
|
|
@ -7,5 +7,7 @@
|
||||||
"guest-zephyr-test": "94.63.0.129",
|
"guest-zephyr-test": "94.63.0.129",
|
||||||
"desktop-roboclean": "46.189.215.231",
|
"desktop-roboclean": "46.189.215.231",
|
||||||
"laptop-nuno": "94.63.0.129",
|
"laptop-nuno": "94.63.0.129",
|
||||||
"phone-luis": "176.223.61.15"
|
"phone-luis": "176.223.61.15",
|
||||||
|
"phone-helena-2": "148.69.192.157",
|
||||||
|
"desktop-zephyr": "86.120.152.74"
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +104,8 @@ function block::rename() {
|
||||||
old_file=$(block::file "$name")
|
old_file=$(block::file "$name")
|
||||||
new_file=$(block::file "$new_name")
|
new_file=$(block::file "$new_name")
|
||||||
[[ -f "$old_file" ]] && mv "$old_file" "$new_file"
|
[[ -f "$old_file" ]] && mv "$old_file" "$new_file"
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function block::clear_full_block() {
|
function block::clear_full_block() {
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,18 @@ function config::_init_defaults() {
|
||||||
function config::validate() {
|
function config::validate() {
|
||||||
local errors=()
|
local errors=()
|
||||||
|
|
||||||
# Required fields
|
# Server key and config files
|
||||||
|
if [[ ! -f "$_WG_SERVER_PUBLIC_KEY_FILE" ]]; then
|
||||||
|
errors+=("Server public key not found: ${_WG_SERVER_PUBLIC_KEY_FILE}")
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$_WG_SERVER_PRIVATE_KEY_FILE" ]]; then
|
||||||
|
errors+=("Server private key not found: ${_WG_SERVER_PRIVATE_KEY_FILE}")
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$_WG_CONFIG" ]]; then
|
||||||
|
errors+=("WireGuard config not found: ${_WG_CONFIG}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Required config values
|
||||||
local endpoint
|
local endpoint
|
||||||
endpoint=$(config::endpoint)
|
endpoint=$(config::endpoint)
|
||||||
if [[ -z "$endpoint" ]]; then
|
if [[ -z "$endpoint" ]]; then
|
||||||
|
|
@ -60,9 +71,9 @@ function config::validate() {
|
||||||
local port
|
local port
|
||||||
port=$(config::port)
|
port=$(config::port)
|
||||||
if [[ -z "$port" ]]; then
|
if [[ -z "$port" ]]; then
|
||||||
errors+=("WG_LISTEN_PORT is not set")
|
errors+=("WG_PORT is not set")
|
||||||
elif ! [[ "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
|
elif ! [[ "$port" =~ ^[0-9]+$ ]] || (( port < 1 || port > 65535 )); then
|
||||||
errors+=("WG_LISTEN_PORT must be a valid port number (1-65535)")
|
errors+=("WG_PORT must be a valid port number (1-65535)")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local dns
|
local dns
|
||||||
|
|
@ -79,13 +90,7 @@ function config::validate() {
|
||||||
errors+=("WG_SUBNET is not set — required for IP allocation")
|
errors+=("WG_SUBNET is not set — required for IP allocation")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local interface
|
# Warn-only
|
||||||
interface=$(config::interface)
|
|
||||||
if [[ -z "$interface" ]]; then
|
|
||||||
errors+=("WG_INTERFACE is not set, defaulting to wg0")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Warn-only fields
|
|
||||||
local lan
|
local lan
|
||||||
lan=$(config::lan)
|
lan=$(config::lan)
|
||||||
if [[ -z "$lan" ]]; then
|
if [[ -z "$lan" ]]; then
|
||||||
|
|
@ -140,38 +145,6 @@ function config::load() {
|
||||||
_WG_TUNNEL_SPLIT="${_WG_SUBNET}, ${_WG_LAN}"
|
_WG_TUNNEL_SPLIT="${_WG_SUBNET}, ${_WG_LAN}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Device Type → Subnet Mapping
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
declare -gA DEVICE_SUBNETS=(
|
|
||||||
[desktop]="10.1.1"
|
|
||||||
[laptop]="10.1.2"
|
|
||||||
[phone]="10.1.3"
|
|
||||||
[tablet]="10.1.4"
|
|
||||||
[guest]="10.1.100"
|
|
||||||
[guest-desktop]="10.1.101"
|
|
||||||
[guest-laptop]="10.1.102"
|
|
||||||
[guest-phone]="10.1.103"
|
|
||||||
[guest-tablet]="10.1.104"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Tunnel Modes
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
declare -gA DEVICE_TUNNEL_MODE=(
|
|
||||||
[desktop]="split"
|
|
||||||
[laptop]="split"
|
|
||||||
[phone]="split"
|
|
||||||
[tablet]="split"
|
|
||||||
[guest]="split"
|
|
||||||
[guest-desktop]="split"
|
|
||||||
[guest-laptop]="split"
|
|
||||||
[guest-phone]="split"
|
|
||||||
[guest-tablet]="split"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Accessors
|
# Accessors
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -194,46 +167,8 @@ function config::activity_current_med() { echo "$_ACTIVITY_TOTAL_MED_BYTES"; }
|
||||||
function config::activity_current_high() { echo "$_ACTIVITY_TOTAL_HIGH_BYTES"; }
|
function config::activity_current_high() { echo "$_ACTIVITY_TOTAL_HIGH_BYTES"; }
|
||||||
function config::server_public_key() { cat "$_WG_SERVER_PUBLIC_KEY_FILE"; }
|
function config::server_public_key() { cat "$_WG_SERVER_PUBLIC_KEY_FILE"; }
|
||||||
|
|
||||||
function config::device_types() {
|
|
||||||
local types
|
|
||||||
{ set +u; types="${!DEVICE_SUBNETS[@]}"; set -u; }
|
|
||||||
echo "$types"
|
|
||||||
}
|
|
||||||
|
|
||||||
function config::is_valid_type() {
|
|
||||||
local type="$1"
|
|
||||||
local subnet
|
|
||||||
subnet=$(config::subnet_for "$type")
|
|
||||||
[[ -n "$subnet" ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
function config::is_guest_type() {
|
|
||||||
local type="$1"
|
|
||||||
[[ "$type" == "guest" || "$type" == guest-* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
function config::subnet_for() {
|
|
||||||
local type="$1"
|
|
||||||
local result
|
|
||||||
{ set +u; result="${DEVICE_SUBNETS[$type]:-}"; set -u; }
|
|
||||||
echo "$result"
|
|
||||||
}
|
|
||||||
|
|
||||||
function config::default_tunnel_for() {
|
|
||||||
local type="$1"
|
|
||||||
local result
|
|
||||||
{ set +u; result="${DEVICE_TUNNEL_MODE[$type]:-split}"; set -u; }
|
|
||||||
echo "$result"
|
|
||||||
}
|
|
||||||
|
|
||||||
function config::allowed_ips_for() {
|
function config::allowed_ips_for() {
|
||||||
local type="$1"
|
local tunnel="${2:-split}"
|
||||||
local tunnel="${2:-}"
|
|
||||||
|
|
||||||
if [[ -z "$tunnel" ]]; then
|
|
||||||
tunnel=$(config::default_tunnel_for "$type")
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$tunnel" in
|
case "$tunnel" in
|
||||||
full) echo "$_WG_TUNNEL_FULL" ;;
|
full) echo "$_WG_TUNNEL_FULL" ;;
|
||||||
split) echo "$_WG_TUNNEL_SPLIT" ;;
|
split) echo "$_WG_TUNNEL_SPLIT" ;;
|
||||||
|
|
@ -242,25 +177,4 @@ function config::allowed_ips_for() {
|
||||||
return 1
|
return 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# Validation
|
|
||||||
# ============================================
|
|
||||||
|
|
||||||
function config::validate() {
|
|
||||||
if [[ ! -f "$_WG_SERVER_PUBLIC_KEY_FILE" ]]; then
|
|
||||||
log::error "Server public key not found: ${_WG_SERVER_PUBLIC_KEY_FILE}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -f "$_WG_SERVER_PRIVATE_KEY_FILE" ]]; then
|
|
||||||
log::error "Server private key not found: ${_WG_SERVER_PRIVATE_KEY_FILE}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ! -f "$_WG_CONFIG" ]]; then
|
|
||||||
log::error "WireGuard config not found: ${_WG_CONFIG}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
@ -34,6 +34,31 @@ function identity::require_not_exists() {
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# identity::require_exists_for_flag <identity_name>
|
||||||
|
# Used by commands to validate --identity value before proceeding.
|
||||||
|
function identity::require_exists_for_flag() {
|
||||||
|
local identity_name="${1:-}"
|
||||||
|
[[ -z "$identity_name" ]] && {
|
||||||
|
log::error "Missing value for --identity"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
# Identity may not exist yet for add (it will be created)
|
||||||
|
# Only require existence for commands that read from it
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# identity::require_has_peers <identity_name>
|
||||||
|
# Used by block/unblock/list to ensure identity has peers to operate on.
|
||||||
|
function identity::require_has_peers() {
|
||||||
|
local identity_name="${1:-}"
|
||||||
|
local peers
|
||||||
|
peers=$(identity::peers "$identity_name")
|
||||||
|
if [[ -z "$peers" ]]; then
|
||||||
|
log::error "Identity '${identity_name}' has no peers"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Peer name inference
|
# Peer name inference
|
||||||
|
|
@ -64,6 +89,25 @@ function identity::next_index() {
|
||||||
json::identity_next_index "$id_file" "$peer_type" 2>/dev/null || echo 1
|
json::identity_next_index "$id_file" "$peer_type" 2>/dev/null || echo 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# identity::next_peer_name <identity_name> <type>
|
||||||
|
# Returns the full peer name for the next device of a given type
|
||||||
|
# for an identity. Creates the name with the correct index.
|
||||||
|
# e.g. identity::next_peer_name helena phone → phone-helena-2
|
||||||
|
# (if phone-helena already exists, index 1 is taken)
|
||||||
|
function identity::next_peer_name() {
|
||||||
|
local identity_name="${1:-}" peer_type="${2:-}"
|
||||||
|
[[ -z "$identity_name" || -z "$peer_type" ]] && return 1
|
||||||
|
|
||||||
|
local index
|
||||||
|
index=$(identity::next_index "$identity_name" "$peer_type")
|
||||||
|
|
||||||
|
if [[ "$index" -eq 1 ]]; then
|
||||||
|
echo "${peer_type}-${identity_name}"
|
||||||
|
else
|
||||||
|
echo "${peer_type}-${identity_name}-${index}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Auto-attach (called from wgctl add)
|
# Auto-attach (called from wgctl add)
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|
|
||||||
|
|
@ -15,40 +15,74 @@ function ip::is_assigned() {
|
||||||
ip::assigned | grep -q "^${candidate}$"
|
ip::assigned | grep -q "^${candidate}$"
|
||||||
}
|
}
|
||||||
|
|
||||||
function ip::next_for_type() {
|
# ip::next_for_subnet <cidr>
|
||||||
local type="$1"
|
# Finds the next unassigned host IP within a CIDR.
|
||||||
local subnet
|
# Replaces ip::next_for_type for the subnet-aware allocation path.
|
||||||
subnet=$(config::subnet_for "$type")
|
function ip::next_for_subnet() {
|
||||||
|
local cidr="${1:-}"
|
||||||
if [[ -z "$subnet" ]]; then
|
|
||||||
log::error "Unknown device type: ${type}"
|
if [[ -z "$cidr" ]]; then
|
||||||
|
log::error "No subnet CIDR provided for IP allocation"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for i in $(seq 1 254); do
|
local prefix
|
||||||
local candidate="${subnet}.${i}"
|
prefix=$(subnet::prefix "$cidr")
|
||||||
if ! ip::is_assigned "$candidate"; then
|
|
||||||
echo "$candidate"
|
local candidate
|
||||||
return 0
|
for i in $(subnet::host_range "$cidr"); do
|
||||||
fi
|
candidate="${prefix}.${i}"
|
||||||
|
ip::is_assigned "$candidate" || { echo "$candidate"; return 0; }
|
||||||
done
|
done
|
||||||
|
|
||||||
log::error "No available IPs in subnet ${subnet}.0/24"
|
log::error "No available IPs in subnet ${cidr}"
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Validation
|
# Validation
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function ip::is_valid() {
|
function ip::is_valid() {
|
||||||
local ip="$1"
|
local ip="${1:-}"
|
||||||
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$ ]]
|
[[ -z "$ip" ]] && return 1
|
||||||
}
|
|
||||||
|
|
||||||
|
# Strip CIDR mask if present
|
||||||
|
local addr="${ip%%/*}"
|
||||||
|
|
||||||
|
# Structural check — 4 octets, optional /mask
|
||||||
|
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$ ]] || return 1
|
||||||
|
|
||||||
|
# Octet range check — each must be 0-255
|
||||||
|
local IFS='.'
|
||||||
|
local -a octets
|
||||||
|
read -ra octets <<< "$addr"
|
||||||
|
for octet in "${octets[@]}"; do
|
||||||
|
(( octet >= 0 && octet <= 255 )) || return 1
|
||||||
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
function ip::is_cidr() {
|
function ip::is_cidr() {
|
||||||
[[ "$1" == *"/"* ]]
|
[[ "$1" == *"/"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ip::is_valid_for_subnet <cidr> <ip>
|
||||||
|
# Convenience wrapper — validates an IP against a specific subnet.
|
||||||
|
# Delegates to subnet::ip_valid_for which handles all the checks.
|
||||||
|
function ip::is_valid_for_subnet() {
|
||||||
|
local cidr="${1:-}" ip="${2:-}"
|
||||||
|
subnet::ip_valid_for "$cidr" "$ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ip::require_valid_for_subnet <cidr> <ip>
|
||||||
|
# Errors and returns 1 if the IP is not valid for the subnet.
|
||||||
|
# Used when a manual --ip override is provided.
|
||||||
|
function ip::require_valid_for_subnet() {
|
||||||
|
local cidr="${1:-}" ip="${2:-}"
|
||||||
|
subnet::require_ip_valid_for "$cidr" "$ip"
|
||||||
|
}
|
||||||
|
|
||||||
function ip::validate() {
|
function ip::validate() {
|
||||||
local ip="$1"
|
local ip="$1"
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,5 @@ function keys::qr() {
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log::wg_qr "QR code for: ${name}"
|
|
||||||
qrencode -t ansiutf8 < "$conf"
|
qrencode -t ansiutf8 < "$conf"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ function peers::create_client_config() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local type="$2"
|
local type="$2"
|
||||||
local ip="$3"
|
local ip="$3"
|
||||||
local allowed_ips="${4:-$(config::allowed_ips_for "$type" "$(config::default_tunnel_for "$type")")}"
|
local allowed_ips="${4:-$(config::allowed_ips_for "split")}"
|
||||||
|
|
||||||
local conf
|
local conf
|
||||||
conf="$(ctx::clients)/${name}.conf"
|
conf="$(ctx::clients)/${name}.conf"
|
||||||
|
|
@ -118,16 +118,9 @@ function peers::list() {
|
||||||
local public_key
|
local public_key
|
||||||
public_key=$(keys::public "$client_name" 2>/dev/null || echo "unknown")
|
public_key=$(keys::public "$client_name" 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
# Determine type from IP
|
local type
|
||||||
local type="unknown"
|
type=$(peers::get_meta "$client_name" "type" 2>/dev/null)
|
||||||
for t in $(config::device_types); do
|
[[ -z "$type" ]] && type=$(peers::get_type_from_ip "$ip")
|
||||||
local subnet
|
|
||||||
subnet=$(config::subnet_for "$t")
|
|
||||||
if string::starts_with "$ip" "$subnet"; then
|
|
||||||
type="$t"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
printf " %-30s %-15s %-10s %s\n" \
|
printf " %-30s %-15s %-10s %s\n" \
|
||||||
"$client_name" "$ip" "$type" "$public_key"
|
"$client_name" "$ip" "$type" "$public_key"
|
||||||
|
|
@ -146,12 +139,12 @@ function peers::list_by_type() {
|
||||||
local ip
|
local ip
|
||||||
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
||||||
|
|
||||||
local subnet
|
local type
|
||||||
subnet=$(config::subnet_for "$filter_type")
|
type=$(peers::get_meta "$client_name" "type" 2>/dev/null)
|
||||||
|
[[ -z "$type" ]] && type=$(peers::get_type_from_ip "$ip")
|
||||||
|
|
||||||
if string::starts_with "$ip" "$subnet"; then
|
[[ "$type" == "$filter_type" ]] && \
|
||||||
printf " %-30s %-15s\n" "$client_name" "$ip"
|
printf " %-30s %-15s\n" "$client_name" "$ip"
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -160,11 +153,6 @@ function peers::exists_in_server() {
|
||||||
grep -q "^# ${name}$" "$(config::config_file)"
|
grep -q "^# ${name}$" "$(config::config_file)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# function peers::is_blocked() {
|
|
||||||
# local name="${1:-}"
|
|
||||||
# peers::exists_in_server "$name" && return 1 || return 0
|
|
||||||
# }
|
|
||||||
|
|
||||||
function peers::is_blocked() {
|
function peers::is_blocked() {
|
||||||
local name="${1:-}"
|
local name="${1:-}"
|
||||||
block::is_blocked "$name"
|
block::is_blocked "$name"
|
||||||
|
|
@ -183,25 +171,12 @@ function peers::get_type() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
local ip
|
local ip
|
||||||
ip=$(peers::get_ip "$name")
|
ip=$(peers::get_ip "$name")
|
||||||
[[ -z "$ip" ]] && echo "unknown" && return
|
[[ -z "$ip" ]] && echo "unknown" && return 0
|
||||||
|
peers::get_type_from_ip "$ip"
|
||||||
local type="unknown"
|
|
||||||
for t in $(config::device_types); do
|
|
||||||
local subnet
|
|
||||||
subnet=$(config::subnet_for "$t")
|
|
||||||
if string::starts_with "$ip" "${subnet}."; then
|
|
||||||
type="$t"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "$type"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function peers::default_rule() {
|
function peers::default_rule() {
|
||||||
local name="$1"
|
echo "user"
|
||||||
local type
|
|
||||||
type=$(peers::get_type "$name")
|
|
||||||
config::is_guest_type "$type" && echo "guest" || echo "user"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function peers::effective_rule() {
|
function peers::effective_rule() {
|
||||||
|
|
@ -305,7 +280,7 @@ function peers::resolve_name() {
|
||||||
local type="${2:-}"
|
local type="${2:-}"
|
||||||
|
|
||||||
if [[ -n "$type" ]]; then
|
if [[ -n "$type" ]]; then
|
||||||
if ! config::is_valid_type "$type"; then
|
if ! subnet::exists "$type"; then
|
||||||
log::error "Invalid device type: ${type}"
|
log::error "Invalid device type: ${type}"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
@ -333,6 +308,15 @@ function peers::resolve_and_require() {
|
||||||
echo "$resolved"
|
echo "$resolved"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function peers::rename_meta() {
|
||||||
|
local name="${1:-}" new_name="${2:-}"
|
||||||
|
local old_meta new_meta
|
||||||
|
old_meta=$(peers::meta_path "$name")
|
||||||
|
new_meta=$(peers::meta_path "$new_name")
|
||||||
|
[[ -f "$old_meta" ]] && mv "$old_meta" "$new_meta"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Cleanup
|
# Cleanup
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -377,22 +361,6 @@ function peers::format_last_seen() {
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# function peers::format_status() {
|
|
||||||
# local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}"
|
|
||||||
# local is_restricted="${4:-false}" handshake_ts="${5:-0}" last_ts="${6:-}"
|
|
||||||
|
|
||||||
# local state
|
|
||||||
# state=$(peers::connection_state "$is_blocked" "$is_restricted" \
|
|
||||||
# "$handshake_ts" "$last_ts")
|
|
||||||
|
|
||||||
# local conn_str modifier color
|
|
||||||
# IFS="|" read -r conn_str modifier color <<< "$state"
|
|
||||||
|
|
||||||
# local display="$conn_str"
|
|
||||||
# [[ -n "$modifier" ]] && display="${conn_str} (${modifier})"
|
|
||||||
# echo -e "${color}${display}\033[0m"
|
|
||||||
# }
|
|
||||||
|
|
||||||
function peers::format_status() {
|
function peers::format_status() {
|
||||||
local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}"
|
local name="${1:-}" public_key="${2:-}" is_blocked="${3:-false}"
|
||||||
local is_restricted="${4:-false}" handshake_ts="${5:-0}" last_ts="${6:-}"
|
local is_restricted="${4:-false}" handshake_ts="${5:-0}" last_ts="${6:-}"
|
||||||
|
|
@ -448,14 +416,8 @@ function peers::format_status_verbose() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function peers::display_type() {
|
function peers::display_type() {
|
||||||
local type="${1:-}" subtype="${2:-}"
|
local type="${1:-}" _subtype="${2:-}"
|
||||||
if config::is_guest_type "$type" && [[ -n "$subtype" && "$subtype" != "0" ]]; then
|
echo "${type:-unknown}"
|
||||||
echo "guest/${subtype}"
|
|
||||||
elif config::is_guest_type "$type"; then
|
|
||||||
echo "guest"
|
|
||||||
else
|
|
||||||
echo "$type"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -516,17 +478,10 @@ function peers::last_seen_data() {
|
||||||
function peers::get_type_from_ip() {
|
function peers::get_type_from_ip() {
|
||||||
local ip="${1:-}"
|
local ip="${1:-}"
|
||||||
[[ -z "$ip" ]] && echo "unknown" && return 0
|
[[ -z "$ip" ]] && echo "unknown" && return 0
|
||||||
local type="unknown"
|
subnet::type_from_ip "$ip"
|
||||||
for t in $(config::device_types); do
|
|
||||||
local subnet
|
|
||||||
subnet=$(config::subnet_for "$t")
|
|
||||||
string::starts_with "$ip" "${subnet}." && type="$t" && break
|
|
||||||
done
|
|
||||||
echo "$type"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Activity
|
# Activity
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ function rule::is_applied() {
|
||||||
local target port proto
|
local target port proto
|
||||||
IFS=":" read -r target port proto <<< "$first_port"
|
IFS=":" read -r target port proto <<< "$first_port"
|
||||||
proto="${proto:-tcp}"
|
proto="${proto:-tcp}"
|
||||||
fw::has_block_rule "$client_ip" "$target" "$proto" "$port"
|
fw::has_block_rule "$client_ip" "$target" "$proto" "$port"
|
||||||
return $?
|
return $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -112,7 +112,6 @@ function rule::apply() {
|
||||||
|
|
||||||
log::debug "rule::apply: peer_name=$peer_name ip=$client_ip"
|
log::debug "rule::apply: peer_name=$peer_name ip=$client_ip"
|
||||||
|
|
||||||
# Check if already applied
|
|
||||||
if rule::is_applied "$rule_name" "$client_ip"; then
|
if rule::is_applied "$rule_name" "$client_ip"; then
|
||||||
log::wg "Rule '${rule_name}' already applied to: ${client_ip}"
|
log::wg "Rule '${rule_name}' already applied to: ${client_ip}"
|
||||||
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
|
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
|
||||||
|
|
@ -149,7 +148,6 @@ function rule::apply() {
|
||||||
fw::allow_port "$client_ip" "$target" "$port" "$proto"
|
fw::allow_port "$client_ip" "$target" "$port" "$proto"
|
||||||
done < <(rule::get "$rule_name" "allow_ports")
|
done < <(rule::get "$rule_name" "allow_ports")
|
||||||
|
|
||||||
# Persist rule assignment
|
|
||||||
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
|
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
|
||||||
|
|
||||||
# DNS redirect
|
# DNS redirect
|
||||||
|
|
@ -221,26 +219,38 @@ function rule::unapply() {
|
||||||
local dns_redirect
|
local dns_redirect
|
||||||
dns_redirect=$(rule::get "$rule_name" "dns_redirect")
|
dns_redirect=$(rule::get "$rule_name" "dns_redirect")
|
||||||
if [[ "$dns_redirect" == "true" ]]; then
|
if [[ "$dns_redirect" == "true" ]]; then
|
||||||
local subtype
|
local peer_ip peer_subnet
|
||||||
subtype=$(peers::get_meta "$peer_name" "subtype")
|
peer_ip=$(peers::get_ip "$peer_name")
|
||||||
local subnet
|
peer_subnet=$(echo "$peer_ip" | cut -d'.' -f1-3)
|
||||||
if [[ -n "$subtype" ]]; then
|
rule::remove_dns_redirect "${peer_subnet}.0/24"
|
||||||
subnet=$(config::subnet_for "$subtype")
|
|
||||||
else
|
|
||||||
local peer_type
|
|
||||||
peer_type=$(peers::get_type "$peer_name") || true
|
|
||||||
[[ -z "$peer_type" ]] && peer_type="phone"
|
|
||||||
subnet=$(config::subnet_for "$peer_type")
|
|
||||||
fi
|
|
||||||
rule::remove_dns_redirect "${subnet}.0/24"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clear rule from meta
|
|
||||||
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" ""
|
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" ""
|
||||||
|
|
||||||
log::debug "Removed rule '${rule_name}' from: ${client_ip}"
|
log::debug "Removed rule '${rule_name}' from: ${client_ip}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Bulk Operations
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# rule::full_restore_peer <peer_name> <client_ip>
|
||||||
|
# Flush and fully restore all fw rules for a peer — rule rules + block rules.
|
||||||
|
# Use this instead of calling rule::apply + block::restore_rules_for separately
|
||||||
|
# to ensure block rules are never left missing after a flush.
|
||||||
|
function rule::full_restore_peer() {
|
||||||
|
local peer_name="${1:-}" client_ip="${2:-}"
|
||||||
|
[[ -z "$peer_name" || -z "$client_ip" ]] && return 1
|
||||||
|
|
||||||
|
fw::flush_peer "$client_ip"
|
||||||
|
|
||||||
|
local rule_name
|
||||||
|
rule_name=$(peers::get_meta "$peer_name" "rule")
|
||||||
|
[[ -n "$rule_name" ]] && rule::apply "$rule_name" "$client_ip" "$peer_name"
|
||||||
|
|
||||||
|
block::restore_rules_for "$peer_name" "$client_ip"
|
||||||
|
}
|
||||||
|
|
||||||
function rule::reapply_all() {
|
function rule::reapply_all() {
|
||||||
local rule_name="${1:-}"
|
local rule_name="${1:-}"
|
||||||
rule::require_exists "$rule_name" || return 1
|
rule::require_exists "$rule_name" || return 1
|
||||||
|
|
@ -254,9 +264,7 @@ function rule::reapply_all() {
|
||||||
local client_ip
|
local client_ip
|
||||||
client_ip=$(peers::get_ip "$peer_name")
|
client_ip=$(peers::get_ip "$peer_name")
|
||||||
[[ -z "$client_ip" ]] && continue
|
[[ -z "$client_ip" ]] && continue
|
||||||
# FLUSH first to ensure clean ordering
|
rule::full_restore_peer "$peer_name" "$client_ip"
|
||||||
fw::flush_peer "$client_ip"
|
|
||||||
rule::apply "$rule_name" "$client_ip" "$peer_name"
|
|
||||||
(( count++ )) || true
|
(( count++ )) || true
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
@ -265,12 +273,10 @@ function rule::reapply_all() {
|
||||||
|
|
||||||
function rule::restore_all() {
|
function rule::restore_all() {
|
||||||
while IFS= read -r peer_name; do
|
while IFS= read -r peer_name; do
|
||||||
# Skip blocked peers - no fw rules needed when blocked
|
|
||||||
block::is_blocked "$peer_name" && continue
|
block::is_blocked "$peer_name" && continue
|
||||||
|
|
||||||
local rule_name
|
local rule_name
|
||||||
rule_name=$(peers::get_meta "$peer_name" "rule")
|
rule_name=$(peers::get_meta "$peer_name" "rule")
|
||||||
|
|
||||||
[[ -z "$rule_name" ]] && continue
|
[[ -z "$rule_name" ]] && continue
|
||||||
|
|
||||||
if ! rule::exists "$rule_name"; then
|
if ! rule::exists "$rule_name"; then
|
||||||
|
|
@ -282,7 +288,8 @@ function rule::restore_all() {
|
||||||
client_ip=$(peers::get_ip "$peer_name")
|
client_ip=$(peers::get_ip "$peer_name")
|
||||||
[[ -z "$client_ip" ]] && continue
|
[[ -z "$client_ip" ]] && continue
|
||||||
|
|
||||||
rule::apply "$rule_name" "$client_ip"
|
# full_restore_peer ensures block rules are restored alongside rule rules
|
||||||
|
rule::full_restore_peer "$peer_name" "$client_ip"
|
||||||
done < <(peers::all)
|
done < <(peers::all)
|
||||||
log::wg "Rules restored for all peers"
|
log::wg "Rules restored for all peers"
|
||||||
}
|
}
|
||||||
|
|
@ -333,12 +340,7 @@ function rule::render_flat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function rule::render_entries() {
|
function rule::render_entries() {
|
||||||
# Renders allow/block entries for a rule name with annotations and DNS
|
|
||||||
# Usage: rule::render_entries <rule_name> <indent>
|
|
||||||
# indent: 4 for rule show, 4 for inspect (same)
|
|
||||||
local rule_name="${1:-}" indent="${2:-4}"
|
local rule_name="${1:-}" indent="${2:-4}"
|
||||||
local rule_file
|
|
||||||
rule_file="$(rule::path "$rule_name")"
|
|
||||||
|
|
||||||
local allow_ports allow_ips block_ips block_ports dns
|
local allow_ports allow_ips block_ips block_ports dns
|
||||||
allow_ports=$(rule::get "$rule_name" "allow_ports" 2>/dev/null || true)
|
allow_ports=$(rule::get "$rule_name" "allow_ports" 2>/dev/null || true)
|
||||||
|
|
@ -362,7 +364,6 @@ function rule::render_entries() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function rule::render_own_entries() {
|
function rule::render_own_entries() {
|
||||||
# Renders own (non-inherited) entries for a rule
|
|
||||||
local rule_name="${1:-}"
|
local rule_name="${1:-}"
|
||||||
local rule_file
|
local rule_file
|
||||||
rule_file="$(rule::path "$rule_name")"
|
rule_file="$(rule::path "$rule_name")"
|
||||||
|
|
@ -374,11 +375,8 @@ function rule::render_own_entries() {
|
||||||
block_ports=$(json::get "$rule_file" "block_ports" 2>/dev/null || true)
|
block_ports=$(json::get "$rule_file" "block_ports" 2>/dev/null || true)
|
||||||
dns=$(json::get "$rule_file" "dns_redirect" 2>/dev/null || true)
|
dns=$(json::get "$rule_file" "dns_redirect" 2>/dev/null || true)
|
||||||
|
|
||||||
local has_own=false
|
|
||||||
local combined="${allow_ports}${allow_ips}${block_ips}${block_ports}"
|
local combined="${allow_ports}${allow_ips}${block_ips}${block_ports}"
|
||||||
[[ -n "${combined//[$'\n']/}" ]] && has_own=true
|
[[ -z "${combined//[$'\n']/}" ]] && return 0
|
||||||
|
|
||||||
$has_own || return 0
|
|
||||||
|
|
||||||
while IFS= read -r e; do
|
while IFS= read -r e; do
|
||||||
[[ -z "$e" ]] && continue
|
[[ -z "$e" ]] && continue
|
||||||
|
|
@ -397,7 +395,6 @@ function rule::render_own_entries() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function rule::render_extends_tree() {
|
function rule::render_extends_tree() {
|
||||||
# Renders full inheritance tree for a rule
|
|
||||||
local rule_name="${1:-}"
|
local rule_name="${1:-}"
|
||||||
local rule_file
|
local rule_file
|
||||||
rule_file="$(rule::path "$rule_name")"
|
rule_file="$(rule::path "$rule_name")"
|
||||||
|
|
@ -413,7 +410,6 @@ function rule::render_extends_tree() {
|
||||||
rule::render_entries "$base_name"
|
rule::render_entries "$base_name"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Own rules after inherited
|
|
||||||
local own_output
|
local own_output
|
||||||
own_output=$(rule::render_own_entries "$rule_name")
|
own_output=$(rule::render_own_entries "$rule_name")
|
||||||
if [[ -n "$own_output" ]]; then
|
if [[ -n "$own_output" ]]; then
|
||||||
|
|
@ -436,4 +432,4 @@ function rule::apply_dns_redirect() {
|
||||||
function rule::remove_dns_redirect() {
|
function rule::remove_dns_redirect() {
|
||||||
local client_subnet="${1:-}"
|
local client_subnet="${1:-}"
|
||||||
fw::nat_remove_dns_redirect "$client_subnet" "$(config::dns)" "$(config::interface)"
|
fw::nat_remove_dns_redirect "$client_subnet" "$(config::dns)" "$(config::interface)"
|
||||||
}
|
}
|
||||||
|
|
@ -1,17 +1,153 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# subnet.module.sh — subnet map lookups, resolution, and validation
|
# subnet.module.sh — subnet map lookups, resolution, validation, and CIDR utilities
|
||||||
# All subnet data lives in subnets.json; this module wraps json:: calls
|
# All subnet data lives in subnets.json; this module wraps json:: calls
|
||||||
# and provides hardcoded fallbacks for safety.
|
# and provides hardcoded fallbacks for safety.
|
||||||
|
|
||||||
# ===========================================================================
|
# ======================================================
|
||||||
# Hardcoded fallbacks (mirrors production subnets.json)
|
# CIDR Utilities
|
||||||
# Used when subnets.json lookup fails — keeps existing peers working.
|
# Pure functions — no external dependencies, no side effects.
|
||||||
# These match the legacy DEVICE_SUBNETS / DEVICE_TUNNEL_MODE maps in config.module.sh.
|
# Suitable for unit testing.
|
||||||
# ===========================================================================
|
# ======================================================
|
||||||
|
|
||||||
function subnet::_hardcoded_subnet() {
|
# subnet::prefix <cidr>
|
||||||
|
# Returns the first three octets of a CIDR (the allocation prefix).
|
||||||
|
# Example: 10.1.3.0/24 -> 10.1.3
|
||||||
|
function subnet::prefix() {
|
||||||
|
echo "${1%%/*}" | cut -d'.' -f1-3
|
||||||
|
}
|
||||||
|
|
||||||
|
# subnet::base_ip <cidr>
|
||||||
|
# Returns the network address without the mask.
|
||||||
|
# Example: 10.1.3.0/24 -> 10.1.3.0
|
||||||
|
function subnet::base_ip() {
|
||||||
|
echo "${1%%/*}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# subnet::mask <cidr>
|
||||||
|
# Returns the prefix length.
|
||||||
|
# Example: 10.1.3.0/24 -> 24
|
||||||
|
function subnet::mask() {
|
||||||
|
echo "${1##*/}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# subnet::host_range <cidr>
|
||||||
|
# Returns the iterable host range for a subnet.
|
||||||
|
# Currently supports /24 (1-254). Mask-aware implementation deferred.
|
||||||
|
function subnet::host_range() {
|
||||||
|
local cidr="${1:-}"
|
||||||
|
local mask
|
||||||
|
mask=$(subnet::mask "$cidr")
|
||||||
|
case "$mask" in
|
||||||
|
24) seq 1 254 ;;
|
||||||
|
*)
|
||||||
|
# Fallback for non-/24 — still seq 1 254, but noted for future upgrade
|
||||||
|
seq 1 254
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# subnet::contains <cidr> <ip>
|
||||||
|
# Returns 0 if the IP falls within the CIDR, 1 otherwise.
|
||||||
|
# Example: subnet::contains 10.1.3.0/24 10.1.3.5 -> 0 (true)
|
||||||
|
function subnet::contains() {
|
||||||
|
local cidr="${1:-}" ip="${2:-}"
|
||||||
|
[[ -z "$cidr" || -z "$ip" ]] && return 1
|
||||||
|
|
||||||
|
local prefix mask
|
||||||
|
prefix=$(subnet::prefix "$cidr")
|
||||||
|
mask=$(subnet::mask "$cidr")
|
||||||
|
|
||||||
|
# For /24: check that the first three octets match
|
||||||
|
case "$mask" in
|
||||||
|
24)
|
||||||
|
local ip_prefix
|
||||||
|
ip_prefix=$(echo "$ip" | cut -d'.' -f1-3)
|
||||||
|
[[ "$ip_prefix" == "$prefix" ]]
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Delegate to Python for non-/24 subnets
|
||||||
|
python3 -c "
|
||||||
|
import ipaddress, sys
|
||||||
|
try:
|
||||||
|
net = ipaddress.ip_network('${cidr}', strict=False)
|
||||||
|
addr = ipaddress.ip_address('${ip}')
|
||||||
|
sys.exit(0 if addr in net else 1)
|
||||||
|
except Exception:
|
||||||
|
sys.exit(1)
|
||||||
|
"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# subnet::is_valid_cidr <cidr>
|
||||||
|
# Returns 0 if the string is a valid CIDR notation.
|
||||||
|
function subnet::is_valid_cidr() {
|
||||||
|
local cidr="${1:-}"
|
||||||
|
[[ "$cidr" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]] || return 1
|
||||||
|
# Also validate each octet is 0-255
|
||||||
|
local ip
|
||||||
|
ip=$(subnet::base_ip "$cidr")
|
||||||
|
local IFS='.'
|
||||||
|
read -ra octets <<< "$ip"
|
||||||
|
for octet in "${octets[@]}"; do
|
||||||
|
(( octet >= 0 && octet <= 255 )) || return 1
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# subnet::ip_valid_for <cidr> <ip>
|
||||||
|
# Returns 0 if the IP is a valid host address within the given CIDR.
|
||||||
|
# Excludes network address (.0) and broadcast address (.255) for /24.
|
||||||
|
function subnet::ip_valid_for() {
|
||||||
|
local cidr="${1:-}" ip="${2:-}"
|
||||||
|
[[ -z "$cidr" || -z "$ip" ]] && return 1
|
||||||
|
|
||||||
|
# Must be a valid IP first
|
||||||
|
ip::is_valid "$ip" || return 1
|
||||||
|
|
||||||
|
# Must be within the subnet
|
||||||
|
subnet::contains "$cidr" "$ip" || return 1
|
||||||
|
|
||||||
|
# Must not be network or broadcast address (for /24)
|
||||||
|
local mask
|
||||||
|
mask=$(subnet::mask "$cidr")
|
||||||
|
if [[ "$mask" == "24" ]]; then
|
||||||
|
local last_octet
|
||||||
|
last_octet=$(echo "$ip" | cut -d'.' -f4)
|
||||||
|
[[ "$last_octet" == "0" || "$last_octet" == "255" ]] && return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# subnet::require_valid_cidr <cidr>
|
||||||
|
# Errors and exits if the CIDR is not valid.
|
||||||
|
function subnet::require_valid_cidr() {
|
||||||
|
local cidr="${1:-}"
|
||||||
|
if ! subnet::is_valid_cidr "$cidr"; then
|
||||||
|
log::error "Invalid CIDR notation: '${cidr}'"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# subnet::require_ip_valid_for <cidr> <ip>
|
||||||
|
# Errors and exits if the IP is not a valid host for the subnet.
|
||||||
|
function subnet::require_ip_valid_for() {
|
||||||
|
local cidr="${1:-}" ip="${2:-}"
|
||||||
|
if ! subnet::ip_valid_for "$cidr" "$ip"; then
|
||||||
|
log::error "IP '${ip}' is not a valid host address in subnet '${cidr}'"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Hardcoded Fallbacks
|
||||||
|
# Mirror of production subnets.json.
|
||||||
|
# Used only when subnets.json lookup fails.
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
|
function subnet::_hardcoded_cidr() {
|
||||||
local type="${1:-}" subnet_name="${2:-}"
|
local type="${1:-}" subnet_name="${2:-}"
|
||||||
# If a subnet name was given, try legacy guest-* mapping first
|
|
||||||
if [[ -n "$subnet_name" ]]; then
|
if [[ -n "$subnet_name" ]]; then
|
||||||
case "$subnet_name" in
|
case "$subnet_name" in
|
||||||
guests) echo "10.1.100.0/24" ;;
|
guests) echo "10.1.100.0/24" ;;
|
||||||
|
|
@ -26,7 +162,6 @@ function subnet::_hardcoded_subnet() {
|
||||||
laptop) echo "10.1.2.0/24" ;;
|
laptop) echo "10.1.2.0/24" ;;
|
||||||
phone) echo "10.1.3.0/24" ;;
|
phone) echo "10.1.3.0/24" ;;
|
||||||
tablet) echo "10.1.4.0/24" ;;
|
tablet) echo "10.1.4.0/24" ;;
|
||||||
# Legacy guest-* types — kept for existing peers during migration
|
|
||||||
guest) echo "10.1.100.0/24" ;;
|
guest) echo "10.1.100.0/24" ;;
|
||||||
guest-desktop) echo "10.1.101.0/24" ;;
|
guest-desktop) echo "10.1.101.0/24" ;;
|
||||||
guest-laptop) echo "10.1.102.0/24" ;;
|
guest-laptop) echo "10.1.102.0/24" ;;
|
||||||
|
|
@ -57,13 +192,12 @@ function subnet::_hardcoded_type() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function subnet::_hardcoded_tunnel_mode() {
|
function subnet::_hardcoded_tunnel_mode() {
|
||||||
# All current subnets use split — placeholder for future full-tunnel entries
|
|
||||||
echo "split"
|
echo "split"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ===========================================================================
|
# ======================================================
|
||||||
# Core resolution
|
# Core Resolution
|
||||||
# ===========================================================================
|
# ======================================================
|
||||||
|
|
||||||
# subnet::lookup <subnet_name> [type_key]
|
# subnet::lookup <subnet_name> [type_key]
|
||||||
# Returns the CIDR for a given subnet name and optional type.
|
# Returns the CIDR for a given subnet name and optional type.
|
||||||
|
|
@ -76,64 +210,48 @@ function subnet::lookup() {
|
||||||
echo "$result"
|
echo "$result"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
subnet::_hardcoded_subnet "" "$subnet_name"
|
subnet::_hardcoded_cidr "" "$subnet_name"
|
||||||
}
|
}
|
||||||
|
|
||||||
# subnet::resolve_for_add <type> [subnet_name]
|
# subnet::resolve_for_add <type> [subnet_name]
|
||||||
# Main entry point for wgctl add.
|
# Main entry point for wgctl add — returns the CIDR to allocate from.
|
||||||
# Returns the CIDR to allocate from.
|
|
||||||
# Resolution order:
|
|
||||||
# 1. subnet_name given + type given -> subnets[subnet_name][type]
|
|
||||||
# 2. subnet_name given, no type -> subnets[subnet_name]["none"]
|
|
||||||
# 3. no subnet_name -> subnets[type] (scalar, type-native)
|
|
||||||
# 4. fallback -> hardcoded map
|
|
||||||
function subnet::resolve_for_add() {
|
function subnet::resolve_for_add() {
|
||||||
local peer_type="${1:-}" subnet_name="${2:-}"
|
local peer_type="${1:-}" subnet_name="${2:-}"
|
||||||
local result
|
local result
|
||||||
|
|
||||||
if [[ -n "$subnet_name" ]]; then
|
if [[ -n "$subnet_name" ]]; then
|
||||||
# Try with type key first
|
# Group entry: try type-specific child first, then "none" slot
|
||||||
if [[ -n "$peer_type" ]]; then
|
if [[ -n "$peer_type" ]]; then
|
||||||
result=$(json::subnet_lookup "$(ctx::subnets)" "$subnet_name" "$peer_type" 2>/dev/null) || true
|
result=$(json::subnet_lookup "$(ctx::subnets)" "$subnet_name" "$peer_type" 2>/dev/null) || true
|
||||||
if [[ -n "$result" ]]; then echo "$result"; return 0; fi
|
[[ -n "$result" ]] && { echo "$result"; return 0; }
|
||||||
fi
|
fi
|
||||||
# Fall back to "none" slot in group, or scalar entry
|
|
||||||
result=$(json::subnet_lookup "$(ctx::subnets)" "$subnet_name" 2>/dev/null) || true
|
result=$(json::subnet_lookup "$(ctx::subnets)" "$subnet_name" 2>/dev/null) || true
|
||||||
if [[ -n "$result" ]]; then echo "$result"; return 0; fi
|
[[ -n "$result" ]] && { echo "$result"; return 0; }
|
||||||
# Hardcoded fallback for subnet name
|
subnet::_hardcoded_cidr "" "$subnet_name"
|
||||||
subnet::_hardcoded_subnet "" "$subnet_name"
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# No subnet_name — resolve from type (native allocation)
|
# No subnet_name — resolve from type (native allocation)
|
||||||
if [[ -n "$peer_type" ]]; then
|
if [[ -n "$peer_type" ]]; then
|
||||||
result=$(json::subnet_lookup "$(ctx::subnets)" "$peer_type" 2>/dev/null) || true
|
result=$(json::subnet_lookup "$(ctx::subnets)" "$peer_type" 2>/dev/null) || true
|
||||||
if [[ -n "$result" ]]; then echo "$result"; return 0; fi
|
[[ -n "$result" ]] && { echo "$result"; return 0; }
|
||||||
fi
|
fi
|
||||||
|
|
||||||
subnet::_hardcoded_subnet "$peer_type"
|
subnet::_hardcoded_cidr "$peer_type"
|
||||||
}
|
}
|
||||||
|
|
||||||
# subnet::type_for_add <type_flag> [subnet_name]
|
# subnet::type_for_add <type_flag> [subnet_name]
|
||||||
# Returns the canonical type string to store in meta.
|
# Returns the canonical type string to store in meta.
|
||||||
# If --subnet given and it's a scalar, type comes from subnets.json entry.
|
|
||||||
# If --subnet is a group, type comes from --type flag (or "none").
|
|
||||||
# If no --subnet, type comes from --type flag directly.
|
|
||||||
function subnet::type_for_add() {
|
function subnet::type_for_add() {
|
||||||
local type_flag="${1:-}" subnet_name="${2:-}"
|
local type_flag="${1:-}" subnet_name="${2:-}"
|
||||||
local result
|
local result
|
||||||
|
|
||||||
if [[ -n "$subnet_name" ]]; then
|
if [[ -n "$subnet_name" ]]; then
|
||||||
result=$(json::subnet_type "$(ctx::subnets)" "$subnet_name" "$type_flag" 2>/dev/null) || true
|
result=$(json::subnet_type "$(ctx::subnets)" "$subnet_name" "$type_flag" 2>/dev/null) || true
|
||||||
if [[ -n "$result" ]]; then echo "$result"; return 0; fi
|
[[ -n "$result" ]] && { echo "$result"; return 0; }
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# No subnet or lookup failed — use the type flag directly
|
echo "${type_flag:-none}"
|
||||||
if [[ -n "$type_flag" ]]; then
|
|
||||||
echo "$type_flag"
|
|
||||||
else
|
|
||||||
echo "none"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# subnet::tunnel_mode <subnet_name> [type_key]
|
# subnet::tunnel_mode <subnet_name> [type_key]
|
||||||
|
|
@ -142,25 +260,31 @@ function subnet::tunnel_mode() {
|
||||||
local subnet_name="${1:-}" type_key="${2:-}"
|
local subnet_name="${1:-}" type_key="${2:-}"
|
||||||
local result
|
local result
|
||||||
result=$(json::subnet_tunnel_mode "$(ctx::subnets)" "$subnet_name" "$type_key" 2>/dev/null) || true
|
result=$(json::subnet_tunnel_mode "$(ctx::subnets)" "$subnet_name" "$type_key" 2>/dev/null) || true
|
||||||
if [[ -n "$result" ]]; then echo "$result"; return 0; fi
|
[[ -n "$result" ]] && { echo "$result"; return 0; }
|
||||||
subnet::_hardcoded_tunnel_mode
|
subnet::_hardcoded_tunnel_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
# subnet::type_from_ip <ip>
|
|
||||||
# Reverse-lookup: given a peer's IP, return its type.
|
|
||||||
# Tries meta file first (by peer name), then subnets.json, then hardcoded.
|
|
||||||
function subnet::type_from_ip() {
|
function subnet::type_from_ip() {
|
||||||
local ip="${1:-}"
|
local ip="${1:-}"
|
||||||
local result
|
[[ -z "$ip" ]] && echo "unknown" && return 0
|
||||||
|
|
||||||
|
# Fast path: hardcoded map covers all production subnets — pure bash, no subshell
|
||||||
|
local type
|
||||||
|
type=$(subnet::_hardcoded_type "$ip")
|
||||||
|
if [[ "$type" != "unknown" ]]; then
|
||||||
|
echo "$type"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Slow path: Python lookup for dynamically-added subnets not in hardcoded map
|
||||||
|
local result
|
||||||
result=$(json::subnet_for_ip "$(ctx::subnets)" "$ip" 2>/dev/null) || true
|
result=$(json::subnet_for_ip "$(ctx::subnets)" "$ip" 2>/dev/null) || true
|
||||||
if [[ -n "$result" ]]; then
|
if [[ -n "$result" ]]; then
|
||||||
# result is "subnet_name|type_key"
|
|
||||||
echo "${result##*|}"
|
echo "${result##*|}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
subnet::_hardcoded_type "$ip"
|
echo "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
# subnet::name_from_ip <ip>
|
# subnet::name_from_ip <ip>
|
||||||
|
|
@ -169,26 +293,34 @@ function subnet::name_from_ip() {
|
||||||
local ip="${1:-}"
|
local ip="${1:-}"
|
||||||
local result
|
local result
|
||||||
result=$(json::subnet_for_ip "$(ctx::subnets)" "$ip" 2>/dev/null) || true
|
result=$(json::subnet_for_ip "$(ctx::subnets)" "$ip" 2>/dev/null) || true
|
||||||
if [[ -n "$result" ]]; then
|
[[ -n "$result" ]] && { echo "${result%%|*}"; return 0; }
|
||||||
echo "${result%%|*}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# ===========================================================================
|
# Returns the default rule for a subnet, or empty string if none configured.
|
||||||
# Validation
|
# Called by add.command.sh to resolve the rule before falling back to "user".
|
||||||
# ===========================================================================
|
function subnet::default_rule() {
|
||||||
|
local subnet_name="${1:-}" type_key="${2:-}"
|
||||||
|
[[ -z "$subnet_name" ]] && return 0
|
||||||
|
json::subnet_default_rule "$(ctx::subnets)" "$subnet_name" "$type_key" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# subnet::list_names
|
||||||
|
# Returns all top-level subnet names, one per line.
|
||||||
|
# Used by commands to dynamically register --<subnet> flags.
|
||||||
|
function subnet::list_names() {
|
||||||
|
json::subnet_list_names "$(ctx::subnets)" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ======================================================
|
||||||
|
# Validation
|
||||||
|
# ======================================================
|
||||||
|
|
||||||
# subnet::exists <name>
|
|
||||||
# Returns 0 if subnet exists in subnets.json, 1 otherwise.
|
|
||||||
function subnet::exists() {
|
function subnet::exists() {
|
||||||
local name="${1:-}"
|
local name="${1:-}"
|
||||||
json::subnet_exists "$(ctx::subnets)" "$name" 2>/dev/null
|
json::subnet_exists "$(ctx::subnets)" "$name" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# subnet::require_exists <name>
|
|
||||||
# Errors and exits if subnet doesn't exist.
|
|
||||||
function subnet::require_exists() {
|
function subnet::require_exists() {
|
||||||
local name="${1:-}"
|
local name="${1:-}"
|
||||||
if ! subnet::exists "$name"; then
|
if ! subnet::exists "$name"; then
|
||||||
|
|
@ -197,9 +329,6 @@ function subnet::require_exists() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# subnet::peers_using <name>
|
|
||||||
# Returns comma-separated list of peer names using this subnet (from meta).
|
|
||||||
# Empty string if none.
|
|
||||||
function subnet::peers_using() {
|
function subnet::peers_using() {
|
||||||
local subnet_name="${1:-}"
|
local subnet_name="${1:-}"
|
||||||
local peers
|
local peers
|
||||||
|
|
@ -213,19 +342,14 @@ function subnet::peers_using() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ======================================================
|
||||||
# Display helpers
|
# Display Data
|
||||||
# ===========================================================================
|
# ======================================================
|
||||||
|
|
||||||
# subnet::list_data
|
|
||||||
# Returns all subnet entries formatted for display.
|
|
||||||
# Output per line: display_name|subnet|type|tunnel_mode|desc|is_group|group_parent
|
|
||||||
function subnet::list_data() {
|
function subnet::list_data() {
|
||||||
json::subnet_list "$(ctx::subnets)" 2>/dev/null || true
|
json::subnet_list "$(ctx::subnets)" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# subnet::show_data <name>
|
|
||||||
# Returns detail lines for a single subnet entry.
|
|
||||||
function subnet::show_data() {
|
function subnet::show_data() {
|
||||||
local name="${1:-}"
|
local name="${1:-}"
|
||||||
json::subnet_show "$(ctx::subnets)" "$name"
|
json::subnet_show "$(ctx::subnets)" "$name"
|
||||||
|
|
|
||||||
4
wgctl
4
wgctl
|
|
@ -9,9 +9,9 @@ LOG_LEVEL=DEBUG
|
||||||
# Modules
|
# Modules
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
load_module config
|
|
||||||
load_module ui
|
|
||||||
load_module ip
|
load_module ip
|
||||||
|
load_module ui
|
||||||
|
load_module config
|
||||||
load_module keys
|
load_module keys
|
||||||
load_module peers
|
load_module peers
|
||||||
load_module firewall
|
load_module firewall
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue