refactor: group::each_peer helper, peer existence checks, group remove cleanup, watch multi-peer filter
This commit is contained in:
parent
51e3443357
commit
a7fd62ce32
26 changed files with 505 additions and 593 deletions
|
|
@ -9,8 +9,8 @@ function cmd::add::on_load() {
|
|||
flag::register --type
|
||||
flag::register --subtype
|
||||
flag::register --rule
|
||||
flag::register --group
|
||||
flag::register --ip
|
||||
flag::register --preset
|
||||
flag::register --guest
|
||||
flag::register --tunnel
|
||||
flag::register --show-config
|
||||
|
|
@ -30,10 +30,12 @@ Add a new WireGuard client.
|
|||
Options:
|
||||
--name <name> Client name (e.g. nuno)
|
||||
--type <type> Device type: desktop, laptop, phone, tablet, guest
|
||||
--subtype <subtype> Guest subtype: desktop, laptop, phone, tablet (mostly used for guest)
|
||||
--ip <ip> Override auto-assigned IP (optional)
|
||||
--preset <name> Apply a firewall preset (repeatable)
|
||||
--guest Shorthand for --type guest
|
||||
--tunnel <mode> Tunnel mode: split (default) or full
|
||||
--tunnel <mode> Tunnel mode: split|full (default: split)
|
||||
--rule <rule> Assign rule on creation (default: user, guest types: guest)
|
||||
--group <group> Add to group on creation (group must exist)
|
||||
--show-config Shows the WireGuard peer config
|
||||
--show-qr Shows the WireGuard config in a QR Code
|
||||
|
||||
|
|
@ -44,6 +46,10 @@ Device Types and Subnets:
|
|||
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
|
||||
|
|
@ -53,7 +59,9 @@ Examples:
|
|||
wgctl add --name nuno --type laptop --ip 10.1.2.5
|
||||
wgctl add --name nuno --type phone --tunnel full
|
||||
wgctl add --name guest1 --type phone --guest
|
||||
wgctl add --name restricted --type desktop --preset no-docker --preset no-proxmox
|
||||
wgctl add --name restricted --type desktop
|
||||
wgctl add --name dev --type laptop --rule dev-01
|
||||
wgctl add --name visitor --type guest --show-qr
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -118,10 +126,10 @@ function cmd::add::run() {
|
|||
local type=""
|
||||
local subtype=""
|
||||
local rule=""
|
||||
local group=""
|
||||
local ip=""
|
||||
local tunnel=""
|
||||
local guest=false
|
||||
local presets=()
|
||||
local show_config=false
|
||||
local show_qr=false
|
||||
|
||||
|
|
@ -132,8 +140,8 @@ function cmd::add::run() {
|
|||
--type) type="$2"; shift 2 ;;
|
||||
--subtype) subtype="$2"; shift 2 ;;
|
||||
--rule) rule="$2"; shift 2 ;;
|
||||
--group) group="$2"; shift 2 ;;
|
||||
--ip) ip="$2"; shift 2 ;;
|
||||
--preset) presets+=("$2"); shift 2 ;;
|
||||
--guest) guest=true; shift ;;
|
||||
--tunnel) tunnel="$2"; shift 2 ;;
|
||||
--show-config) show_config=true; shift ;;
|
||||
|
|
@ -176,14 +184,19 @@ function cmd::add::run() {
|
|||
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
|
||||
|
||||
for preset in "${presets[@]}"; do
|
||||
fw::apply_preset "$preset" "$ip" || return 1
|
||||
done
|
||||
|
||||
[[ -n "$rule" ]] && rule::apply "$rule" "$ip" || return 1
|
||||
peers::reload || return 1
|
||||
|
||||
|
|
|
|||
|
|
@ -28,16 +28,20 @@ Block rules are persisted and restored on WireGuard restart.
|
|||
|
||||
Options:
|
||||
--name <name> Client name (e.g. phone-nuno)
|
||||
--type <type> Device type (optional, combines with --name)
|
||||
--ip <ip> Block access to specific IP (repeatable)
|
||||
--subnet <cidr> Block access to subnet (repeatable)
|
||||
--port <ip:port:proto> Block specific port, e.g. 10.0.0.210:9000:tcp (repeatable)
|
||||
--force Skip confirmation prompt
|
||||
--quiet Suppress output (used by group block)
|
||||
|
||||
Examples:
|
||||
wgctl block --name phone-nuno
|
||||
wgctl block --name nuno --type phone
|
||||
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 ban --name phone-nuno --ip 10.0.0.100 --ip 10.0.0.200
|
||||
wgctl ban --name phone-nuno
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -260,12 +260,19 @@ function cmd::group::show() {
|
|||
printf " %s\n" "$(printf '─%.0s' {1..65})"
|
||||
for peer_name in "${peers_list[@]}"; do
|
||||
[[ -z "$peer_name" ]] && continue
|
||||
|
||||
# Skip if peer no longer exists
|
||||
if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
|
||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
local ip rule status_str status_color
|
||||
ip=$(peers::get_ip "$peer_name")
|
||||
rule=$(peers::get_meta "$peer_name" "rule")
|
||||
rule="${rule:-—}"
|
||||
|
||||
if peers::is_blocked "$peer_name"; then
|
||||
if peers::is_blocked "$peer_name" 2>/dev/null; then
|
||||
status_color="\033[1;31m"
|
||||
status_str="blocked"
|
||||
else
|
||||
|
|
@ -274,14 +281,15 @@ function cmd::group::show() {
|
|||
fi
|
||||
|
||||
printf " %-28s %-15s %-12s %b\n" \
|
||||
"$peer_name" "${ip:-—}" "$rule" \
|
||||
"${status_color}${status_str}\033[0m"
|
||||
"$peer_name" "0" "$rule" \
|
||||
"${status_str}\033[0m"
|
||||
done
|
||||
else
|
||||
printf " —\n"
|
||||
fi
|
||||
|
||||
printf "\n"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -307,15 +315,7 @@ function cmd::group::add() {
|
|||
return 1
|
||||
fi
|
||||
|
||||
local group_file
|
||||
group_file="$(group::path "$name")"
|
||||
|
||||
python3 -c "
|
||||
import json
|
||||
group = {'name': '${name}', 'desc': '${desc}', 'peers': []}
|
||||
with open('${group_file}', 'w') as f:
|
||||
json.dump(group, f, indent=2)
|
||||
" </dev/null
|
||||
json::create_group "$(group::path "$name")" "$name" "$desc"
|
||||
|
||||
log::wg_success "Group created: ${name}"
|
||||
}
|
||||
|
|
@ -479,7 +479,7 @@ function cmd::group::rm_peers() {
|
|||
local peers_list=()
|
||||
mapfile -t peers_list < <(group::peers "$name")
|
||||
local peer_count=${#peers_list[@]}
|
||||
[[ -z "${peers_list[0]}" ]] && peer_count=0
|
||||
[[ -z "${peers_list[0]:-}" ]] && peer_count=0
|
||||
|
||||
if [[ "$peer_count" -eq 0 ]]; then
|
||||
log::wg_warning "Group '${name}' has no peers"
|
||||
|
|
@ -494,16 +494,70 @@ function cmd::group::rm_peers() {
|
|||
esac
|
||||
fi
|
||||
|
||||
local count=0
|
||||
for peer_name in "${peers_list[@]}"; do
|
||||
[[ -z "$peer_name" ]] && continue
|
||||
cmd::remove::run --name "$peer_name" --force
|
||||
(( count++ )) || true
|
||||
done
|
||||
|
||||
log::wg_success "Removed ${count} peers from WireGuard (group '${name}' definition kept)"
|
||||
load_command remove
|
||||
group::each_peer "$name" cmd::group::_rm_peer_cb
|
||||
log::wg_success "Removed peers from group '${name}' (definition kept)"
|
||||
}
|
||||
|
||||
function cmd::group::_rm_peer_cb() {
|
||||
local peer_name="${1:-}"
|
||||
if ! group::_peer_exists_check "$peer_name"; then
|
||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
|
||||
return 0
|
||||
fi
|
||||
cmd::remove::run --name "$peer_name" --force
|
||||
}
|
||||
|
||||
# function cmd::group::rm_peers() {
|
||||
# local name="" force=false
|
||||
|
||||
# while [[ $# -gt 0 ]]; do
|
||||
# case "$1" in
|
||||
# --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
|
||||
# --force) force=true; shift ;;
|
||||
# --help) cmd::group::help; return ;;
|
||||
# *) log::error "Unknown flag: $1"; return 1 ;;
|
||||
# esac
|
||||
# done
|
||||
|
||||
# [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
||||
# group::require_exists "$name" || return 1
|
||||
|
||||
# local peers_list=()
|
||||
# mapfile -t peers_list < <(group::peers "$name")
|
||||
# local peer_count=${#peers_list[@]}
|
||||
# [[ -z "${peers_list[0]}" ]] && peer_count=0
|
||||
|
||||
# if [[ "$peer_count" -eq 0 ]]; then
|
||||
# log::wg_warning "Group '${name}' has no peers"
|
||||
# return 0
|
||||
# fi
|
||||
|
||||
# if ! $force; then
|
||||
# read -r -p "Remove all ${peer_count} peers in group '${name}' from WireGuard? [y/N] " confirm
|
||||
# case "$confirm" in
|
||||
# [yY][eE][sS]|[yY]) ;;
|
||||
# *) log::info "Aborted"; return 0 ;;
|
||||
# esac
|
||||
# fi
|
||||
|
||||
# local count=0
|
||||
# for peer_name in "${peers_list[@]}"; do
|
||||
# [[ -z "$peer_name" ]] && continue
|
||||
|
||||
# # Skip if peer no longer exists
|
||||
# if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
|
||||
# log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
|
||||
# continue
|
||||
# fi
|
||||
|
||||
# cmd::remove::run --name "$peer_name" --force
|
||||
# (( count++ )) || true
|
||||
# done
|
||||
|
||||
# log::wg_success "Removed ${count} peers from WireGuard (group '${name}' definition kept)"
|
||||
# }
|
||||
|
||||
# ============================================
|
||||
# Block / Unblock
|
||||
# ============================================
|
||||
|
|
@ -536,6 +590,12 @@ function cmd::group::block() {
|
|||
local count=0 blocked_names=()
|
||||
for peer_name in "${peers_list[@]}"; do
|
||||
[[ -z "$peer_name" ]] && continue
|
||||
# Skip if peer no longer exists
|
||||
if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
|
||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
if peers::is_blocked "$peer_name"; then
|
||||
log::wg_warning "${peer_name} — already blocked"
|
||||
continue
|
||||
|
|
@ -582,6 +642,12 @@ function cmd::group::unblock() {
|
|||
local count=0 unblocked_names=()
|
||||
for peer_name in "${peers_list[@]}"; do
|
||||
[[ -z "$peer_name" ]] && continue
|
||||
# Skip if peer no longer exists
|
||||
if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
|
||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! peers::is_blocked "$peer_name"; then
|
||||
log::wg_warning "${peer_name} — not blocked"
|
||||
continue
|
||||
|
|
@ -639,16 +705,20 @@ function cmd::group::rule_assign() {
|
|||
|
||||
local peers_list=()
|
||||
mapfile -t peers_list < <(group::peers "$name")
|
||||
[[ -z "${peers_list[0]}" ]] && log::wg_warning "Group '${name}' has no peers" && return 0
|
||||
[[ -z "${peers_list[0]:-}" ]] && log::wg_warning "Group '${name}' has no peers" && return 0
|
||||
|
||||
local count=0
|
||||
for peer_name in "${peers_list[@]}"; do
|
||||
[[ -z "$peer_name" ]] && continue
|
||||
group::each_peer "$name" cmd::group::_rule_assign_cb "$rule"
|
||||
log::wg_success "Assigned rule '${rule}' to group '${name}'"
|
||||
}
|
||||
|
||||
function cmd::group::_rule_assign_cb() {
|
||||
local peer_name="${1:-}" rule="${2:-}"
|
||||
if ! group::_peer_exists_check "$peer_name"; then
|
||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
|
||||
return 0
|
||||
fi
|
||||
load_command rule
|
||||
cmd::rule::assign --name "$rule" --peer "$peer_name"
|
||||
(( count++ )) || true
|
||||
done
|
||||
|
||||
log::wg_success "Assigned rule '${rule}' to ${count} peers in group '${name}'"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -694,6 +764,11 @@ function cmd::group::audit() {
|
|||
|
||||
test::section "Peer Rules"
|
||||
for peer_name in "${peer_args[@]}"; do
|
||||
# Skip if peer no longer exists
|
||||
if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
|
||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
|
||||
continue
|
||||
fi
|
||||
cmd::audit::check_peer "$peer_name" false "${peer_fw_counts[$peer_name]:-0}"
|
||||
done
|
||||
|
||||
|
|
@ -727,6 +802,11 @@ function cmd::group::logs() {
|
|||
|
||||
for peer_name in "${peers_list[@]}"; do
|
||||
[[ -z "$peer_name" ]] && continue
|
||||
# Skip if peer no longer exists
|
||||
if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
|
||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
|
||||
continue
|
||||
fi
|
||||
printf "\n \033[1;37m── %s ──\033[0m\n" "$peer_name"
|
||||
load_command logs
|
||||
cmd::logs::show --name "$peer_name" --limit "$limit"
|
||||
|
|
@ -755,15 +835,18 @@ function cmd::group::watch() {
|
|||
mapfile -t peers_list < <(group::peers "$name")
|
||||
[[ -z "${peers_list[0]}" ]] && log::wg_warning "Group '${name}' has no peers" && return 0
|
||||
|
||||
local peer_filter
|
||||
peer_filter=$(IFS=','; echo "${peers_list[*]}")
|
||||
|
||||
# Build comma-separated peer list for watch filter
|
||||
# Watch already supports --type filter but not multiple peers
|
||||
# For now, use follow mode filtered to group peers one at a time
|
||||
# or just run watch with no filter (shows all, user sees group context)
|
||||
log::section "Live Monitor: Group '${name}'"
|
||||
printf " Monitoring peers: %s\n\n" "${peers_list[*]}"
|
||||
printf " Monitoring: %s\n\n" "${peers_list[*]}"
|
||||
|
||||
load_command logs
|
||||
# Use follow mode — it shows all peers but user knows context
|
||||
# Future: add --peers flag to follow for multi-peer filter
|
||||
cmd::logs::follow "" "" "" false false
|
||||
cmd::logs::follow "" "" "" false false "$peer_filter"
|
||||
}
|
||||
|
|
@ -110,7 +110,7 @@ function cmd::inspect::_rule_info() {
|
|||
ui::print_list "-" "$block_ips"
|
||||
ui::print_list "-" "$block_ports"
|
||||
else
|
||||
ui::row "Blocks" "— (full access)"
|
||||
ui::row "Blocks" "—"
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
function cmd::list::on_load() {
|
||||
flag::register --type
|
||||
flag::register --rule
|
||||
flag::register --group
|
||||
flag::register --online
|
||||
flag::register --offline
|
||||
flag::register --restricted
|
||||
|
|
@ -26,18 +28,22 @@ Usage: wgctl list [options]
|
|||
List all WireGuard clients.
|
||||
|
||||
Options:
|
||||
--type <type> Filter by device type
|
||||
--type <type> Filter by device type (desktop, laptop, phone, tablet, guest)
|
||||
--rule <rule> Filter by assigned rule
|
||||
--group <group> Filter by group membership
|
||||
--online Show only connected clients
|
||||
--offline Show only disconnected clients
|
||||
--allowed Show only fully allowed clients
|
||||
--restricted Show only restricted clients
|
||||
--blocked Show only blocked clients
|
||||
--restricted Show only restricted clients
|
||||
--allowed Show only unrestricted clients
|
||||
--detailed Show full detail cards for all clients
|
||||
--name <name> Show detail card for a single client
|
||||
|
||||
Examples:
|
||||
wgctl list
|
||||
wgctl list --type phone
|
||||
wgctl list --type guest
|
||||
wgctl list --rule user
|
||||
wgctl list --group family
|
||||
wgctl list --online
|
||||
wgctl list --blocked
|
||||
wgctl list --detailed
|
||||
|
|
@ -45,47 +51,6 @@ Examples:
|
|||
EOF
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Precompute helpers
|
||||
# ============================================
|
||||
|
||||
function cmd::list::_precompute_wg() {
|
||||
# Returns two associative arrays via nameref
|
||||
local -n _handshakes="$1"
|
||||
local -n _endpoints="$2"
|
||||
|
||||
while IFS=$'\t' read -r pubkey ts; do
|
||||
[[ -n "$pubkey" ]] && _handshakes["$pubkey"]="$ts"
|
||||
done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
|
||||
|
||||
while IFS=$'\t' read -r pubkey endpoint; do
|
||||
[[ -n "$pubkey" ]] && _endpoints["$pubkey"]="$endpoint"
|
||||
done < <(wg show "$(config::interface)" endpoints 2>/dev/null)
|
||||
}
|
||||
|
||||
function cmd::list::_precompute_block_status() {
|
||||
local -n _blocked="$1"
|
||||
local -n _restricted="$2"
|
||||
|
||||
# Blocked = not in wg server config
|
||||
local wg_peers
|
||||
wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null)
|
||||
|
||||
while IFS= read -r name; do
|
||||
# Check block file
|
||||
[[ -f "$(ctx::block::path "${name}.block")" ]] && _restricted["$name"]=true || _restricted["$name"]=false
|
||||
|
||||
# Check if in server config
|
||||
local pubkey
|
||||
pubkey=$(keys::public "$name" 2>/dev/null || echo "")
|
||||
if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then
|
||||
_blocked["$name"]=true
|
||||
else
|
||||
_blocked["$name"]=false
|
||||
fi
|
||||
done < <(peers::all)
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Status / Display helpers
|
||||
# ============================================
|
||||
|
|
@ -186,7 +151,7 @@ function peers::get_type() {
|
|||
function cmd::list::display_type() {
|
||||
local name="${1:-0}"
|
||||
local type="${2:-0}"
|
||||
local subtype="${3:-0}"
|
||||
local subtype="${3:-}"
|
||||
if config::is_guest_type "$type" && [[ -n "$subtype" ]]; then
|
||||
echo "guest/${subtype}"
|
||||
elif config::is_guest_type "$type"; then
|
||||
|
|
@ -234,9 +199,6 @@ function cmd::list::_render_footer() {
|
|||
function cmd::list::_render_summary() {
|
||||
local group_summary="${1:-}"
|
||||
local -n _rule_counts="$2"
|
||||
local total="${3:-}" # filtered total
|
||||
|
||||
# total=$(find "$(ctx::clients)" -name "*.conf" | wc -l | tr -d ' ')
|
||||
|
||||
# Count total from rule_counts (only filtered peers)
|
||||
local total=0
|
||||
|
|
@ -273,7 +235,7 @@ function cmd::list::show_client() {
|
|||
|
||||
local ip allowed_ips public_key
|
||||
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
||||
allowed_ips=$(grep "^AllowedIPs" "$conf" | awk '{print $3}')
|
||||
allowed_ips=$(grep "^AllowedIPs" "$conf" | cut -d'=' -f2- | xargs)
|
||||
public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
|
||||
|
||||
local type
|
||||
|
|
@ -356,7 +318,7 @@ function cmd::list::is_attempting() { cmd::list::_is_attempting "$@"; }
|
|||
# ============================================
|
||||
|
||||
function cmd::list::run() {
|
||||
local filter_type=""
|
||||
local filter_type="" filter_rule="" filter_group=""
|
||||
local online_only=false offline_only=false
|
||||
local restricted_only=false blocked_only=false allowed_only=false
|
||||
local detailed=false single_name=""
|
||||
|
|
@ -364,6 +326,8 @@ function cmd::list::run() {
|
|||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--type) filter_type="$2"; shift 2 ;;
|
||||
--rule) filter_rule="$2"; shift 2 ;;
|
||||
--group) filter_group="$2"; shift 2 ;;
|
||||
--online) online_only=true; shift ;;
|
||||
--offline) offline_only=true; shift ;;
|
||||
--restricted) restricted_only=true; shift ;;
|
||||
|
|
@ -383,92 +347,45 @@ function cmd::list::run() {
|
|||
# Single detail card
|
||||
if [[ -n "$single_name" ]]; then
|
||||
cmd::list::show_client "$single_name"
|
||||
return
|
||||
return 0
|
||||
fi
|
||||
|
||||
local dir
|
||||
dir="$(ctx::clients)"
|
||||
local confs=("${dir}"/*.conf)
|
||||
if [[ ! -f "${confs[0]}" ]]; then
|
||||
log::wg_list "No clients configured"
|
||||
log::wg_warning "No clients configured"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Precompute everything ──────────────────
|
||||
|
||||
# Peer data (ip, rule, subtype, last_ts, last_evt) — single Python call
|
||||
declare -A 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 — two wg show calls
|
||||
declare -A wg_handshakes wg_endpoints
|
||||
cmd::list::_precompute_wg wg_handshakes wg_endpoints
|
||||
|
||||
# Block/restricted status
|
||||
declare -A p_blocked p_restricted
|
||||
cmd::list::_precompute_block_status p_blocked p_restricted
|
||||
|
||||
# Public keys — read from key files
|
||||
declare -A p_pubkeys
|
||||
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
|
||||
|
||||
# Group map
|
||||
local has_groups=false
|
||||
local groups_dir
|
||||
groups_dir="$(ctx::groups)"
|
||||
local group_files=("${groups_dir}"/*.group)
|
||||
[[ -f "${group_files[0]}" ]] && has_groups=true
|
||||
|
||||
declare -A peer_group_map
|
||||
if $has_groups; then
|
||||
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
|
||||
cmd::list::_precompute_all
|
||||
|
||||
# ── Detailed mode ──────────────────────────
|
||||
|
||||
if $detailed; then
|
||||
log::section "WireGuard Clients"
|
||||
cmd::list::_iter_confs "$filter_type" cmd::list::show_client
|
||||
return
|
||||
cmd::list::_iter_confs "$filter_type" cmd::list::_show_client_safe
|
||||
return 0
|
||||
fi
|
||||
|
||||
# ── Table view ─────────────────────────────
|
||||
# ── Build filter description ───────────────
|
||||
local filter_desc=""
|
||||
cmd::list::_build_filter_desc
|
||||
|
||||
log::section "WireGuard Clients"
|
||||
cmd::list::_render_header $has_groups
|
||||
|
||||
declare -A rule_counts
|
||||
declare -A group_counts
|
||||
# ── Table view ─────────────────────────────
|
||||
declare -A rule_counts=() group_counts=()
|
||||
_list_header_printed=false
|
||||
|
||||
cmd::list::_iter_confs "$filter_type" cmd::list::_render_row
|
||||
|
||||
if [[ "$_list_header_printed" == "true" ]]; then
|
||||
cmd::list::_render_footer $has_groups
|
||||
|
||||
# Build summaries
|
||||
declare -A displayed_rules displayed_groups
|
||||
|
||||
local group_summary=""
|
||||
if $has_groups; then
|
||||
for g in "${!group_counts[@]}"; do
|
||||
group_summary+="${group_counts[$g]} in ${g}, "
|
||||
done
|
||||
group_summary="${group_summary%, }"
|
||||
cmd::list::_build_group_summary
|
||||
cmd::list::_render_summary "$group_summary" rule_counts "$filter_desc"
|
||||
else
|
||||
log::wg_warning "No results found${filter_desc:+ for: ${filter_desc}}"
|
||||
fi
|
||||
|
||||
cmd::list::_render_summary "$group_summary" rule_counts
|
||||
}
|
||||
|
||||
function cmd::list::_iter_confs() {
|
||||
|
|
@ -510,6 +427,11 @@ function cmd::list::_render_row() {
|
|||
if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
|
||||
[[ "$is_restricted" == "true" ]]; }; then return 0; fi
|
||||
|
||||
if [[ -n "$filter_group" ]]; then
|
||||
local peer_group="${peer_group_map[$client_name]:-}"
|
||||
[[ "$peer_group" != "$filter_group" ]] && return 0
|
||||
fi
|
||||
|
||||
# Format display values
|
||||
local status last_seen display_type rule group_display
|
||||
status=$(cmd::list::_format_status "$client_name" "$pubkey" \
|
||||
|
|
@ -520,6 +442,15 @@ function cmd::list::_render_row() {
|
|||
"${p_subtypes[$client_name]:-}")
|
||||
rule="${p_rules[$client_name]:-—}"
|
||||
|
||||
if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi
|
||||
|
||||
# Print header on first match
|
||||
if [[ "${_list_header_printed:-false}" == "false" ]]; then
|
||||
log::section "WireGuard Clients"
|
||||
cmd::list::_render_header $has_groups
|
||||
_list_header_printed=true
|
||||
fi
|
||||
|
||||
# Update rule counts for summary (outer scope array)
|
||||
rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true
|
||||
|
||||
|
|
@ -549,3 +480,100 @@ function cmd::list::_render_row() {
|
|||
"$padded_status" "$last_seen"
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Private helpers
|
||||
# ============================================
|
||||
|
||||
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=()
|
||||
local wg_peers
|
||||
wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null)
|
||||
while IFS= read -r name; do
|
||||
[[ -f "$(ctx::block::path "${name}.block")" ]] \
|
||||
&& p_restricted["$name"]=true || p_restricted["$name"]=false
|
||||
local pubkey
|
||||
pubkey=$(keys::public "$name" 2>/dev/null || echo "")
|
||||
if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then
|
||||
p_blocked["$name"]=true
|
||||
else
|
||||
p_blocked["$name"]=false
|
||||
fi
|
||||
done < <(peers::all)
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
function cmd::list::_build_filter_desc() {
|
||||
filter_desc=""
|
||||
[[ -n "$filter_type" ]] && filter_desc+="type=${filter_type} "
|
||||
[[ -n "$filter_rule" ]] && filter_desc+="rule=${filter_rule} "
|
||||
[[ -n "$filter_group" ]] && filter_desc+="group=${filter_group} "
|
||||
$online_only && filter_desc+="online "
|
||||
$offline_only && filter_desc+="offline "
|
||||
$blocked_only && filter_desc+="blocked "
|
||||
filter_desc="${filter_desc% }"
|
||||
}
|
||||
|
||||
function cmd::list::_build_group_summary() {
|
||||
group_summary=""
|
||||
if $has_groups; then
|
||||
declare -A _gs=()
|
||||
for peer in "${!peer_group_map[@]}"; do
|
||||
local g="${peer_group_map[$peer]}"
|
||||
_gs["$g"]=$(( ${_gs["$g"]:-0} + 1 )) || true
|
||||
done
|
||||
for g in "${!_gs[@]}"; do
|
||||
group_summary+="${_gs[$g]} in ${g}, "
|
||||
done
|
||||
group_summary="${group_summary%, }"
|
||||
fi
|
||||
}
|
||||
|
||||
function cmd::list::_show_client_safe() {
|
||||
local name="$1"
|
||||
cmd::list::show_client "$name" || true
|
||||
}
|
||||
|
|
@ -101,8 +101,10 @@ function cmd::logs::show() {
|
|||
}
|
||||
|
||||
function cmd::logs::follow() {
|
||||
local filter_ip="$1" filter_name="$2" filter_type="$3"
|
||||
local fw_only="$4" wg_only="$5"
|
||||
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}"
|
||||
local fw_only="${4:-false}" wg_only="${5:-false}"
|
||||
local filter_peers="${6:-}"
|
||||
|
||||
local clients_dir
|
||||
clients_dir="$(ctx::clients)"
|
||||
|
||||
|
|
@ -137,7 +139,7 @@ function cmd::logs::follow() {
|
|||
printf " %-20s %-8s %-20s %-25s %b\n" \
|
||||
"$ts" "wireguard" "$client" "$dst_or_endpoint" "$colored_event"
|
||||
fi
|
||||
done < <(json::follow_logs "$fw_log" "$wg_log" "$filter_ip" "$filter_type" "$clients_dir")
|
||||
done < <(json::follow_logs "$fw_log" "$wg_log" "$filter_ip" "$filter_type" "$clients_dir" "$filter_peers")
|
||||
}
|
||||
|
||||
function cmd::logs::remove() {
|
||||
|
|
|
|||
|
|
@ -1,199 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# ============================================
|
||||
# Help
|
||||
# ============================================
|
||||
|
||||
function cmd::preset::help() {
|
||||
cat <<EOF
|
||||
Usage: wgctl preset <subcommand> [options]
|
||||
|
||||
Manage firewall presets.
|
||||
|
||||
Subcommands:
|
||||
list, ls List available presets
|
||||
add, new, create Add a new preset
|
||||
remove, rm, del Remove a preset
|
||||
|
||||
Options for add:
|
||||
--name <name> Preset name (e.g. no-jellyfin)
|
||||
--desc <description> Human readable description
|
||||
--block-ip <ip> Block specific IP (repeatable)
|
||||
--block-subnet <cidr> Block subnet (repeatable)
|
||||
--block-port <ip:port:proto> Block specific port (repeatable)
|
||||
|
||||
Examples:
|
||||
wgctl preset list
|
||||
wgctl preset add --name no-jellyfin --desc "Block Jellyfin" --block-ip 10.0.0.210 --block-port 10.0.0.210:8096:tcp
|
||||
wgctl preset remove --name no-jellyfin
|
||||
EOF
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Run
|
||||
# ============================================
|
||||
|
||||
function cmd::preset::run() {
|
||||
local subcmd="${1:-help}"
|
||||
shift || true
|
||||
|
||||
case "$subcmd" in
|
||||
list|ls) cmd::preset::list "$@" ;;
|
||||
add|new|create) cmd::preset::add "$@" ;;
|
||||
remove|rm|del|delete) cmd::preset::remove "$@" ;;
|
||||
help) cmd::preset::help ;;
|
||||
*)
|
||||
log::error "Unknown subcommand: '${subcmd}'"
|
||||
cmd::preset::help
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# List
|
||||
# ============================================
|
||||
|
||||
function cmd::preset::list() {
|
||||
local dir
|
||||
dir="$(ctx::presets)"
|
||||
|
||||
local presets=("${dir}"/*.preset)
|
||||
if [[ ! -f "${presets[0]}" ]]; then
|
||||
log::wg_preset "No presets configured"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log::section "Available Presets"
|
||||
|
||||
printf "\n %-25s %-40s %s\n" "NAME" "DESCRIPTION" "RULES"
|
||||
printf " %s\n" "$(printf '─%.0s' {1..75})"
|
||||
|
||||
for preset_file in "${dir}"/*.preset; do
|
||||
[[ -f "$preset_file" ]] || continue
|
||||
|
||||
# Reset vars before sourcing
|
||||
local PRESET_NAME="" PRESET_DESC=""
|
||||
local BLOCK_IPS="" BLOCK_SUBNETS="" BLOCK_PORTS=""
|
||||
|
||||
source "$preset_file"
|
||||
|
||||
local rules=""
|
||||
[[ -n "$BLOCK_IPS" ]] && rules+="IPs:$(echo "$BLOCK_IPS" | wc -w) "
|
||||
[[ -n "$BLOCK_SUBNETS" ]] && rules+="Subnets:$(echo "$BLOCK_SUBNETS" | wc -w) "
|
||||
[[ -n "$BLOCK_PORTS" ]] && rules+="Ports:$(echo "$BLOCK_PORTS" | wc -w)"
|
||||
|
||||
printf " %-25s %-40s %s\n" \
|
||||
"$PRESET_NAME" \
|
||||
"${PRESET_DESC:-—}" \
|
||||
"${rules:-—}"
|
||||
done
|
||||
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Add
|
||||
# ============================================
|
||||
|
||||
function cmd::preset::add() {
|
||||
local name=""
|
||||
local desc=""
|
||||
local block_ips=()
|
||||
local block_subnets=()
|
||||
local block_ports=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--desc) desc="$2"; shift 2 ;;
|
||||
--block-ip) block_ips+=("$2"); shift 2 ;;
|
||||
--block-subnet) block_subnets+=("$2"); shift 2 ;;
|
||||
--block-port) block_ports+=("$2"); shift 2 ;;
|
||||
--help) cmd::preset::help; return ;;
|
||||
*)
|
||||
log::error "Unknown flag: $1"
|
||||
cmd::preset::help
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$name" ]]; then
|
||||
log::error "Missing required flag: --name"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ${#block_ips[@]} -eq 0 && ${#block_subnets[@]} -eq 0 && ${#block_ports[@]} -eq 0 ]]; then
|
||||
log::error "At least one of --block-ip, --block-subnet, or --block-port is required"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local preset_file
|
||||
preset_file="$(ctx::preset::path "${name}.preset")"
|
||||
|
||||
if [[ -f "$preset_file" ]]; then
|
||||
log::error "Preset already exists: ${name}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
cat > "$preset_file" <<EOF
|
||||
# wgctl preset — ${name}
|
||||
PRESET_NAME="${name}"
|
||||
PRESET_DESC="${desc}"
|
||||
BLOCK_IPS="${block_ips[*]:-}"
|
||||
BLOCK_SUBNETS="${block_subnets[*]:-}"
|
||||
BLOCK_PORTS="${block_ports[*]:-}"
|
||||
EOF
|
||||
|
||||
log::wg_success "Preset created: ${name}"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Remove
|
||||
# ============================================
|
||||
|
||||
function cmd::preset::remove() {
|
||||
local name=""
|
||||
local force=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--force) force=true; shift ;;
|
||||
--help) cmd::preset::help; return ;;
|
||||
*)
|
||||
log::error "Unknown flag: $1"
|
||||
cmd::preset::help
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$name" ]]; then
|
||||
log::error "Missing required flag: --name"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local preset_file
|
||||
preset_file="$(ctx::preset::path "${name}.preset")"
|
||||
|
||||
if [[ ! -f "$preset_file" ]]; then
|
||||
log::error "Preset not found: ${name}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! $force; then
|
||||
read -r -p "Are you sure you want to remove preset '${name}'? [y/N] " confirm
|
||||
case "$confirm" in
|
||||
[yY][eE][sS]|[yY]) ;;
|
||||
*)
|
||||
log::info "Aborted"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
rm -f "$preset_file"
|
||||
log::wg_success "Preset removed: ${name}"
|
||||
}
|
||||
|
|
@ -93,6 +93,7 @@ function cmd::remove::_cleanup() {
|
|||
peers::remove_from_server "$name" || return 1
|
||||
peers::remove_client_config "$name" || return 1
|
||||
keys::remove "$name" || return 1
|
||||
group::remove_peer_from_all "$name" || return 1
|
||||
|
||||
[[ -n "$client_ip" ]] && $was_blocked && fw::unblock_all "$client_ip"
|
||||
fw::remove_block_file "$name" 2>/dev/null || true
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ function cmd::shell::_is_wgctl_command() {
|
|||
local known=(
|
||||
list add remove rm inspect block unblock
|
||||
rule group audit logs watch fw config qr
|
||||
rename keys ip preset service shell help
|
||||
rename keys ip service shell help
|
||||
)
|
||||
local c
|
||||
for c in "${known[@]}"; do
|
||||
|
|
|
|||
|
|
@ -138,13 +138,12 @@ function cmd::test::run_function() {
|
|||
function cmd::test::section_list() {
|
||||
test::section "List"
|
||||
cmd::test::run_cmd "list" "WireGuard Clients" list
|
||||
cmd::test::run_cmd "list --online" "STATUS" list --online
|
||||
cmd::test::run_cmd "list --offline" "STATUS" list --offline
|
||||
cmd::test::run_cmd "list --blocked" "STATUS" list --blocked
|
||||
cmd::test::run_cmd "list --type phone" "phone" list --type phone
|
||||
cmd::test::run_cmd "list --type guest" "guest" list --type guest
|
||||
# TODO: Fix detailed, hangs
|
||||
# cmd::test::run_cmd "list --detailed" "Client:" list --detailed
|
||||
cmd::test::run_cmd "list --online" "" list --online
|
||||
cmd::test::run_cmd "list --offline" "" list --offline
|
||||
cmd::test::run_cmd "list --blocked" "" list --blocked
|
||||
cmd::test::run_cmd "list --type phone" "" list --type phone
|
||||
cmd::test::run_cmd "list --type guest" "" list --type guest
|
||||
cmd::test::run_cmd "list --detailed" "Client:" list --detailed
|
||||
cmd::test::run_cmd "list --name phone-nuno" "phone-nuno" list --name phone-nuno
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,15 +28,19 @@ Remove block rules for a client.
|
|||
|
||||
Options:
|
||||
--name <name> Client name (e.g. phone-nuno)
|
||||
--type <type> Device type (optional, combines with --name)
|
||||
--ip <ip> Unblock specific IP (repeatable)
|
||||
--subnet <cidr> Unblock specific subnet (repeatable)
|
||||
--port <ip:port:proto> Unblock specific port (repeatable)
|
||||
--all Remove all block rules for this client
|
||||
--force Skip confirmation prompt
|
||||
--quiet Suppress output (used by group unblock)
|
||||
|
||||
Examples:
|
||||
wgctl unblock --name phone-nuno --all
|
||||
wgctl unblock --name phone-nuno
|
||||
wgctl unblock --name nuno --type phone
|
||||
wgctl unblock --name phone-nuno --ip 10.0.0.210
|
||||
wgctl unban --name phone-nuno --all
|
||||
wgctl unban --name phone-nuno
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +131,6 @@ function cmd::unblock::_unblock_all() {
|
|||
local client_ip="${2:-}"
|
||||
local quiet="${3:-false}"
|
||||
|
||||
log::debug "_unblock_all: name=$name ip=$client_ip"
|
||||
fw::unblock_all "$client_ip"
|
||||
fw::remove_block_file "$name"
|
||||
monitor::unwatch_client "$name"
|
||||
|
|
@ -135,7 +138,6 @@ function cmd::unblock::_unblock_all() {
|
|||
if ! peers::exists_in_server "$name"; then
|
||||
local public_key
|
||||
public_key=$(keys::public "$name") || return 1
|
||||
log::debug "_unblock_all: adding to server pub=$public_key"
|
||||
peers::add_to_server "$name" "$public_key" "$client_ip"
|
||||
peers::reload
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -161,17 +161,13 @@ function cmd::watch::tail_events() {
|
|||
[[ -z "$line" ]] && continue
|
||||
|
||||
local event_data
|
||||
event_data=$(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
e = json.loads('${line//\'/\'\\\'\'}')
|
||||
print(e.get('timestamp',''), e.get('client',''), e.get('endpoint',''), e.get('event',''))
|
||||
except:
|
||||
pass
|
||||
" 2>/dev/null)
|
||||
event_data=$(json::parse_event "$line")
|
||||
|
||||
[[ -z "$event_data" ]] && continue
|
||||
|
||||
local ts client endpoint event
|
||||
IFS="|" read -r ts client endpoint event <<< "$event_data"
|
||||
|
||||
if $restricted_only; then
|
||||
local conf
|
||||
conf="$(ctx::clients)/${client}.conf"
|
||||
|
|
@ -179,9 +175,6 @@ except:
|
|||
cmd::list::is_restricted "$client" || continue
|
||||
fi
|
||||
|
||||
local ts client endpoint event
|
||||
read -r ts client endpoint event <<< "$event_data"
|
||||
|
||||
# Apply filters
|
||||
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ _CTX_WG="/etc/wireguard"
|
|||
_CTX_CORE="${_CTX_ROOT}/core"
|
||||
_CTX_MODULES="${_CTX_ROOT}/modules"
|
||||
_CTX_COMMANDS="${_CTX_ROOT}/commands"
|
||||
_CTX_PRESETS="${_CTX_ROOT}/presets"
|
||||
_CTX_CLIENTS="${_CTX_WG}/clients"
|
||||
_CTX_DATA="${_CTX_WG}/.wgctl"
|
||||
|
||||
|
|
@ -29,7 +28,6 @@ function ctx::root() { echo "$_CTX_ROOT"; }
|
|||
function ctx::core() { echo "$_CTX_CORE"; }
|
||||
function ctx::modules() { echo "$_CTX_MODULES"; }
|
||||
function ctx::commands() { echo "$_CTX_COMMANDS"; }
|
||||
function ctx::presets() { echo "$_CTX_PRESETS"; }
|
||||
function ctx::blocks() { echo "$_CTX_BLOCKS"; }
|
||||
function ctx::groups() { echo "$_CTX_GROUPS"; }
|
||||
function ctx::rules() { echo "$_CTX_RULES"; }
|
||||
|
|
@ -54,11 +52,6 @@ function ctx::client::path() {
|
|||
echo "$_CTX_CLIENTS/$*"
|
||||
}
|
||||
|
||||
function ctx::preset::path() {
|
||||
local IFS="/"
|
||||
echo "$_CTX_PRESETS/$*"
|
||||
}
|
||||
|
||||
function ctx::meta::path() {
|
||||
local IFS="/"
|
||||
echo "$_CTX_META/$*"
|
||||
|
|
|
|||
|
|
@ -28,3 +28,7 @@ function json::rule_list_data() { python3 "$JSON_HELPER" rule_list_data "$@"
|
|||
function json::group_list_data() { python3 "$JSON_HELPER" group_list_data "$@" </dev/null; }
|
||||
function json::fmt_datetime() { python3 "$JSON_HELPER" fmt_datetime "$@" </dev/null; }
|
||||
function json::create_rule() { python3 "$JSON_HELPER" create_rule "$@" </dev/null; }
|
||||
function json::cleanup_config() { python3 "$JSON_HELPER" cleanup_config "$@" </dev/null; }
|
||||
function json::remove_peer_block() { python3 "$JSON_HELPER" remove_peer_block "$@" </dev/null; }
|
||||
function json::create_group() { python3 "$JSON_HELPER" create_group "$@" </dev/null; }
|
||||
function json::parse_event() { python3 "$JSON_HELPER" parse_event "$@" </dev/null; }
|
||||
|
|
@ -349,11 +349,12 @@ def remove_events(file, identifier):
|
|||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def follow_logs(fw_file, wg_file, filter_ip, filter_type, clients_dir):
|
||||
def follow_logs(fw_file, wg_file, filter_ip, filter_type, clients_dir, filter_peers=""):
|
||||
"""Follow both log files and output formatted events"""
|
||||
import glob, time, select
|
||||
|
||||
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
|
||||
peer_filter = set(filter_peers.split(',')) if filter_peers else set()
|
||||
|
||||
# Build ip->name map
|
||||
ip_to_name = {}
|
||||
|
|
@ -424,6 +425,9 @@ def follow_logs(fw_file, wg_file, filter_ip, filter_type, clients_dir):
|
|||
ip = ip_to_name.get(filter_ip, '')
|
||||
if client != ip and client != filter_ip:
|
||||
continue
|
||||
|
||||
if peer_filter and client not in peer_filter:
|
||||
continue
|
||||
if filter_type and not client.startswith(filter_type + '-'):
|
||||
continue
|
||||
ts = e.get('timestamp', '')[:16].replace('T', ' ')
|
||||
|
|
@ -638,6 +642,52 @@ def create_rule(file, name, desc, dns_redirect, allow_ips, block_ips, block_port
|
|||
with open(file, 'w') as f:
|
||||
json.dump(rule, f, indent=2)
|
||||
|
||||
def cleanup_config(config_file):
|
||||
"""Normalize blank lines in WireGuard config"""
|
||||
import re
|
||||
try:
|
||||
with open(config_file) as f:
|
||||
config = f.read()
|
||||
config = re.sub(r'\n{3,}', '\n\n', config)
|
||||
config = config.rstrip('\n') + '\n'
|
||||
with open(config_file, 'w') as f:
|
||||
f.write(config)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def remove_peer_block(config_file, name):
|
||||
"""Remove a peer block from WireGuard config by name"""
|
||||
import re
|
||||
try:
|
||||
with open(config_file) as f:
|
||||
config = f.read()
|
||||
pattern = r'\n\[Peer\]\n# ' + re.escape(name) + r'\n[^\n]+\n[^\n]+\n'
|
||||
result = re.sub(pattern, '\n', config)
|
||||
with open(config_file, 'w') as f:
|
||||
f.write(result)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def create_group(file, name, desc):
|
||||
"""Create a new group JSON file"""
|
||||
try:
|
||||
group = {'name': name, 'desc': desc, 'peers': []}
|
||||
with open(file, 'w') as f:
|
||||
json.dump(group, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def parse_event(line):
|
||||
"""Parse a single JSON event line"""
|
||||
try:
|
||||
e = json.loads(line)
|
||||
print(f"{e.get('timestamp','')}|{e.get('client','')}|{e.get('endpoint','')}|{e.get('event','')}")
|
||||
except:
|
||||
pass
|
||||
|
||||
commands = {
|
||||
'get': lambda args: get(args[0], args[1]),
|
||||
'set': lambda args: set_key(args[0], args[1], args[2]),
|
||||
|
|
@ -654,7 +704,7 @@ commands = {
|
|||
'format_fw_event': lambda args: format_fw_event(sys.stdin.read(), args[0]),
|
||||
'format_wg_event': lambda args: format_wg_event(sys.stdin.read()),
|
||||
'remove_events': lambda args: remove_events(args[0], args[1]),
|
||||
'follow_logs': lambda args: follow_logs(args[0], args[1], args[2], args[3], args[4]),
|
||||
'follow_logs': lambda args: follow_logs(args[0], args[1], args[2], args[3], args[4], args[5]),
|
||||
'count': lambda args: count(args[0], args[1]),
|
||||
'audit_fw_counts': lambda args: audit_fw_counts(args[0]),
|
||||
'peer_group_map': lambda args: peer_group_map(args[0]),
|
||||
|
|
@ -665,7 +715,10 @@ commands = {
|
|||
'group_list_data': lambda args: group_list_data(args[0], args[1]),
|
||||
'fmt_datetime': lambda args: fmt_datetime(args[0], args[1]),
|
||||
'create_rule': lambda args: create_rule(args[0], args[1], args[2], args[3], args[4], args[5], args[6]),
|
||||
|
||||
'cleanup_config': lambda args: cleanup_config(args[0]),
|
||||
'remove_peer_block': lambda args: remove_peer_block(args[0], args[1]),
|
||||
'create_group': lambda args: create_group(args[0], args[1], args[2]),
|
||||
'parse_event': lambda args: parse_event(args[0]),
|
||||
}
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"phone-fred": "94.63.0.129",
|
||||
"phone-helena": "148.69.37.26",
|
||||
"phone-helena": "148.69.44.173",
|
||||
"phone-nuno": "94.63.0.129",
|
||||
"tablet-nuno": "148.69.202.5",
|
||||
"guest-zephyr": "5.13.82.5",
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ function config::load() {
|
|||
WG_LAN) _WG_LAN="$value" ;;
|
||||
# Add debug temporarily to config::load:
|
||||
DATE_FORMAT)
|
||||
log::debug "config: setting date format to $value"
|
||||
_FMT_DATE_FORMAT="$value"
|
||||
fmt::set_date_format "$value"
|
||||
;;
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ function fw::unallow_port() {
|
|||
}
|
||||
|
||||
function fw::flush_peer() {
|
||||
local client_ip="$1"
|
||||
local client_ip="${1:?client_ip required}"
|
||||
log::debug "flush_peer: starting for $client_ip"
|
||||
|
||||
# Collect line numbers into array
|
||||
|
|
@ -205,47 +205,3 @@ function fw::restore_blocks() {
|
|||
log::debug "Restored block rules for: ${name}"
|
||||
done
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Preset Application
|
||||
# ============================================
|
||||
|
||||
function fw::apply_preset() {
|
||||
local name="$1"
|
||||
local client_ip="$2"
|
||||
local preset_file
|
||||
preset_file="$(ctx::preset::path "${name}.preset")"
|
||||
|
||||
if [[ ! -f "$preset_file" ]]; then
|
||||
log::error "Preset not found: ${name}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
source "$preset_file"
|
||||
|
||||
if [[ -n "${BLOCK_IPS:-}" ]]; then
|
||||
for ip in $BLOCK_IPS; do
|
||||
fw::block_ip "$client_ip" "$ip"
|
||||
fw::save_block "$client_ip" "$client_ip" "$ip"
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -n "${BLOCK_SUBNETS:-}" ]]; then
|
||||
for subnet in $BLOCK_SUBNETS; do
|
||||
fw::block_subnet "$client_ip" "$subnet"
|
||||
fw::save_block "$client_ip" "$client_ip" "$subnet"
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -n "${BLOCK_PORTS:-}" ]]; then
|
||||
for entry in $BLOCK_PORTS; do
|
||||
local target port proto
|
||||
IFS=":" read -r target port proto <<< "$entry"
|
||||
proto="${proto:-tcp}"
|
||||
fw::block_port "$client_ip" "$target" "$port" "$proto"
|
||||
fw::save_block "$name" "$client_ip" "$target" "$port" "$proto"
|
||||
done
|
||||
fi
|
||||
|
||||
log::debug "Applied preset '${name}' to: ${client_ip}"
|
||||
}
|
||||
|
|
@ -51,3 +51,36 @@ function group::peer_groups() {
|
|||
fi
|
||||
done < <(group::all)
|
||||
}
|
||||
|
||||
function group::remove_peer_from_all() {
|
||||
local peer_name="${1:?peer_name required}"
|
||||
while IFS= read -r group_name; do
|
||||
group::remove_peer "$group_name" "$peer_name"
|
||||
done < <(json::peer_groups "$(ctx::groups)" "$peer_name")
|
||||
}
|
||||
|
||||
function group::each_peer() {
|
||||
local name="${1:?name required}"
|
||||
local callback="${2:?callback required}"
|
||||
shift 2
|
||||
# $@ = extra args passed to callback
|
||||
|
||||
local peers_list=()
|
||||
mapfile -t peers_list < <(group::peers "$name")
|
||||
|
||||
local filtered=()
|
||||
for p in "${peers_list[@]:-}"; do
|
||||
[[ -n "$p" ]] && filtered+=("$p")
|
||||
done
|
||||
|
||||
[[ ${#filtered[@]} -eq 0 ]] && return 0
|
||||
|
||||
for peer_name in "${filtered[@]}"; do
|
||||
"$callback" "$peer_name" "$@"
|
||||
done
|
||||
}
|
||||
|
||||
function group::_peer_exists_check() {
|
||||
local peer_name="${1:-}"
|
||||
peers::require_exists "$peer_name" > /dev/null 2>&1
|
||||
}
|
||||
|
|
@ -60,27 +60,14 @@ function peers::remove_client_config() {
|
|||
# ============================================
|
||||
|
||||
function peers::cleanup_config() {
|
||||
local config
|
||||
config=$(config::config_file)
|
||||
|
||||
python3 -c "
|
||||
import re
|
||||
config = open('${config}').read()
|
||||
|
||||
# Normalize multiple blank lines to single blank line
|
||||
config = re.sub(r'\n{3,}', '\n\n', config)
|
||||
|
||||
# Ensure file ends with single newline
|
||||
config = config.rstrip('\n') + '\n'
|
||||
|
||||
open('${config}', 'w').write(config)
|
||||
"
|
||||
json::cleanup_config "$(config::config_file)"
|
||||
}
|
||||
|
||||
|
||||
function peers::add_to_server() {
|
||||
local name="$1"
|
||||
local public_key="$2"
|
||||
local ip="$3"
|
||||
local name="${1:?name required}"
|
||||
local public_key="${2:?public_key required}"
|
||||
local ip="${3:?ip required}"
|
||||
|
||||
local config
|
||||
config=$(config::config_file)
|
||||
|
|
@ -97,21 +84,12 @@ EOF
|
|||
}
|
||||
|
||||
function peers::remove_block() {
|
||||
local name="$1"
|
||||
local config
|
||||
config=$(config::config_file)
|
||||
|
||||
python3 -c "
|
||||
import re
|
||||
config = open('${config}').read()
|
||||
pattern = r'\n\[Peer\]\n# ${name}\n[^\n]+\n[^\n]+\n'
|
||||
result = re.sub(pattern, '\n', config)
|
||||
open('${config}', 'w').write(result)
|
||||
"
|
||||
local name="${1:?name required}"
|
||||
json::remove_peer_block "$(config::config_file)" "$name"
|
||||
}
|
||||
|
||||
function peers::remove_from_server() {
|
||||
local name="$1"
|
||||
local name="${1:?name required}"
|
||||
peers::remove_block "$name"
|
||||
peers::cleanup_config
|
||||
log::debug "Removed peer from server config: ${name}"
|
||||
|
|
@ -183,8 +161,8 @@ function peers::exists_in_server() {
|
|||
}
|
||||
|
||||
function peers::is_blocked() {
|
||||
local name="$1"
|
||||
! peers::exists_in_server "$name"
|
||||
local name="${1:-}"
|
||||
peers::exists_in_server "$name" && return 1 || return 0
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -248,7 +226,7 @@ function peers::with_rule() {
|
|||
function peers::get_ip() {
|
||||
local name="$1"
|
||||
grep "^Address" "$(ctx::clients)/${name}.conf" 2>/dev/null \
|
||||
| awk '{print $3}' | cut -d'/' -f1
|
||||
| awk '{print $3}' | cut -d'/' -f1 || true
|
||||
}
|
||||
|
||||
function peers::find_by_ip() {
|
||||
|
|
|
|||
|
|
@ -72,13 +72,11 @@ function rule::is_applied() {
|
|||
# ============================================
|
||||
|
||||
function rule::apply() {
|
||||
local rule_name="$1"
|
||||
local client_ip="$2"
|
||||
local rule_name="${1:?rule_name required}"
|
||||
local client_ip="${2:?client_ip required}"
|
||||
local peer_name="${3:-}" # optional, avoids find_by_ip call
|
||||
|
||||
log::debug "rule::apply ENTRY: rule=$rule_name ip=$client_ip peer=$peer_name"
|
||||
rule::require_exists "$rule_name" || return 1
|
||||
log::debug "rule::apply: exists check passed"
|
||||
|
||||
# Use provided peer_name or look it up
|
||||
if [[ -z "$peer_name" ]]; then
|
||||
|
|
@ -100,7 +98,6 @@ function rule::apply() {
|
|||
# Check if already applied
|
||||
local peer_name
|
||||
peer_name=$(peers::find_by_ip "$client_ip")
|
||||
log::debug "rule::apply: find_by_ip($client_ip) = '$peer_name'"
|
||||
if [[ -n "$peer_name" ]]; then
|
||||
# Check if already applied via iptables
|
||||
if rule::is_applied "$rule_name" "$client_ip"; then
|
||||
|
|
@ -140,10 +137,8 @@ function rule::apply() {
|
|||
done < <(rule::get "$rule_name" "allow_ports")
|
||||
|
||||
# Persist rule assignment in meta
|
||||
log::debug "rule::apply: peer_name=$peer_name ip=$client_ip"
|
||||
if [[ -n "$peer_name" ]]; then
|
||||
peers::set_meta "$peer_name" "rule" "$rule_name"
|
||||
log::debug "rule::apply: set meta rule=$rule_name for $peer_name"
|
||||
fi
|
||||
|
||||
local dns_redirect
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
PRESET_NAME="guest"
|
||||
PRESET_DESC="Internet only, no LAN access"
|
||||
BLOCK_IPS=""
|
||||
BLOCK_SUBNETS="10.0.0.0/24"
|
||||
BLOCK_PORTS=""
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
PRESET_NAME="no-docker"
|
||||
PRESET_DESC="Block access to Docker host"
|
||||
BLOCK_IPS="10.0.0.210"
|
||||
BLOCK_SUBNETS=""
|
||||
BLOCK_PORTS=""
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
PRESET_NAME="no-internet"
|
||||
PRESET_DESC="LAN access only, no internet"
|
||||
BLOCK_IPS=""
|
||||
BLOCK_SUBNETS="0.0.0.0/0"
|
||||
BLOCK_PORTS=""
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
PRESET_NAME="no-proxmox"
|
||||
PRESET_DESC="Block access to Proxmox"
|
||||
BLOCK_IPS="10.0.0.100"
|
||||
BLOCK_SUBNETS=""
|
||||
BLOCK_PORTS=""
|
||||
5
wgctl
5
wgctl
|
|
@ -120,11 +120,6 @@ Service Commands:
|
|||
enable Enable WireGuard on boot
|
||||
disable Disable WireGuard on boot
|
||||
|
||||
Preset Commands:
|
||||
preset list List available presets
|
||||
preset add Add a new preset
|
||||
preset remove Remove a preset
|
||||
|
||||
Run 'wgctl <command> --help' for command-specific help.
|
||||
EOF
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue