feat: duplicate rule validation, peer command, fallback DNS
- rule assign: block if rule already in peer's identity - identity rule assign: --migrate flag to remove conflicting direct peer rules - commands/peer.command.sh: update-dns and update-tunnel subcommands - config.sh: config::dns_fallback, config::dns_string - peers.module.sh: peers::get_display_subnet extraction - wgctl peer update-dns --all: retrofits existing peer configs with fallback DNS - wgctl.conf: WG_DNS_FALLBACK support
This commit is contained in:
parent
d14db5e85c
commit
794e75bc9b
6 changed files with 270 additions and 12 deletions
|
|
@ -30,6 +30,7 @@ function cmd::identity::on_load() {
|
|||
flag::register --unset-auto-apply
|
||||
flag::register --field
|
||||
flag::register --value
|
||||
flag::register --migrate
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -358,11 +359,12 @@ function cmd::identity::_rule() {
|
|||
}
|
||||
|
||||
function cmd::identity::_rule_assign() {
|
||||
local name="" rule=""
|
||||
local name="" rule="" migrate=false
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--rule) rule="$2"; shift 2 ;;
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--rule) rule="$2"; shift 2 ;;
|
||||
--migrate) migrate=true; shift ;;
|
||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||
esac
|
||||
done
|
||||
|
|
@ -372,6 +374,29 @@ function cmd::identity::_rule_assign() {
|
|||
identity::require_exists "$name" || return 1
|
||||
rule::exists "$rule" || { log::error "Rule not found: ${rule}"; return 1; }
|
||||
|
||||
local conflicts=()
|
||||
while IFS= read -r peer_name; do
|
||||
[[ -z "$peer_name" ]] && continue
|
||||
local peer_rule
|
||||
peer_rule=$(peers::get_meta "$peer_name" "rule" 2>/dev/null)
|
||||
[[ "$peer_rule" == "$rule" ]] && conflicts+=("$peer_name")
|
||||
done < <(identity::peers "$name")
|
||||
|
||||
if [[ ${#conflicts[@]} -gt 0 ]]; then
|
||||
if ! $migrate; then
|
||||
log::error "The following peers have '${rule}' as a direct rule: ${conflicts[*]}"
|
||||
log::error "Use --migrate to remove direct rules and let the identity rule take over."
|
||||
return 1
|
||||
fi
|
||||
# Migrate — remove direct rules from conflicting peers
|
||||
for peer_name in "${conflicts[@]}"; do
|
||||
local ip
|
||||
ip=$(peers::get_ip "$peer_name")
|
||||
rule::unapply "$rule" "$ip"
|
||||
log::wg "Migrated '${rule}' from peer '${peer_name}' to identity '${name}'"
|
||||
done
|
||||
fi
|
||||
|
||||
local exit_code
|
||||
identity::add_rule "$name" "$rule" || exit_code=$?
|
||||
|
||||
|
|
|
|||
|
|
@ -419,13 +419,10 @@ function cmd::list::_render_detailed() {
|
|||
ui::peer::list_identity_header "$id_name"
|
||||
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||
[[ -z "$name" ]] && continue
|
||||
local subnet
|
||||
subnet=$(peers::get_meta "$name" "subnet" 2>/dev/null)
|
||||
if [[ -z "$subnet" ]]; then
|
||||
local peer_type="${p_types[$name]:-}"
|
||||
[[ -n "$peer_type" ]] && subnet="$peer_type"
|
||||
fi
|
||||
[[ -z "$subnet" ]] && subnet="-"
|
||||
|
||||
local peer_type="${p_types[$name]:-}"
|
||||
subnet=$(peers::get_display_subnet "$name" "$peer_type")
|
||||
|
||||
ui::peer::list_row_detailed \
|
||||
"$w_name" "$w_ip" "$w_type" "$w_rule" "$w_group" "$w_subnet" \
|
||||
"$name" "$ip" "$type" "$rule" "$group" "$subnet" \
|
||||
|
|
|
|||
197
commands/peer.command.sh
Normal file
197
commands/peer.command.sh
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
#!/usr/bin/env bash
|
||||
# peer.command.sh — peer management operations
|
||||
|
||||
# ============================================
|
||||
# Lifecycle
|
||||
# ============================================
|
||||
|
||||
function cmd::peer::on_load() {
|
||||
flag::register --name
|
||||
flag::register --type
|
||||
flag::register --all
|
||||
flag::register --mode
|
||||
flag::register --dns
|
||||
flag::register --fallback-dns
|
||||
flag::register --force
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Help
|
||||
# ============================================
|
||||
|
||||
function cmd::peer::help() {
|
||||
cat <<EOF
|
||||
Usage: wgctl peer <subcommand> [options]
|
||||
|
||||
Manage peer configuration and settings.
|
||||
|
||||
Subcommands:
|
||||
update-dns Update DNS settings in client config(s)
|
||||
update-tunnel Update tunnel mode (split/full) in client config(s)
|
||||
|
||||
Options for update-dns:
|
||||
--name <name> Target peer
|
||||
--all Apply to all peers
|
||||
--type <type> Filter by device type
|
||||
--dns <ip> Primary DNS (default: from config)
|
||||
--fallback-dns <ips> Fallback DNS servers (comma-separated)
|
||||
Default: from WG_DNS_FALLBACK in wgctl.conf
|
||||
|
||||
Options for update-tunnel:
|
||||
--name <name> Target peer
|
||||
--all Apply to all peers
|
||||
--type <type> Filter by device type
|
||||
--mode <mode> Tunnel mode: split | full
|
||||
--force Skip confirmation for --all
|
||||
|
||||
Examples:
|
||||
wgctl peer update-dns --all
|
||||
wgctl peer update-dns --name phone-nuno
|
||||
wgctl peer update-dns --name phone-nuno --fallback-dns 9.9.9.9,1.1.1.1
|
||||
wgctl peer update-tunnel --all --mode split
|
||||
wgctl peer update-tunnel --name phone-nuno --mode full
|
||||
EOF
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Run
|
||||
# ============================================
|
||||
|
||||
function cmd::peer::run() {
|
||||
local subcmd="${1:-help}"
|
||||
shift || true
|
||||
case "$subcmd" in
|
||||
update-dns) cmd::peer::update_dns "$@" ;;
|
||||
update-tunnel) cmd::peer::update_tunnel "$@" ;;
|
||||
help) cmd::peer::help ;;
|
||||
*)
|
||||
log::error "Unknown subcommand: '${subcmd}'"
|
||||
cmd::peer::help
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Update DNS
|
||||
# ============================================
|
||||
|
||||
function cmd::peer::update_dns() {
|
||||
local name="" type="" all=false
|
||||
local dns="" fallback_dns=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--type) type="$2"; shift 2 ;;
|
||||
--all) all=true; shift ;;
|
||||
--dns) dns="$2"; shift 2 ;;
|
||||
--fallback-dns) fallback_dns="$2"; shift 2 ;;
|
||||
--help) cmd::peer::help; return ;;
|
||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$name" && "$all" == "false" ]] && \
|
||||
log::error "Specify --name or --all" && return 1
|
||||
|
||||
# Resolve DNS string
|
||||
local primary="${dns:-$(config::dns)}"
|
||||
local fallback="${fallback_dns:-$(config::dns_fallback)}"
|
||||
local dns_string
|
||||
if [[ -n "$fallback" ]]; then
|
||||
dns_string="${primary}, ${fallback}"
|
||||
else
|
||||
dns_string="$primary"
|
||||
fi
|
||||
|
||||
# Collect target peers
|
||||
local peers=()
|
||||
if $all; then
|
||||
while IFS= read -r conf; do
|
||||
peers+=("$(basename "$conf" .conf)")
|
||||
done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null)
|
||||
else
|
||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||
peers=("$name")
|
||||
fi
|
||||
|
||||
local updated=0
|
||||
for peer_name in "${peers[@]}"; do
|
||||
local conf
|
||||
conf="$(ctx::clients)/${peer_name}.conf"
|
||||
[[ ! -f "$conf" ]] && continue
|
||||
|
||||
# Replace DNS line in-place
|
||||
if grep -q "^DNS" "$conf"; then
|
||||
sed -i "s|^DNS = .*|DNS = ${dns_string}|" "$conf"
|
||||
else
|
||||
# Add DNS line after Address line
|
||||
sed -i "/^Address/a DNS = ${dns_string}" "$conf"
|
||||
fi
|
||||
(( updated++ )) || true
|
||||
log::debug "Updated DNS for: ${peer_name}"
|
||||
done
|
||||
|
||||
log::wg_success "Updated DNS to '${dns_string}' for ${updated} peer(s)"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Update Tunnel
|
||||
# ============================================
|
||||
|
||||
function cmd::peer::update_tunnel() {
|
||||
local name="" type="" all=false mode="" force=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--type) type="$2"; shift 2 ;;
|
||||
--all) all=true; shift ;;
|
||||
--mode) mode="$2"; shift 2 ;;
|
||||
--force) force=true; shift ;;
|
||||
--help) cmd::peer::help; return ;;
|
||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$name" && "$all" == "false" ]] && \
|
||||
log::error "Specify --name or --all" && return 1
|
||||
[[ -z "$mode" ]] && \
|
||||
log::error "Missing required flag: --mode (split|full)" && return 1
|
||||
[[ "$mode" != "split" && "$mode" != "full" ]] && \
|
||||
log::error "Invalid mode: ${mode} (must be split or full)" && return 1
|
||||
|
||||
local allowed_ips
|
||||
allowed_ips=$(config::allowed_ips_for "$mode")
|
||||
|
||||
# Collect target peers
|
||||
local peers=()
|
||||
if $all; then
|
||||
if ! $force; then
|
||||
read -r -p "Update tunnel mode to '${mode}' for ALL peers? [y/N] " confirm
|
||||
case "$confirm" in [yY]*) ;; *) log::info "Aborted"; return 0 ;; esac
|
||||
fi
|
||||
while IFS= read -r conf; do
|
||||
peers+=("$(basename "$conf" .conf)")
|
||||
done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null)
|
||||
else
|
||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||
peers=("$name")
|
||||
fi
|
||||
|
||||
local updated=0
|
||||
for peer_name in "${peers[@]}"; do
|
||||
local conf
|
||||
conf="$(ctx::clients)/${peer_name}.conf"
|
||||
[[ ! -f "$conf" ]] && continue
|
||||
|
||||
# Replace AllowedIPs line in-place
|
||||
sed -i "s|^AllowedIPs = .*|AllowedIPs = ${allowed_ips}|" "$conf"
|
||||
(( updated++ )) || true
|
||||
log::debug "Updated tunnel for: ${peer_name}"
|
||||
done
|
||||
|
||||
log::wg_success "Updated tunnel to '${mode}' (${allowed_ips}) for ${updated} peer(s)"
|
||||
log::wg "Peers must reconnect to apply the new tunnel mode"
|
||||
}
|
||||
|
|
@ -551,6 +551,19 @@ function cmd::rule::assign() {
|
|||
|
||||
peer=$(peers::resolve_and_require "$peer" "$type") || return 1
|
||||
|
||||
# Identity rule check
|
||||
local peer_identity
|
||||
|
||||
peer_identity=$(peers::get_meta "$peer" "identity")
|
||||
if [[ -n "$peer_identity" ]]; then
|
||||
local identity_rules
|
||||
identity_rules=$(identity::rules "$peer_identity" 2>/dev/null)
|
||||
if echo "$identity_rules" | grep -qx "$name"; then
|
||||
log::error "Rule '${name}' is already applied to '${peer}' via identity '${peer_identity}' — cannot assign directly"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
local existing_rule ip
|
||||
existing_rule=$(peers::get_meta "$peer" "rule")
|
||||
ip=$(peers::get_ip "$peer")
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ declare -g _ACTIVITY_CURRENT_HIGH_BYTES="${ACTIVITY_CURRENT_HIGH_BYTES:-10000000
|
|||
function config::_init_defaults() {
|
||||
_WG_INTERFACE="${WG_INTERFACE:-wg0}"
|
||||
_WG_DNS="${WG_DNS:-10.0.0.103}"
|
||||
_WG_DNS_FALLBACK="${WG_DNS_FALLBACK:-}"
|
||||
_WG_LAN="${WG_LAN:-10.0.0.0/24}"
|
||||
_WG_SUBNET="${WG_SUBNET:-10.1.0.0/16}"
|
||||
_WG_PORT="${WG_PORT:-51820}"
|
||||
|
|
@ -127,6 +128,7 @@ function config::load() {
|
|||
WG_INTERFACE) _WG_INTERFACE="$value" ;;
|
||||
WG_ENDPOINT) _WG_ENDPOINT="$value" ;;
|
||||
WG_DNS) _WG_DNS="$value" ;;
|
||||
WG_DNS_FALLBACK) _WG_DNS_FALLBACK="$value" ;;
|
||||
WG_PORT) _WG_PORT="$value" ;;
|
||||
WG_SUBNET) _WG_SUBNET="$value" ;;
|
||||
WG_LAN) _WG_LAN="$value" ;;
|
||||
|
|
@ -154,6 +156,7 @@ function config::interface() { echo "$_WG_INTERFACE"; }
|
|||
function config::config_file() { echo "$_WG_CONFIG"; }
|
||||
function config::endpoint() { echo "$_WG_ENDPOINT"; }
|
||||
function config::dns() { echo "$_WG_DNS"; }
|
||||
function config::dns_fallback() { echo "${_WG_DNS_FALLBACK:-}"; }
|
||||
function config::port() { echo "$_WG_PORT"; }
|
||||
function config::subnet() { echo "$_WG_SUBNET"; }
|
||||
function config::lan() { echo "$_WG_LAN"; }
|
||||
|
|
@ -178,4 +181,15 @@ function config::allowed_ips_for() {
|
|||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
}
|
||||
|
||||
function config::dns_string() {
|
||||
local fallback
|
||||
fallback=$(config::dns_fallback)
|
||||
if [[ -n "$fallback" ]]; then
|
||||
echo "$(config::dns), ${fallback}"
|
||||
else
|
||||
echo "$(config::dns)"
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ function peers::create_client_config() {
|
|||
[Interface]
|
||||
PrivateKey = ${private_key}
|
||||
Address = ${ip}/32
|
||||
DNS = $(config::dns)
|
||||
DNS = $(config::dns_string)
|
||||
|
||||
[Peer]
|
||||
PublicKey = ${server_public_key}
|
||||
|
|
@ -271,6 +271,18 @@ function peers::set_main_group() {
|
|||
peers::set_meta "$name" "main_group" "$group"
|
||||
}
|
||||
|
||||
function peers::get_display_subnet() {
|
||||
local peer_name="${1:-}" peer_type="${2:-}"
|
||||
local subnet
|
||||
subnet=$(peers::get_meta "$peer_name" "subnet" 2>/dev/null)
|
||||
if [[ -z "$subnet" ]]; then
|
||||
subnet=$(subnet::type_from_ip "$(peers::get_ip "$peer_name")" 2>/dev/null)
|
||||
[[ -n "$peer_type" ]] && subnet="$peer_type"
|
||||
fi
|
||||
[[ -z "$subnet" ]] && subnet="-"
|
||||
echo "$subnet"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Name + Type Parsing
|
||||
# ============================================
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue