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 --type
|
||||||
flag::register --subtype
|
flag::register --subtype
|
||||||
flag::register --rule
|
flag::register --rule
|
||||||
|
flag::register --group
|
||||||
flag::register --ip
|
flag::register --ip
|
||||||
flag::register --preset
|
|
||||||
flag::register --guest
|
flag::register --guest
|
||||||
flag::register --tunnel
|
flag::register --tunnel
|
||||||
flag::register --show-config
|
flag::register --show-config
|
||||||
|
|
@ -30,10 +30,12 @@ Add a new WireGuard client.
|
||||||
Options:
|
Options:
|
||||||
--name <name> Client name (e.g. nuno)
|
--name <name> Client name (e.g. nuno)
|
||||||
--type <type> Device type: desktop, laptop, phone, tablet, guest
|
--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)
|
--ip <ip> Override auto-assigned IP (optional)
|
||||||
--preset <name> Apply a firewall preset (repeatable)
|
|
||||||
--guest Shorthand for --type guest
|
--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-config Shows the WireGuard peer config
|
||||||
--show-qr Shows the WireGuard config in a QR Code
|
--show-qr Shows the WireGuard config in a QR Code
|
||||||
|
|
||||||
|
|
@ -44,6 +46,10 @@ Device Types and Subnets:
|
||||||
tablet 10.1.4.x
|
tablet 10.1.4.x
|
||||||
guest 10.1.100.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:
|
Tunnel Modes:
|
||||||
split Route only VPN subnet + LAN through WireGuard (default)
|
split Route only VPN subnet + LAN through WireGuard (default)
|
||||||
full Route all traffic through WireGuard
|
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 laptop --ip 10.1.2.5
|
||||||
wgctl add --name nuno --type phone --tunnel full
|
wgctl add --name nuno --type phone --tunnel full
|
||||||
wgctl add --name guest1 --type phone --guest
|
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
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,10 +126,10 @@ function cmd::add::run() {
|
||||||
local type=""
|
local type=""
|
||||||
local subtype=""
|
local subtype=""
|
||||||
local rule=""
|
local rule=""
|
||||||
|
local group=""
|
||||||
local ip=""
|
local ip=""
|
||||||
local tunnel=""
|
local tunnel=""
|
||||||
local guest=false
|
local guest=false
|
||||||
local presets=()
|
|
||||||
local show_config=false
|
local show_config=false
|
||||||
local show_qr=false
|
local show_qr=false
|
||||||
|
|
||||||
|
|
@ -132,8 +140,8 @@ function cmd::add::run() {
|
||||||
--type) type="$2"; shift 2 ;;
|
--type) type="$2"; shift 2 ;;
|
||||||
--subtype) subtype="$2"; shift 2 ;;
|
--subtype) subtype="$2"; shift 2 ;;
|
||||||
--rule) rule="$2"; shift 2 ;;
|
--rule) rule="$2"; shift 2 ;;
|
||||||
|
--group) group="$2"; shift 2 ;;
|
||||||
--ip) ip="$2"; shift 2 ;;
|
--ip) ip="$2"; shift 2 ;;
|
||||||
--preset) presets+=("$2"); shift 2 ;;
|
|
||||||
--guest) guest=true; shift ;;
|
--guest) guest=true; shift ;;
|
||||||
--tunnel) tunnel="$2"; shift 2 ;;
|
--tunnel) tunnel="$2"; shift 2 ;;
|
||||||
--show-config) show_config=true; shift ;;
|
--show-config) show_config=true; shift ;;
|
||||||
|
|
@ -176,16 +184,21 @@ function cmd::add::run() {
|
||||||
peers::create_client_config "$full_name" "$effective_type" "$ip" "$allowed_ips" || return 1
|
peers::create_client_config "$full_name" "$effective_type" "$ip" "$allowed_ips" || return 1
|
||||||
[[ -n "$subtype" ]] && peers::set_meta "$full_name" "subtype" "$subtype"
|
[[ -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
|
local public_key
|
||||||
public_key=$(keys::public "$full_name") || return 1
|
public_key=$(keys::public "$full_name") || return 1
|
||||||
peers::add_to_server "$full_name" "$public_key" "$ip" || 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
|
[[ -n "$rule" ]] && rule::apply "$rule" "$ip" || return 1
|
||||||
peers::reload || return 1
|
peers::reload || return 1
|
||||||
|
|
||||||
log::wg_success "Client added successfully: ${full_name} (${ip}) [${tunnel} tunnel]"
|
log::wg_success "Client added successfully: ${full_name} (${ip}) [${tunnel} tunnel]"
|
||||||
cmd::add::_show_result "$full_name" "${subtype:-$type}"
|
cmd::add::_show_result "$full_name" "${subtype:-$type}"
|
||||||
|
|
|
||||||
|
|
@ -27,17 +27,21 @@ Block a client entirely or restrict access to specific IPs/ports/subnets.
|
||||||
Block rules are persisted and restored on WireGuard restart.
|
Block rules are persisted and restored on WireGuard restart.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--name <name> Client name (e.g. phone-nuno)
|
--name <name> Client name (e.g. phone-nuno)
|
||||||
--ip <ip> Block access to specific IP (repeatable)
|
--type <type> Device type (optional, combines with --name)
|
||||||
--subnet <cidr> Block access to subnet (repeatable)
|
--ip <ip> Block access to specific IP (repeatable)
|
||||||
--port <ip:port:proto> Block specific port, e.g. 10.0.0.210:9000:tcp (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:
|
Examples:
|
||||||
wgctl block --name phone-nuno
|
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 --ip 10.0.0.210
|
||||||
wgctl block --name phone-nuno --subnet 10.0.0.0/24
|
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 --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
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -260,12 +260,19 @@ function cmd::group::show() {
|
||||||
printf " %s\n" "$(printf '─%.0s' {1..65})"
|
printf " %s\n" "$(printf '─%.0s' {1..65})"
|
||||||
for peer_name in "${peers_list[@]}"; do
|
for peer_name in "${peers_list[@]}"; do
|
||||||
[[ -z "$peer_name" ]] && continue
|
[[ -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
|
local ip rule status_str status_color
|
||||||
ip=$(peers::get_ip "$peer_name")
|
ip=$(peers::get_ip "$peer_name")
|
||||||
rule=$(peers::get_meta "$peer_name" "rule")
|
rule=$(peers::get_meta "$peer_name" "rule")
|
||||||
rule="${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_color="\033[1;31m"
|
||||||
status_str="blocked"
|
status_str="blocked"
|
||||||
else
|
else
|
||||||
|
|
@ -274,14 +281,15 @@ function cmd::group::show() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf " %-28s %-15s %-12s %b\n" \
|
printf " %-28s %-15s %-12s %b\n" \
|
||||||
"$peer_name" "${ip:-—}" "$rule" \
|
"$peer_name" "0" "$rule" \
|
||||||
"${status_color}${status_str}\033[0m"
|
"${status_str}\033[0m"
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
printf " —\n"
|
printf " —\n"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf "\n"
|
printf "\n"
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -307,15 +315,7 @@ function cmd::group::add() {
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local group_file
|
json::create_group "$(group::path "$name")" "$name" "$desc"
|
||||||
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
|
|
||||||
|
|
||||||
log::wg_success "Group created: ${name}"
|
log::wg_success "Group created: ${name}"
|
||||||
}
|
}
|
||||||
|
|
@ -479,7 +479,7 @@ function cmd::group::rm_peers() {
|
||||||
local peers_list=()
|
local peers_list=()
|
||||||
mapfile -t peers_list < <(group::peers "$name")
|
mapfile -t peers_list < <(group::peers "$name")
|
||||||
local peer_count=${#peers_list[@]}
|
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
|
if [[ "$peer_count" -eq 0 ]]; then
|
||||||
log::wg_warning "Group '${name}' has no peers"
|
log::wg_warning "Group '${name}' has no peers"
|
||||||
|
|
@ -494,16 +494,70 @@ function cmd::group::rm_peers() {
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local count=0
|
load_command remove
|
||||||
for peer_name in "${peers_list[@]}"; do
|
group::each_peer "$name" cmd::group::_rm_peer_cb
|
||||||
[[ -z "$peer_name" ]] && continue
|
log::wg_success "Removed peers from group '${name}' (definition kept)"
|
||||||
cmd::remove::run --name "$peer_name" --force
|
|
||||||
(( count++ )) || true
|
|
||||||
done
|
|
||||||
|
|
||||||
log::wg_success "Removed ${count} peers from WireGuard (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
|
# Block / Unblock
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -536,6 +590,12 @@ function cmd::group::block() {
|
||||||
local count=0 blocked_names=()
|
local count=0 blocked_names=()
|
||||||
for peer_name in "${peers_list[@]}"; do
|
for peer_name in "${peers_list[@]}"; do
|
||||||
[[ -z "$peer_name" ]] && continue
|
[[ -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
|
if peers::is_blocked "$peer_name"; then
|
||||||
log::wg_warning "${peer_name} — already blocked"
|
log::wg_warning "${peer_name} — already blocked"
|
||||||
continue
|
continue
|
||||||
|
|
@ -582,6 +642,12 @@ function cmd::group::unblock() {
|
||||||
local count=0 unblocked_names=()
|
local count=0 unblocked_names=()
|
||||||
for peer_name in "${peers_list[@]}"; do
|
for peer_name in "${peers_list[@]}"; do
|
||||||
[[ -z "$peer_name" ]] && continue
|
[[ -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
|
if ! peers::is_blocked "$peer_name"; then
|
||||||
log::wg_warning "${peer_name} — not blocked"
|
log::wg_warning "${peer_name} — not blocked"
|
||||||
continue
|
continue
|
||||||
|
|
@ -635,20 +701,24 @@ function cmd::group::rule_assign() {
|
||||||
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
||||||
[[ -z "$rule" ]] && log::error "Missing required flag: --rule" && return 1
|
[[ -z "$rule" ]] && log::error "Missing required flag: --rule" && return 1
|
||||||
group::require_exists "$name" || return 1
|
group::require_exists "$name" || return 1
|
||||||
rule::require_exists "$rule" || return 1
|
rule::require_exists "$rule" || return 1
|
||||||
|
|
||||||
local peers_list=()
|
local peers_list=()
|
||||||
mapfile -t peers_list < <(group::peers "$name")
|
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
|
group::each_peer "$name" cmd::group::_rule_assign_cb "$rule"
|
||||||
for peer_name in "${peers_list[@]}"; do
|
log::wg_success "Assigned rule '${rule}' to group '${name}'"
|
||||||
[[ -z "$peer_name" ]] && continue
|
}
|
||||||
cmd::rule::assign --name "$rule" --peer "$peer_name"
|
|
||||||
(( count++ )) || true
|
|
||||||
done
|
|
||||||
|
|
||||||
log::wg_success "Assigned rule '${rule}' to ${count} peers in 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"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -694,6 +764,11 @@ function cmd::group::audit() {
|
||||||
|
|
||||||
test::section "Peer Rules"
|
test::section "Peer Rules"
|
||||||
for peer_name in "${peer_args[@]}"; do
|
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}"
|
cmd::audit::check_peer "$peer_name" false "${peer_fw_counts[$peer_name]:-0}"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
@ -727,6 +802,11 @@ function cmd::group::logs() {
|
||||||
|
|
||||||
for peer_name in "${peers_list[@]}"; do
|
for peer_name in "${peers_list[@]}"; do
|
||||||
[[ -z "$peer_name" ]] && continue
|
[[ -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"
|
printf "\n \033[1;37m── %s ──\033[0m\n" "$peer_name"
|
||||||
load_command logs
|
load_command logs
|
||||||
cmd::logs::show --name "$peer_name" --limit "$limit"
|
cmd::logs::show --name "$peer_name" --limit "$limit"
|
||||||
|
|
@ -755,15 +835,18 @@ function cmd::group::watch() {
|
||||||
mapfile -t peers_list < <(group::peers "$name")
|
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 peer_filter
|
||||||
|
peer_filter=$(IFS=','; echo "${peers_list[*]}")
|
||||||
|
|
||||||
# Build comma-separated peer list for watch filter
|
# Build comma-separated peer list for watch filter
|
||||||
# Watch already supports --type filter but not multiple peers
|
# Watch already supports --type filter but not multiple peers
|
||||||
# For now, use follow mode filtered to group peers one at a time
|
# 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)
|
# or just run watch with no filter (shows all, user sees group context)
|
||||||
log::section "Live Monitor: Group '${name}'"
|
log::section "Live Monitor: Group '${name}'"
|
||||||
printf " Monitoring peers: %s\n\n" "${peers_list[*]}"
|
printf " Monitoring: %s\n\n" "${peers_list[*]}"
|
||||||
|
|
||||||
load_command logs
|
load_command logs
|
||||||
# Use follow mode — it shows all peers but user knows context
|
# Use follow mode — it shows all peers but user knows context
|
||||||
# Future: add --peers flag to follow for multi-peer filter
|
# 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_ips"
|
||||||
ui::print_list "-" "$block_ports"
|
ui::print_list "-" "$block_ports"
|
||||||
else
|
else
|
||||||
ui::row "Blocks" "— (full access)"
|
ui::row "Blocks" "—"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
|
|
||||||
function cmd::list::on_load() {
|
function cmd::list::on_load() {
|
||||||
flag::register --type
|
flag::register --type
|
||||||
|
flag::register --rule
|
||||||
|
flag::register --group
|
||||||
flag::register --online
|
flag::register --online
|
||||||
flag::register --offline
|
flag::register --offline
|
||||||
flag::register --restricted
|
flag::register --restricted
|
||||||
|
|
@ -26,18 +28,22 @@ Usage: wgctl list [options]
|
||||||
List all WireGuard clients.
|
List all WireGuard clients.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--type <type> Filter by device type
|
--type <type> Filter by device type (desktop, laptop, phone, tablet, guest)
|
||||||
--online Show only connected clients
|
--rule <rule> Filter by assigned rule
|
||||||
--offline Show only disconnected clients
|
--group <group> Filter by group membership
|
||||||
--allowed Show only fully allowed clients
|
--online Show only connected clients
|
||||||
--restricted Show only restricted clients
|
--offline Show only disconnected clients
|
||||||
--blocked Show only blocked clients
|
--blocked Show only blocked clients
|
||||||
--detailed Show full detail cards for all clients
|
--restricted Show only restricted clients
|
||||||
--name <name> Show detail card for a single client
|
--allowed Show only unrestricted clients
|
||||||
|
--detailed Show full detail cards for all clients
|
||||||
|
--name <name> Show detail card for a single client
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
wgctl list
|
wgctl list
|
||||||
wgctl list --type phone
|
wgctl list --type guest
|
||||||
|
wgctl list --rule user
|
||||||
|
wgctl list --group family
|
||||||
wgctl list --online
|
wgctl list --online
|
||||||
wgctl list --blocked
|
wgctl list --blocked
|
||||||
wgctl list --detailed
|
wgctl list --detailed
|
||||||
|
|
@ -45,47 +51,6 @@ Examples:
|
||||||
EOF
|
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
|
# Status / Display helpers
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -186,7 +151,7 @@ function peers::get_type() {
|
||||||
function cmd::list::display_type() {
|
function cmd::list::display_type() {
|
||||||
local name="${1:-0}"
|
local name="${1:-0}"
|
||||||
local type="${2:-0}"
|
local type="${2:-0}"
|
||||||
local subtype="${3:-0}"
|
local subtype="${3:-}"
|
||||||
if config::is_guest_type "$type" && [[ -n "$subtype" ]]; then
|
if config::is_guest_type "$type" && [[ -n "$subtype" ]]; then
|
||||||
echo "guest/${subtype}"
|
echo "guest/${subtype}"
|
||||||
elif config::is_guest_type "$type"; then
|
elif config::is_guest_type "$type"; then
|
||||||
|
|
@ -234,9 +199,6 @@ 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 total="${3:-}" # filtered total
|
|
||||||
|
|
||||||
# total=$(find "$(ctx::clients)" -name "*.conf" | wc -l | tr -d ' ')
|
|
||||||
|
|
||||||
# Count total from rule_counts (only filtered peers)
|
# Count total from rule_counts (only filtered peers)
|
||||||
local total=0
|
local total=0
|
||||||
|
|
@ -273,7 +235,7 @@ function cmd::list::show_client() {
|
||||||
|
|
||||||
local ip allowed_ips public_key
|
local ip allowed_ips public_key
|
||||||
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
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")
|
public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
local type
|
local type
|
||||||
|
|
@ -356,7 +318,7 @@ function cmd::list::is_attempting() { cmd::list::_is_attempting "$@"; }
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function cmd::list::run() {
|
function cmd::list::run() {
|
||||||
local filter_type=""
|
local filter_type="" filter_rule="" filter_group=""
|
||||||
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=""
|
||||||
|
|
@ -364,6 +326,8 @@ function cmd::list::run() {
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--type) filter_type="$2"; shift 2 ;;
|
--type) filter_type="$2"; shift 2 ;;
|
||||||
|
--rule) filter_rule="$2"; shift 2 ;;
|
||||||
|
--group) filter_group="$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 ;;
|
||||||
|
|
@ -383,92 +347,45 @@ function cmd::list::run() {
|
||||||
# Single detail card
|
# 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
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local dir
|
local dir
|
||||||
dir="$(ctx::clients)"
|
dir="$(ctx::clients)"
|
||||||
local confs=("${dir}"/*.conf)
|
local confs=("${dir}"/*.conf)
|
||||||
if [[ ! -f "${confs[0]}" ]]; then
|
if [[ ! -f "${confs[0]}" ]]; then
|
||||||
log::wg_list "No clients configured"
|
log::wg_warning "No clients configured"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Precompute everything ──────────────────
|
# ── Precompute everything ──────────────────
|
||||||
|
cmd::list::_precompute_all
|
||||||
# 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
|
|
||||||
|
|
||||||
# ── Detailed mode ──────────────────────────
|
# ── 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
|
cmd::list::_iter_confs "$filter_type" cmd::list::_show_client_safe
|
||||||
return
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Table view ─────────────────────────────
|
# ── Build filter description ───────────────
|
||||||
|
local filter_desc=""
|
||||||
|
cmd::list::_build_filter_desc
|
||||||
|
|
||||||
log::section "WireGuard Clients"
|
# ── Table view ─────────────────────────────
|
||||||
cmd::list::_render_header $has_groups
|
declare -A rule_counts=() group_counts=()
|
||||||
|
_list_header_printed=false
|
||||||
declare -A rule_counts
|
|
||||||
declare -A group_counts
|
|
||||||
|
|
||||||
cmd::list::_iter_confs "$filter_type" cmd::list::_render_row
|
cmd::list::_iter_confs "$filter_type" cmd::list::_render_row
|
||||||
cmd::list::_render_footer $has_groups
|
|
||||||
|
|
||||||
# Build summaries
|
if [[ "$_list_header_printed" == "true" ]]; then
|
||||||
declare -A displayed_rules displayed_groups
|
cmd::list::_render_footer $has_groups
|
||||||
|
local group_summary=""
|
||||||
local group_summary=""
|
cmd::list::_build_group_summary
|
||||||
if $has_groups; then
|
cmd::list::_render_summary "$group_summary" rule_counts "$filter_desc"
|
||||||
for g in "${!group_counts[@]}"; do
|
else
|
||||||
group_summary+="${group_counts[$g]} in ${g}, "
|
log::wg_warning "No results found${filter_desc:+ for: ${filter_desc}}"
|
||||||
done
|
|
||||||
group_summary="${group_summary%, }"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cmd::list::_render_summary "$group_summary" rule_counts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::list::_iter_confs() {
|
function cmd::list::_iter_confs() {
|
||||||
|
|
@ -510,6 +427,11 @@ function cmd::list::_render_row() {
|
||||||
if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
|
if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
|
||||||
[[ "$is_restricted" == "true" ]]; }; then return 0; fi
|
[[ "$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
|
# Format display values
|
||||||
local status last_seen display_type rule group_display
|
local status last_seen display_type rule group_display
|
||||||
status=$(cmd::list::_format_status "$client_name" "$pubkey" \
|
status=$(cmd::list::_format_status "$client_name" "$pubkey" \
|
||||||
|
|
@ -520,6 +442,15 @@ function cmd::list::_render_row() {
|
||||||
"${p_subtypes[$client_name]:-}")
|
"${p_subtypes[$client_name]:-}")
|
||||||
rule="${p_rules[$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)
|
# Update rule counts for summary (outer scope array)
|
||||||
rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true
|
rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true
|
||||||
|
|
||||||
|
|
@ -549,3 +480,100 @@ function cmd::list::_render_row() {
|
||||||
"$padded_status" "$last_seen"
|
"$padded_status" "$last_seen"
|
||||||
fi
|
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() {
|
function cmd::logs::follow() {
|
||||||
local filter_ip="$1" filter_name="$2" filter_type="$3"
|
local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}"
|
||||||
local fw_only="$4" wg_only="$5"
|
local fw_only="${4:-false}" wg_only="${5:-false}"
|
||||||
|
local filter_peers="${6:-}"
|
||||||
|
|
||||||
local clients_dir
|
local clients_dir
|
||||||
clients_dir="$(ctx::clients)"
|
clients_dir="$(ctx::clients)"
|
||||||
|
|
||||||
|
|
@ -137,7 +139,7 @@ function cmd::logs::follow() {
|
||||||
printf " %-20s %-8s %-20s %-25s %b\n" \
|
printf " %-20s %-8s %-20s %-25s %b\n" \
|
||||||
"$ts" "wireguard" "$client" "$dst_or_endpoint" "$colored_event"
|
"$ts" "wireguard" "$client" "$dst_or_endpoint" "$colored_event"
|
||||||
fi
|
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() {
|
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_from_server "$name" || return 1
|
||||||
peers::remove_client_config "$name" || return 1
|
peers::remove_client_config "$name" || return 1
|
||||||
keys::remove "$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"
|
[[ -n "$client_ip" ]] && $was_blocked && fw::unblock_all "$client_ip"
|
||||||
fw::remove_block_file "$name" 2>/dev/null || true
|
fw::remove_block_file "$name" 2>/dev/null || true
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ function cmd::shell::_is_wgctl_command() {
|
||||||
local known=(
|
local known=(
|
||||||
list add remove rm inspect block unblock
|
list add remove rm inspect block unblock
|
||||||
rule group audit logs watch fw config qr
|
rule group audit logs watch fw config qr
|
||||||
rename keys ip preset service shell help
|
rename keys ip service shell help
|
||||||
)
|
)
|
||||||
local c
|
local c
|
||||||
for c in "${known[@]}"; do
|
for c in "${known[@]}"; do
|
||||||
|
|
|
||||||
|
|
@ -138,13 +138,12 @@ function cmd::test::run_function() {
|
||||||
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" "WireGuard Clients" list
|
||||||
cmd::test::run_cmd "list --online" "STATUS" list --online
|
cmd::test::run_cmd "list --online" "" list --online
|
||||||
cmd::test::run_cmd "list --offline" "STATUS" list --offline
|
cmd::test::run_cmd "list --offline" "" list --offline
|
||||||
cmd::test::run_cmd "list --blocked" "STATUS" list --blocked
|
cmd::test::run_cmd "list --blocked" "" list --blocked
|
||||||
cmd::test::run_cmd "list --type phone" "phone" list --type phone
|
cmd::test::run_cmd "list --type phone" "" list --type phone
|
||||||
cmd::test::run_cmd "list --type guest" "guest" list --type guest
|
cmd::test::run_cmd "list --type guest" "" list --type guest
|
||||||
# TODO: Fix detailed, hangs
|
cmd::test::run_cmd "list --detailed" "Client:" list --detailed
|
||||||
# cmd::test::run_cmd "list --detailed" "Client:" 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,16 +27,20 @@ Usage: wgctl unblock --name <name> [options]
|
||||||
Remove block rules for a client.
|
Remove block rules for a client.
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--name <name> Client name (e.g. phone-nuno)
|
--name <name> Client name (e.g. phone-nuno)
|
||||||
--ip <ip> Unblock specific IP (repeatable)
|
--type <type> Device type (optional, combines with --name)
|
||||||
--subnet <cidr> Unblock specific subnet (repeatable)
|
--ip <ip> Unblock specific IP (repeatable)
|
||||||
--port <ip:port:proto> Unblock specific port (repeatable)
|
--subnet <cidr> Unblock specific subnet (repeatable)
|
||||||
--all Remove all block rules for this client
|
--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:
|
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 unblock --name phone-nuno --ip 10.0.0.210
|
||||||
wgctl unban --name phone-nuno --all
|
wgctl unban --name phone-nuno
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,7 +131,6 @@ function cmd::unblock::_unblock_all() {
|
||||||
local client_ip="${2:-}"
|
local client_ip="${2:-}"
|
||||||
local quiet="${3:-false}"
|
local quiet="${3:-false}"
|
||||||
|
|
||||||
log::debug "_unblock_all: name=$name ip=$client_ip"
|
|
||||||
fw::unblock_all "$client_ip"
|
fw::unblock_all "$client_ip"
|
||||||
fw::remove_block_file "$name"
|
fw::remove_block_file "$name"
|
||||||
monitor::unwatch_client "$name"
|
monitor::unwatch_client "$name"
|
||||||
|
|
@ -135,7 +138,6 @@ function cmd::unblock::_unblock_all() {
|
||||||
if ! peers::exists_in_server "$name"; then
|
if ! peers::exists_in_server "$name"; then
|
||||||
local public_key
|
local public_key
|
||||||
public_key=$(keys::public "$name") || return 1
|
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::add_to_server "$name" "$public_key" "$client_ip"
|
||||||
peers::reload
|
peers::reload
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -158,72 +158,65 @@ function cmd::watch::tail_events() {
|
||||||
declare -A _WATCH_LAST_ATTEMPT=()
|
declare -A _WATCH_LAST_ATTEMPT=()
|
||||||
|
|
||||||
tail -f "$(ctx::root)/daemon/events.log" 2>/dev/null | while IFS= read -r line; do
|
tail -f "$(ctx::root)/daemon/events.log" 2>/dev/null | while IFS= read -r line; do
|
||||||
[[ -z "$line" ]] && continue
|
[[ -z "$line" ]] && continue
|
||||||
|
|
||||||
local event_data
|
local event_data
|
||||||
event_data=$(python3 -c "
|
event_data=$(json::parse_event "$line")
|
||||||
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)
|
|
||||||
|
|
||||||
[[ -z "$event_data" ]] && continue
|
[[ -z "$event_data" ]] && continue
|
||||||
|
|
||||||
if $restricted_only; then
|
local ts client endpoint event
|
||||||
local conf
|
IFS="|" read -r ts client endpoint event <<< "$event_data"
|
||||||
conf="$(ctx::clients)/${client}.conf"
|
|
||||||
[[ -f "$conf" ]] || continue
|
|
||||||
cmd::list::is_restricted "$client" || continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
local ts client endpoint event
|
if $restricted_only; then
|
||||||
read -r ts client endpoint event <<< "$event_data"
|
local conf
|
||||||
|
conf="$(ctx::clients)/${client}.conf"
|
||||||
|
[[ -f "$conf" ]] || continue
|
||||||
|
cmd::list::is_restricted "$client" || continue
|
||||||
|
fi
|
||||||
|
|
||||||
# Apply filters
|
# Apply filters
|
||||||
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
|
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
|
||||||
|
|
||||||
if [[ -n "$filter_type" ]]; then
|
if [[ -n "$filter_type" ]]; then
|
||||||
local conf
|
local conf
|
||||||
conf="$(ctx::clients)/${client}.conf"
|
conf="$(ctx::clients)/${client}.conf"
|
||||||
[[ -f "$conf" ]] || continue
|
[[ -f "$conf" ]] || continue
|
||||||
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 subnet
|
||||||
subnet=$(config::subnet_for "$filter_type")
|
subnet=$(config::subnet_for "$filter_type")
|
||||||
string::starts_with "$ip" "$subnet" || continue
|
string::starts_with "$ip" "$subnet" || continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Filter by status
|
# Filter by status
|
||||||
if $allowed_only && [[ "$event" != "handshake" ]]; then
|
if $allowed_only && [[ "$event" != "handshake" ]]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if $restricted_only; then
|
if $restricted_only; then
|
||||||
local conf
|
local conf
|
||||||
conf="$(ctx::clients)/${client}.conf"
|
conf="$(ctx::clients)/${client}.conf"
|
||||||
[[ -f "$conf" ]] || continue
|
[[ -f "$conf" ]] || continue
|
||||||
cmd::list::is_restricted "$client" || continue
|
cmd::list::is_restricted "$client" || continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local formatted_ts
|
local formatted_ts
|
||||||
formatted_ts=$(fmt::datetime_iso "$ts")
|
formatted_ts=$(fmt::datetime_iso "$ts")
|
||||||
|
|
||||||
# Before printing the event
|
# Before printing the event
|
||||||
local now
|
local now
|
||||||
now=$(date +%s)
|
now=$(date +%s)
|
||||||
local safe_client="${client//[-.]/_}"
|
local safe_client="${client//[-.]/_}"
|
||||||
local last="${_WATCH_LAST_ATTEMPT[$safe_client]:-0}"
|
local last="${_WATCH_LAST_ATTEMPT[$safe_client]:-0}"
|
||||||
local diff=$(( now - last ))
|
local diff=$(( now - last ))
|
||||||
if (( diff < 30 )); then
|
if (( diff < 30 )); then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
_WATCH_LAST_ATTEMPT[$safe_client]="$now"
|
_WATCH_LAST_ATTEMPT[$safe_client]="$now"
|
||||||
|
|
||||||
cmd::watch::format_event \
|
cmd::watch::format_event \
|
||||||
"$formatted_ts" "$client" "${endpoint:-—}" "$event" "blocked"
|
"$formatted_ts" "$client" "${endpoint:-—}" "$event" "blocked"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ _CTX_WG="/etc/wireguard"
|
||||||
_CTX_CORE="${_CTX_ROOT}/core"
|
_CTX_CORE="${_CTX_ROOT}/core"
|
||||||
_CTX_MODULES="${_CTX_ROOT}/modules"
|
_CTX_MODULES="${_CTX_ROOT}/modules"
|
||||||
_CTX_COMMANDS="${_CTX_ROOT}/commands"
|
_CTX_COMMANDS="${_CTX_ROOT}/commands"
|
||||||
_CTX_PRESETS="${_CTX_ROOT}/presets"
|
|
||||||
_CTX_CLIENTS="${_CTX_WG}/clients"
|
_CTX_CLIENTS="${_CTX_WG}/clients"
|
||||||
_CTX_DATA="${_CTX_WG}/.wgctl"
|
_CTX_DATA="${_CTX_WG}/.wgctl"
|
||||||
|
|
||||||
|
|
@ -29,7 +28,6 @@ function ctx::root() { echo "$_CTX_ROOT"; }
|
||||||
function ctx::core() { echo "$_CTX_CORE"; }
|
function ctx::core() { echo "$_CTX_CORE"; }
|
||||||
function ctx::modules() { echo "$_CTX_MODULES"; }
|
function ctx::modules() { echo "$_CTX_MODULES"; }
|
||||||
function ctx::commands() { echo "$_CTX_COMMANDS"; }
|
function ctx::commands() { echo "$_CTX_COMMANDS"; }
|
||||||
function ctx::presets() { echo "$_CTX_PRESETS"; }
|
|
||||||
function ctx::blocks() { echo "$_CTX_BLOCKS"; }
|
function ctx::blocks() { echo "$_CTX_BLOCKS"; }
|
||||||
function ctx::groups() { echo "$_CTX_GROUPS"; }
|
function ctx::groups() { echo "$_CTX_GROUPS"; }
|
||||||
function ctx::rules() { echo "$_CTX_RULES"; }
|
function ctx::rules() { echo "$_CTX_RULES"; }
|
||||||
|
|
@ -54,11 +52,6 @@ function ctx::client::path() {
|
||||||
echo "$_CTX_CLIENTS/$*"
|
echo "$_CTX_CLIENTS/$*"
|
||||||
}
|
}
|
||||||
|
|
||||||
function ctx::preset::path() {
|
|
||||||
local IFS="/"
|
|
||||||
echo "$_CTX_PRESETS/$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
function ctx::meta::path() {
|
function ctx::meta::path() {
|
||||||
local IFS="/"
|
local IFS="/"
|
||||||
echo "$_CTX_META/$*"
|
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::group_list_data() { python3 "$JSON_HELPER" group_list_data "$@" </dev/null; }
|
||||||
function json::fmt_datetime() { python3 "$JSON_HELPER" fmt_datetime "$@" </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::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)
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
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"""
|
"""Follow both log files and output formatted events"""
|
||||||
import glob, time, select
|
import glob, time, select
|
||||||
|
|
||||||
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
|
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
|
||||||
|
peer_filter = set(filter_peers.split(',')) if filter_peers else set()
|
||||||
|
|
||||||
# Build ip->name map
|
# Build ip->name map
|
||||||
ip_to_name = {}
|
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, '')
|
ip = ip_to_name.get(filter_ip, '')
|
||||||
if client != ip and client != filter_ip:
|
if client != ip and client != filter_ip:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if peer_filter and client not in peer_filter:
|
||||||
|
continue
|
||||||
if filter_type and not client.startswith(filter_type + '-'):
|
if filter_type and not client.startswith(filter_type + '-'):
|
||||||
continue
|
continue
|
||||||
ts = e.get('timestamp', '')[:16].replace('T', ' ')
|
ts = e.get('timestamp', '')[:16].replace('T', ' ')
|
||||||
|
|
@ -638,34 +642,83 @@ def create_rule(file, name, desc, dns_redirect, allow_ips, block_ips, block_port
|
||||||
with open(file, 'w') as f:
|
with open(file, 'w') as f:
|
||||||
json.dump(rule, f, indent=2)
|
json.dump(rule, f, indent=2)
|
||||||
|
|
||||||
commands = {
|
def cleanup_config(config_file):
|
||||||
'get': lambda args: get(args[0], args[1]),
|
"""Normalize blank lines in WireGuard config"""
|
||||||
'set': lambda args: set_key(args[0], args[1], args[2]),
|
import re
|
||||||
'delete': lambda args: delete_key(args[0], args[1]),
|
try:
|
||||||
'append': lambda args: append(args[0], args[1], args[2]),
|
with open(config_file) as f:
|
||||||
'remove': lambda args: remove_value(args[0], args[1], args[2]),
|
config = f.read()
|
||||||
'cat': lambda args: cat(args[0]),
|
config = re.sub(r'\n{3,}', '\n\n', config)
|
||||||
'has_key': lambda args: has_key(args[0], args[1]),
|
config = config.rstrip('\n') + '\n'
|
||||||
'filter_values': lambda args: filter_values(args[0], args[1], args[2]),
|
with open(config_file, 'w') as f:
|
||||||
'last_event': lambda args: last_event(args[0], args[1], args[2], args[3]),
|
f.write(config)
|
||||||
'events_for': lambda args: events_for(args[0], args[1], args[2]),
|
except Exception as e:
|
||||||
'fw_events': lambda args: fw_events(args[0], args[1], args[2], args[3], args[4]),
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
'wg_events': lambda args: wg_events(args[0], args[1], args[2], args[3]),
|
sys.exit(1)
|
||||||
'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]),
|
|
||||||
'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]),
|
|
||||||
'peer_groups': lambda args: peer_groups(args[0], args[1]),
|
|
||||||
'peer_data': lambda args: peer_data(args[0], args[1], args[2]),
|
|
||||||
'iso_to_ts': lambda args: iso_to_ts(args[0]),
|
|
||||||
'rule_list_data': lambda args: rule_list_data(args[0], args[1]),
|
|
||||||
'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]),
|
|
||||||
|
|
||||||
|
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]),
|
||||||
|
'delete': lambda args: delete_key(args[0], args[1]),
|
||||||
|
'append': lambda args: append(args[0], args[1], args[2]),
|
||||||
|
'remove': lambda args: remove_value(args[0], args[1], args[2]),
|
||||||
|
'cat': lambda args: cat(args[0]),
|
||||||
|
'has_key': lambda args: has_key(args[0], args[1]),
|
||||||
|
'filter_values': lambda args: filter_values(args[0], args[1], args[2]),
|
||||||
|
'last_event': lambda args: last_event(args[0], args[1], args[2], args[3]),
|
||||||
|
'events_for': lambda args: events_for(args[0], args[1], args[2]),
|
||||||
|
'fw_events': lambda args: fw_events(args[0], args[1], args[2], args[3], args[4]),
|
||||||
|
'wg_events': lambda args: wg_events(args[0], args[1], args[2], args[3]),
|
||||||
|
'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], 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]),
|
||||||
|
'peer_groups': lambda args: peer_groups(args[0], args[1]),
|
||||||
|
'peer_data': lambda args: peer_data(args[0], args[1], args[2]),
|
||||||
|
'iso_to_ts': lambda args: iso_to_ts(args[0]),
|
||||||
|
'rule_list_data': lambda args: rule_list_data(args[0], args[1]),
|
||||||
|
'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__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"phone-fred": "94.63.0.129",
|
"phone-fred": "94.63.0.129",
|
||||||
"phone-helena": "148.69.37.26",
|
"phone-helena": "148.69.44.173",
|
||||||
"phone-nuno": "94.63.0.129",
|
"phone-nuno": "94.63.0.129",
|
||||||
"tablet-nuno": "148.69.202.5",
|
"tablet-nuno": "148.69.202.5",
|
||||||
"guest-zephyr": "5.13.82.5",
|
"guest-zephyr": "5.13.82.5",
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ function config::load() {
|
||||||
WG_LAN) _WG_LAN="$value" ;;
|
WG_LAN) _WG_LAN="$value" ;;
|
||||||
# Add debug temporarily to config::load:
|
# Add debug temporarily to config::load:
|
||||||
DATE_FORMAT)
|
DATE_FORMAT)
|
||||||
log::debug "config: setting date format to $value"
|
|
||||||
_FMT_DATE_FORMAT="$value"
|
_FMT_DATE_FORMAT="$value"
|
||||||
fmt::set_date_format "$value"
|
fmt::set_date_format "$value"
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ function fw::unallow_port() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function fw::flush_peer() {
|
function fw::flush_peer() {
|
||||||
local client_ip="$1"
|
local client_ip="${1:?client_ip required}"
|
||||||
log::debug "flush_peer: starting for $client_ip"
|
log::debug "flush_peer: starting for $client_ip"
|
||||||
|
|
||||||
# Collect line numbers into array
|
# Collect line numbers into array
|
||||||
|
|
@ -205,47 +205,3 @@ function fw::restore_blocks() {
|
||||||
log::debug "Restored block rules for: ${name}"
|
log::debug "Restored block rules for: ${name}"
|
||||||
done
|
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
|
fi
|
||||||
done < <(group::all)
|
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() {
|
function peers::cleanup_config() {
|
||||||
local config
|
json::cleanup_config "$(config::config_file)"
|
||||||
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)
|
|
||||||
"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function peers::add_to_server() {
|
function peers::add_to_server() {
|
||||||
local name="$1"
|
local name="${1:?name required}"
|
||||||
local public_key="$2"
|
local public_key="${2:?public_key required}"
|
||||||
local ip="$3"
|
local ip="${3:?ip required}"
|
||||||
|
|
||||||
local config
|
local config
|
||||||
config=$(config::config_file)
|
config=$(config::config_file)
|
||||||
|
|
@ -97,21 +84,12 @@ EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
function peers::remove_block() {
|
function peers::remove_block() {
|
||||||
local name="$1"
|
local name="${1:?name required}"
|
||||||
local config
|
json::remove_peer_block "$(config::config_file)" "$name"
|
||||||
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)
|
|
||||||
"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function peers::remove_from_server() {
|
function peers::remove_from_server() {
|
||||||
local name="$1"
|
local name="${1:?name required}"
|
||||||
peers::remove_block "$name"
|
peers::remove_block "$name"
|
||||||
peers::cleanup_config
|
peers::cleanup_config
|
||||||
log::debug "Removed peer from server config: ${name}"
|
log::debug "Removed peer from server config: ${name}"
|
||||||
|
|
@ -183,8 +161,8 @@ function peers::exists_in_server() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function peers::is_blocked() {
|
function peers::is_blocked() {
|
||||||
local name="$1"
|
local name="${1:-}"
|
||||||
! peers::exists_in_server "$name"
|
peers::exists_in_server "$name" && return 1 || return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -248,7 +226,7 @@ function peers::with_rule() {
|
||||||
function peers::get_ip() {
|
function peers::get_ip() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
grep "^Address" "$(ctx::clients)/${name}.conf" 2>/dev/null \
|
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() {
|
function peers::find_by_ip() {
|
||||||
|
|
|
||||||
|
|
@ -72,13 +72,11 @@ function rule::is_applied() {
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
function rule::apply() {
|
function rule::apply() {
|
||||||
local rule_name="$1"
|
local rule_name="${1:?rule_name required}"
|
||||||
local client_ip="$2"
|
local client_ip="${2:?client_ip required}"
|
||||||
local peer_name="${3:-}" # optional, avoids find_by_ip call
|
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
|
rule::require_exists "$rule_name" || return 1
|
||||||
log::debug "rule::apply: exists check passed"
|
|
||||||
|
|
||||||
# Use provided peer_name or look it up
|
# Use provided peer_name or look it up
|
||||||
if [[ -z "$peer_name" ]]; then
|
if [[ -z "$peer_name" ]]; then
|
||||||
|
|
@ -100,7 +98,6 @@ function rule::apply() {
|
||||||
# Check if already applied
|
# Check if already applied
|
||||||
local peer_name
|
local peer_name
|
||||||
peer_name=$(peers::find_by_ip "$client_ip")
|
peer_name=$(peers::find_by_ip "$client_ip")
|
||||||
log::debug "rule::apply: find_by_ip($client_ip) = '$peer_name'"
|
|
||||||
if [[ -n "$peer_name" ]]; then
|
if [[ -n "$peer_name" ]]; then
|
||||||
# Check if already applied via iptables
|
# Check if already applied via iptables
|
||||||
if rule::is_applied "$rule_name" "$client_ip"; then
|
if rule::is_applied "$rule_name" "$client_ip"; then
|
||||||
|
|
@ -140,10 +137,8 @@ function rule::apply() {
|
||||||
done < <(rule::get "$rule_name" "allow_ports")
|
done < <(rule::get "$rule_name" "allow_ports")
|
||||||
|
|
||||||
# Persist rule assignment in meta
|
# Persist rule assignment in meta
|
||||||
log::debug "rule::apply: peer_name=$peer_name ip=$client_ip"
|
|
||||||
if [[ -n "$peer_name" ]]; then
|
if [[ -n "$peer_name" ]]; then
|
||||||
peers::set_meta "$peer_name" "rule" "$rule_name"
|
peers::set_meta "$peer_name" "rule" "$rule_name"
|
||||||
log::debug "rule::apply: set meta rule=$rule_name for $peer_name"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local dns_redirect
|
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
|
enable Enable WireGuard on boot
|
||||||
disable Disable 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.
|
Run 'wgctl <command> --help' for command-specific help.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue