feat: block system JSON migration, M:N group tracking, block module, block::restore_all, color module, fw refactor
This commit is contained in:
parent
7b32dcfebc
commit
cf90ab22db
26 changed files with 1466 additions and 487 deletions
|
|
@ -10,16 +10,23 @@ function cmd::audit::help() {
|
|||
cat <<EOF
|
||||
Usage: wgctl audit [options]
|
||||
|
||||
Verify WireGuard and firewall state matches expected configuration.
|
||||
Verify that all peers have the correct iptables firewall rules applied.
|
||||
Checks expected rule count (including inherited rules) vs actual iptables state.
|
||||
|
||||
Options:
|
||||
--fix Attempt to fix detected issues
|
||||
--peer <name> Audit specific peer only
|
||||
--type <type> Audit peers of specific type only
|
||||
--type <type> Audit peers of specific device type only
|
||||
--fix Attempt to auto-repair missing or extra rules
|
||||
|
||||
Output:
|
||||
✅ pass — peer has correct rule count
|
||||
❌ fail — peer has missing rules (run --fix to repair)
|
||||
⚠️ warn — peer has extra rules (e.g. blocked peers with base rules)
|
||||
|
||||
Examples:
|
||||
wgctl audit
|
||||
wgctl audit --peer phone-nuno
|
||||
wgctl audit --type guest
|
||||
wgctl audit --fix
|
||||
EOF
|
||||
}
|
||||
|
|
@ -98,10 +105,10 @@ function cmd::audit::check_peer() {
|
|||
local rule_file
|
||||
rule_file="$(ctx::rule::path "${rule}.rule")"
|
||||
local block_ports block_ips allow_ports allow_ips expected
|
||||
block_ports=$(json::count "$rule_file" "block_ports")
|
||||
block_ips=$(json::count "$rule_file" "block_ips")
|
||||
allow_ports=$(json::count "$rule_file" "allow_ports")
|
||||
allow_ips=$(json::count "$rule_file" "allow_ips")
|
||||
block_ports=$(json::count_resolved "$rule" "block_ports")
|
||||
block_ips=$(json::count_resolved "$rule" "block_ips")
|
||||
allow_ports=$(json::count_resolved "$rule" "allow_ports")
|
||||
allow_ips=$(json::count_resolved "$rule" "allow_ips")
|
||||
expected=$(( (block_ports + block_ips) * 2 + allow_ports + allow_ips ))
|
||||
|
||||
# actual is passed in as $3 — no iptables call here
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ function cmd::block::on_load() {
|
|||
flag::register --port
|
||||
flag::register --proto
|
||||
flag::register --subnet
|
||||
flag::register --block-name
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -52,6 +53,7 @@ EOF
|
|||
function cmd::block::run() {
|
||||
local name=""
|
||||
local type=""
|
||||
local block_name=""
|
||||
local ips=()
|
||||
local subnets=()
|
||||
local ports=()
|
||||
|
|
@ -62,6 +64,7 @@ function cmd::block::run() {
|
|||
--name) name="$2"; shift 2 ;;
|
||||
--type) type="$2"; shift 2 ;;
|
||||
--ip) ips+=("$2"); shift 2 ;;
|
||||
--block-name) block_name="$2"; shift 2 ;;
|
||||
--force) force=true; shift ;;
|
||||
--quiet) quiet=true; shift ;;
|
||||
--subnet) subnets+=("$2"); shift 2 ;;
|
||||
|
|
@ -84,7 +87,7 @@ function cmd::block::run() {
|
|||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||
|
||||
# Check if actually blocked
|
||||
if peers::is_blocked "$name" || [[ -f "$(ctx::block::path "${name}.block")" ]]; then
|
||||
if peers::is_blocked "$name" || block::has_file "$name"; then
|
||||
log::wg_warning "Client is already blocked: ${name}"
|
||||
return 0
|
||||
fi
|
||||
|
|
@ -101,33 +104,38 @@ function cmd::block::run() {
|
|||
local client_ip
|
||||
client_ip=$(peers::get_ip "$name") || return 1
|
||||
|
||||
# $quiet || log::section "Blocking client: ${name} (${client_ip})"
|
||||
|
||||
# No specific target — block everything
|
||||
# Only full block if no specific targets provided
|
||||
if [[ ${#ips[@]} -eq 0 && ${#ports[@]} -eq 0 && ${#subnets[@]} -eq 0 ]]; then
|
||||
cmd::block::_block_all "$name" "$client_ip" "$quiet"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Specific rules — don't full block
|
||||
block::set_direct "$name" "$client_ip" "false" # ensure not marked as full block
|
||||
|
||||
# Block specific IPs
|
||||
for ip in "${ips[@]}"; do
|
||||
ip::require_valid "$ip"
|
||||
fw::block_ip "$client_ip" "$ip"
|
||||
fw::save_block "$name" "$client_ip" "$ip"
|
||||
block::add_rule "$name" "$client_ip" "ip" "" "$ip"
|
||||
done
|
||||
|
||||
# Block specific subnets
|
||||
for subnet in "${subnets[@]}"; do
|
||||
ip::require_valid "$subnet"
|
||||
fw::block_subnet "$client_ip" "$subnet"
|
||||
fw::save_block "$name" "$client_ip" "$subnet"
|
||||
block::add_rule "$name" "$client_ip" "subnet" "" "$target_ip"
|
||||
done
|
||||
|
||||
# Block specific ports
|
||||
for entry in "${ports[@]}"; do
|
||||
local target port proto
|
||||
IFS=":" read -r target port proto <<< "$entry"
|
||||
proto="${proto:-tcp}"
|
||||
ip::require_valid "$target"
|
||||
fw::block_port "$client_ip" "$target" "$port" "$proto"
|
||||
fw::save_block "$name" "$client_ip" "$target" "$port" "$proto"
|
||||
|
||||
fw::block_port "$client_ip" "$target" "$port" "${proto:-tcp}"
|
||||
block::add_rule "$name" "$client_ip" "port" "" "$target" "$port" "${proto:-tcp}"
|
||||
done
|
||||
|
||||
log::debug "Block rules applied for: ${name}"
|
||||
|
|
@ -144,27 +152,15 @@ function cmd::block::_get_endpoint() {
|
|||
}
|
||||
|
||||
function cmd::block::_block_all() {
|
||||
local name="${1:-}"
|
||||
local client_ip="${2:-}"
|
||||
local name="${1:?name required}"
|
||||
local client_ip="${2:?client_ip required}"
|
||||
local quiet="${3:-false}"
|
||||
|
||||
[[ -z "$name" ]] && log::error "name required" && return 1
|
||||
[[ -z "$client_ip" ]] && log::error "client_ip required" && return 1
|
||||
# Apply fw rules and remove from server
|
||||
block::apply_full "$name" "$client_ip"
|
||||
|
||||
local public_key endpoint
|
||||
public_key=$(keys::public "$name") || return 1
|
||||
endpoint=$(cmd::block::_get_endpoint "$name" "$public_key")
|
||||
|
||||
fw::block_all "$client_ip" "$name"
|
||||
fw::save_block "$name" "$client_ip"
|
||||
|
||||
if [[ -n "$endpoint" ]]; then
|
||||
monitor::unwatch "$client_ip"
|
||||
monitor::watch "$endpoint" "$name"
|
||||
fi
|
||||
|
||||
peers::remove_from_server "$name"
|
||||
peers::reload
|
||||
# Mark as directly blocked
|
||||
block::set_direct "$name" "$client_ip" "true"
|
||||
|
||||
$quiet || log::wg_success "${name} has been blocked."
|
||||
}
|
||||
|
|
@ -21,26 +21,27 @@ function cmd::fw::help() {
|
|||
cat <<EOF
|
||||
Usage: wgctl fw [subcommand] [options]
|
||||
|
||||
Inspect and manage firewall rules.
|
||||
Inspect iptables firewall rules applied by wgctl.
|
||||
|
||||
Subcommands:
|
||||
list Show FORWARD chain rules (default)
|
||||
nat Show NAT/PREROUTING rules
|
||||
list (default) Show FORWARD chain rules
|
||||
nat Show NAT/PREROUTING rules (DNS redirects)
|
||||
flush-nat Flush NAT rules for a subnet
|
||||
count Show rule counts by type
|
||||
count Show rule counts by type (ACCEPT/DROP/NFLOG)
|
||||
|
||||
Options for list:
|
||||
--peer <name> Filter by peer name
|
||||
--rule <rule> Filter by rule name (shows all peers with that rule)
|
||||
--no-nflog Hide NFLOG rules
|
||||
--peer <name> Filter rules for a specific peer
|
||||
--rule <rule> Filter by rule — shows all peers with that rule
|
||||
--no-nflog Hide NFLOG logging rules
|
||||
--no-accept Hide ACCEPT rules
|
||||
--no-drop Hide DROP rules
|
||||
|
||||
Examples:
|
||||
wgctl fw list
|
||||
wgctl fw
|
||||
wgctl fw list --peer phone-nuno
|
||||
wgctl fw list --rule guest
|
||||
wgctl fw list --no-nflog
|
||||
wgctl fw list --peer phone-nuno --no-nflog
|
||||
wgctl fw nat
|
||||
wgctl fw count
|
||||
wgctl fw flush-nat --subnet 10.1.103.0/24
|
||||
|
|
@ -93,16 +94,8 @@ function cmd::fw::list() {
|
|||
log::section "Firewall Rules (FORWARD) — rule: ${rule}"
|
||||
printf "\n"
|
||||
local found=false
|
||||
while IFS= read -r peer_name; do
|
||||
local ip
|
||||
ip=$(peers::get_ip "$peer_name")
|
||||
[[ -z "$ip" ]] && continue
|
||||
printf " \033[0;37m── %s (%s)\033[0m\n" "$peer_name" "$ip"
|
||||
iptables -L FORWARD -n -v | grep -F "$ip" \
|
||||
| cmd::fw::_print_filtered "$show_nflog" "$show_accept" "$show_drop" || true
|
||||
found=true
|
||||
done < <(peers::with_rule "$rule")
|
||||
$found || log::wg_warning "No peers found with rule: ${rule}"
|
||||
fw::list_peer_rules "$ip" "$show_nflog"
|
||||
printf "\n"
|
||||
return 0
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -22,39 +22,47 @@ function cmd::group::help() {
|
|||
cat <<EOF
|
||||
Usage: wgctl group <subcommand> [options]
|
||||
|
||||
Manage peer groups.
|
||||
Manage peer groups. Groups are organizational — a peer can belong
|
||||
to multiple groups. Operations like block/unblock act on all peers in a group.
|
||||
|
||||
Subcommands:
|
||||
list, ls List all groups
|
||||
show Show group details and members
|
||||
list, ls List all groups with status
|
||||
show Show group members and their status
|
||||
add, new, create Create a new group
|
||||
remove, rm, del Remove a group definition
|
||||
remove, rm, del Remove a group definition (not the peers)
|
||||
rename Rename a group
|
||||
peer add Add a peer to a group
|
||||
peer remove, peer rm Remove a peer from a group
|
||||
rm-peers Remove all peers from WireGuard server
|
||||
rm-peers Remove all peers in group from WireGuard
|
||||
block Block all peers in group
|
||||
unblock Unblock all peers in group
|
||||
rule assign Assign a rule to all peers in group
|
||||
audit Audit all peers in group
|
||||
logs Show logs for all peers in group
|
||||
audit Audit firewall rules for all peers in group
|
||||
logs Show activity logs for all peers in group
|
||||
watch Live monitor for all peers in group
|
||||
|
||||
Options:
|
||||
--name <name> Group name
|
||||
--desc <description> Group description
|
||||
--desc <description> Group description (for add)
|
||||
--peer <peer> Peer name
|
||||
--type <type> Peer device type (for peer resolution)
|
||||
--type <type> Peer device type (optional)
|
||||
--rule <rule> Rule name (for rule assign)
|
||||
--new-name <name> New name (for rename)
|
||||
--new-name <name> New group name (for rename)
|
||||
--limit <n> Max log entries per peer (for logs)
|
||||
--force Skip confirmation prompts
|
||||
|
||||
Examples:
|
||||
wgctl group list
|
||||
wgctl group add --name family --desc "Family devices"
|
||||
wgctl group peer add --name family --peer phone-nuno
|
||||
wgctl group peer remove --name family --peer phone-nuno
|
||||
wgctl group show --name family
|
||||
wgctl group block --name family
|
||||
wgctl group unblock --name family
|
||||
wgctl group rule assign --name family --rule user
|
||||
wgctl group audit --name family
|
||||
wgctl group logs --name family --limit 20
|
||||
wgctl group watch --name family
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -585,37 +593,28 @@ function cmd::group::block() {
|
|||
return 0
|
||||
fi
|
||||
|
||||
# log::section "Blocking group: ${name}"
|
||||
local count=0 skipped=0 blocked_names=()
|
||||
local filtered=()
|
||||
for p in "${peers_list[@]:-}"; do
|
||||
[[ -n "$p" ]] && filtered+=("$p")
|
||||
done
|
||||
[[ ${#filtered[@]} -eq 0 ]] && log::wg_warning "Group '${name}' has no peers" && return 0
|
||||
|
||||
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
|
||||
fi
|
||||
( core::set_quiet; load_command block; cmd::block::run --name "$peer_name" --force )
|
||||
blocked_names+=("$peer_name")
|
||||
for peer_name in "${filtered[@]}"; do
|
||||
if cmd::group::_block_peer "$peer_name" "$name"; then
|
||||
(( count++ )) || true
|
||||
else
|
||||
(( skipped++ )) || true
|
||||
fi
|
||||
done
|
||||
|
||||
[[ "$count" -eq 0 ]] && return 0
|
||||
|
||||
# if [[ "$count" -gt 0 ]]; then
|
||||
# printf "\n"
|
||||
# for n in "${blocked_names[@]}"; do
|
||||
# log::wg " Blocked: ${n}"
|
||||
# done
|
||||
# fi
|
||||
|
||||
# log::wg_block "Blocked ${count} peers in group '${name}'"
|
||||
if [[ "$count" -gt 0 ]]; then
|
||||
log::wg_block "All peers from ${name} have been blocked (${count} peers)."
|
||||
fi
|
||||
|
||||
if [[ "$skipped" -gt 0 ]]; then
|
||||
log::wg_warning "${skipped} peers already blocked"
|
||||
fi
|
||||
}
|
||||
|
||||
function cmd::group::unblock() {
|
||||
|
|
@ -635,38 +634,114 @@ function cmd::group::unblock() {
|
|||
|
||||
local peers_list=()
|
||||
mapfile -t peers_list < <(group::peers "$name")
|
||||
[[ -z "${peers_list[0]}" ]] && log::wg_warning "Group '${name}' has no peers" && return 0
|
||||
|
||||
# log::section "Unblocking group: ${name}"
|
||||
local filtered=()
|
||||
for p in "${peers_list[@]:-}"; do [[ -n "$p" ]] && filtered+=("$p"); done
|
||||
[[ ${#filtered[@]} -eq 0 ]] && log::wg_warning "Group '${name}' has no peers" && return 0
|
||||
|
||||
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
|
||||
local count=0 skipped=0
|
||||
|
||||
if ! peers::is_blocked "$peer_name"; then
|
||||
log::wg_warning "${peer_name} — not blocked"
|
||||
continue
|
||||
fi
|
||||
( core::set_quiet; load_command unblock; cmd::unblock::run --name "$peer_name" --force )
|
||||
unblocked_names+=("$peer_name")
|
||||
for peer_name in "${filtered[@]}"; do
|
||||
if cmd::group::_unblock_peer "$peer_name" "$name"; then
|
||||
(( count++ )) || true
|
||||
else
|
||||
(( skipped++ )) || true
|
||||
fi
|
||||
done
|
||||
|
||||
[[ "$count" -eq 0 ]] && return 0
|
||||
if [[ "$count" -gt 0 ]]; then
|
||||
log::wg_unblock "All peers from ${name} have been unblocked."
|
||||
fi
|
||||
|
||||
# if [[ "$count" -gt 0 ]]; then
|
||||
# printf "\n"
|
||||
# for n in "${blocked_names[@]}"; do
|
||||
# log::wg " Unblocked: ${n}"
|
||||
# done
|
||||
# fi
|
||||
if [[ "$skipped" -gt 0 ]]; then
|
||||
log::wg_warning "${skipped} peer(s) remain blocked (blocked directly or by other groups)"
|
||||
fi
|
||||
}
|
||||
|
||||
log::wg_unblock "All peers from ${name} have been unblocked (${count} peers)."
|
||||
function cmd::group::_block_peer() {
|
||||
local peer_name="${1:-}" group_name="${2:-}"
|
||||
if ! group::_peer_exists_check "$peer_name"; then
|
||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local client_ip
|
||||
client_ip=$(peers::get_ip "$peer_name")
|
||||
|
||||
# Check if already blocked by this group
|
||||
local current_blocked_groups
|
||||
current_blocked_groups=$(block::get_groups "$peer_name")
|
||||
|
||||
local IFS=','
|
||||
for g in $current_blocked_groups; do
|
||||
if [[ "$g" == "$group_name" ]]; then
|
||||
log::wg_warning "${peer_name} — already blocked by group '${group_name}'"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Add group to block tracking
|
||||
block::add_group "$peer_name" "$client_ip" "$group_name"
|
||||
|
||||
# Apply fw rules only if peer is still in WG server (not yet blocked)
|
||||
if peers::exists_in_server "$peer_name"; then
|
||||
block::apply_full "$peer_name" "$client_ip"
|
||||
fi
|
||||
}
|
||||
|
||||
function cmd::group::_unblock_peer() {
|
||||
local peer_name="${1:-}" group_name="${2:-}"
|
||||
|
||||
if ! group::_peer_exists_check "$peer_name"; then
|
||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check if blocked by this group at all
|
||||
if ! block::has_file "$peer_name"; then
|
||||
log::wg_warning "${peer_name} — not blocked"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local current_groups
|
||||
current_groups=$(block::get_groups "$peer_name")
|
||||
if [[ "$current_groups" != *"$group_name"* ]]; then
|
||||
log::wg_warning "${peer_name} — not blocked by group '${group_name}'"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local client_ip
|
||||
client_ip=$(peers::get_ip "$peer_name")
|
||||
|
||||
block::remove_group "$peer_name" "$client_ip" "$group_name"
|
||||
|
||||
if block::is_blocked "$peer_name"; then
|
||||
local groups
|
||||
groups=$(block::get_groups "$peer_name")
|
||||
|
||||
local direct
|
||||
direct=$(block::is_blocked_direct "$peer_name")
|
||||
|
||||
if [[ "$direct" == "true" ]]; then
|
||||
log::wg_warning "${peer_name} — still blocked directly, skipping"
|
||||
else
|
||||
log::wg_warning "${peer_name} — still blocked by group(s): ${groups}, skipping"
|
||||
fi
|
||||
|
||||
return 1
|
||||
fi
|
||||
|
||||
log::debug "_unblock_peer: restoring $peer_name"
|
||||
block::restore_peer "$peer_name" "$client_ip"
|
||||
block::remove_file "$peer_name"
|
||||
|
||||
local rule
|
||||
rule=$(peers::get_meta "$peer_name" "rule")
|
||||
|
||||
[[ -n "$rule" ]] && rule::exists "$rule" && \
|
||||
rule::apply "$rule" "$client_ip" "$peer_name"
|
||||
|
||||
log::debug "_unblock_peer: done"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
|
|||
|
|
@ -9,22 +9,24 @@ function cmd::inspect::on_load() {
|
|||
|
||||
function cmd::inspect::help() {
|
||||
cat <<EOF
|
||||
Usage: wgctl inspect --name <name> [--type <type>]
|
||||
Usage: wgctl inspect --name <name> [options]
|
||||
wgctl inspect <full-name>
|
||||
|
||||
Show detailed information for a single client.
|
||||
Show detailed information for a WireGuard client including
|
||||
status, rule with inheritance, groups, firewall rules, and activity.
|
||||
|
||||
Options:
|
||||
--name <name> Client name
|
||||
--type <type> Device type (optional, combines with --name)
|
||||
--config Show raw client config
|
||||
--qr Show QR code
|
||||
--name <name> Client name (e.g. phone-nuno)
|
||||
--type <type> Device type — combines with --name (e.g. --name nuno --type phone)
|
||||
--config Also show raw WireGuard client config
|
||||
--qr Also show QR code
|
||||
|
||||
Examples:
|
||||
wgctl inspect --name phone-nuno
|
||||
wgctl inspect --name nuno --type phone
|
||||
wgctl inspect --name phone-nuno --config
|
||||
wgctl inspect --name phone-nuno --qr
|
||||
wgctl inspect guest-zephyr
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -70,61 +72,205 @@ function cmd::inspect::_peer_info() {
|
|||
local subtype
|
||||
subtype=$(peers::get_meta "$name" "subtype")
|
||||
|
||||
local rule_extends=""
|
||||
if [[ -n "$rule" ]]; then
|
||||
local rule_file
|
||||
rule_file="$(rule::path "$rule" 2>/dev/null)" || true
|
||||
if [[ -n "$rule_file" ]]; then
|
||||
local ext=()
|
||||
mapfile -t ext < <(json::get "$rule_file" "extends" 2>/dev/null || true)
|
||||
if [[ ${#ext[@]} -gt 0 && -n "${ext[0]:-}" ]]; then
|
||||
rule_extends=" (↳ ${ext[*]})"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Rule formatting
|
||||
local rule_display="${rule:-—}"
|
||||
if [[ -n "$rule_file" && ${#ext[@]} -gt 0 && -n "${ext[0]:-}" ]]; then
|
||||
local extends_str
|
||||
extends_str=$(printf '%s, ' "${ext[@]}" | sed 's/, $//')
|
||||
rule_display="${rule} ↳ (${extends_str})"
|
||||
fi
|
||||
|
||||
cmd::inspect::_section "Client"
|
||||
ui::row "Name" "$name"
|
||||
ui::row "IP" "$ip"
|
||||
ui::row "Type" "$(peers::display_type "$type" "$subtype")"
|
||||
ui::row "Rule" "${rule:-—}"
|
||||
ui::row "Rule" "$rule_display"
|
||||
ui::row "Status" "$(echo -e "$status")"
|
||||
ui::row "Endpoint" "${endpoint:-—}"
|
||||
ui::row "Last seen" "$last_seen"
|
||||
ui::row "AllowedIPs" "$allowed_ips"
|
||||
ui::row "Public key" "${public_key:-—}"
|
||||
ui::row "Activity" "Total: $activity_total | Current: $activity_current"
|
||||
ui::row "Activity (total)" "$activity_total"
|
||||
ui::row "Activity (current)" "$activity_current"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function cmd::inspect::_rule_info() {
|
||||
local name="$1"
|
||||
local name="${1:-}"
|
||||
local rule
|
||||
rule=$(peers::get_meta "$name" "rule")
|
||||
[[ -z "$rule" ]] && return 0
|
||||
|
||||
rule::exists "$rule" || return 0
|
||||
|
||||
cmd::inspect::_section "Rule: ${rule}"
|
||||
|
||||
local rule_file
|
||||
rule_file="$(ctx::rule::path "${rule}.rule")"
|
||||
rule_file="$(rule::path "$rule")"
|
||||
|
||||
ui::section "Rule: ${rule}"
|
||||
# Check for inheritance
|
||||
local extends_raw=()
|
||||
mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true)
|
||||
|
||||
local desc dns_redirect
|
||||
desc=$(json::get "$rule_file" "desc")
|
||||
dns_redirect=$(json::get "$rule_file" "dns_redirect")
|
||||
ui::row "Description" "${desc:-—}"
|
||||
ui::row "DNS Redirect" "${dns_redirect:-false}"
|
||||
if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]:-}" ]]; then
|
||||
# Show inheritance tree
|
||||
for base_name in "${extends_raw[@]}"; do
|
||||
[[ -z "$base_name" ]] && continue
|
||||
printf "\n \033[0;37m↳ %s\033[0m\n" "$base_name"
|
||||
|
||||
local base_allows base_blocks
|
||||
base_allows=$(rule::get "$base_name" "allow_ports")$'\n'$(rule::get "$base_name" "allow_ips")
|
||||
base_blocks=$(rule::get "$base_name" "block_ips")$'\n'$(rule::get "$base_name" "block_ports")
|
||||
|
||||
while IFS= read -r e; do
|
||||
[[ -z "$e" ]] && continue
|
||||
printf " \033[0;32m+\033[0m %s\n" "$e"
|
||||
done <<< "$base_allows"
|
||||
while IFS= read -r e; do
|
||||
[[ -z "$e" ]] && continue
|
||||
printf " \033[0;31m-\033[0m %s\n" "$e"
|
||||
done <<< "$base_blocks"
|
||||
|
||||
local base_dns
|
||||
base_dns=$(rule::get_own "$base_name" "dns_redirect")
|
||||
[[ "${base_dns,,}" == "true" ]] && \
|
||||
printf " \033[0;36m↺\033[0m DNS → %s\n" "$(config::dns)"
|
||||
done
|
||||
|
||||
# Own rules
|
||||
local own_allows own_blocks
|
||||
own_allows=$(json::get "$rule_file" "allow_ports" 2>/dev/null)$'\n'$(json::get "$rule_file" "allow_ips" 2>/dev/null)
|
||||
own_blocks=$(json::get "$rule_file" "block_ips" 2>/dev/null)$'\n'$(json::get "$rule_file" "block_ports" 2>/dev/null)
|
||||
local has_own=false
|
||||
|
||||
while IFS= read -r e; do [[ -n "$e" ]] && has_own=true && break; done <<< "$own_allows$own_blocks"
|
||||
|
||||
if $has_own; then
|
||||
printf "\n \033[0;37mOwn:\033[0m\n"
|
||||
while IFS= read -r e; do
|
||||
[[ -z "$e" ]] && continue
|
||||
printf " \033[0;32m+\033[0m %s\n" "$e"
|
||||
done <<< "$own_allows"
|
||||
while IFS= read -r e; do
|
||||
[[ -z "$e" ]] && continue
|
||||
printf " \033[0;31m-\033[0m %s\n" "$e"
|
||||
done <<< "$own_blocks"
|
||||
fi
|
||||
|
||||
else
|
||||
# No inheritance — flat view
|
||||
local allow_ports allow_ips block_ips block_ports
|
||||
allow_ports=$(json::get "$rule_file" "allow_ports")
|
||||
allow_ips=$(json::get "$rule_file" "allow_ips")
|
||||
block_ips=$(json::get "$rule_file" "block_ips")
|
||||
block_ports=$(json::get "$rule_file" "block_ports")
|
||||
allow_ports=$(rule::get "$rule" "allow_ports")
|
||||
allow_ips=$(rule::get "$rule" "allow_ips")
|
||||
block_ips=$(rule::get "$rule" "block_ips")
|
||||
block_ports=$(rule::get "$rule" "block_ports")
|
||||
|
||||
if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then
|
||||
printf " %-20s\n" "Allows:"
|
||||
ui::print_list "+" "$allow_ports"
|
||||
ui::print_list "+" "$allow_ips"
|
||||
else
|
||||
ui::row "Allows" "—"
|
||||
printf "\n"
|
||||
while IFS= read -r e; do
|
||||
[[ -z "$e" ]] && continue
|
||||
printf " \033[0;32m+\033[0m %s\n" "$e"
|
||||
done <<< "$allow_ports"$'\n'"$allow_ips"
|
||||
fi
|
||||
|
||||
if [[ -n "$block_ips" || -n "$block_ports" ]]; then
|
||||
printf " %-20s\n" "Blocks:"
|
||||
ui::print_list "-" "$block_ips"
|
||||
ui::print_list "-" "$block_ports"
|
||||
else
|
||||
ui::row "Blocks" "—"
|
||||
printf "\n"
|
||||
while IFS= read -r e; do
|
||||
[[ -z "$e" ]] && continue
|
||||
printf " \033[0;31m-\033[0m %s\n" "$e"
|
||||
done <<< "$block_ips"$'\n'"$block_ports"
|
||||
fi
|
||||
|
||||
if [[ -z "$allow_ports" && -z "$allow_ips" && \
|
||||
-z "$block_ips" && -z "$block_ports" ]]; then
|
||||
printf "\n full access (no restrictions)\n"
|
||||
fi
|
||||
|
||||
local dns_redirect
|
||||
dns_redirect=$(rule::get_own "$rule" "dns_redirect")
|
||||
[[ "${dns_redirect,,}" == "true" ]] && \
|
||||
printf "\n \033[0;36m↺\033[0m DNS → %s\n" "$(config::dns)"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function cmd::inspect::_blocks_info() {
|
||||
local name="${1:-}"
|
||||
block::has_file "$name" || return 0
|
||||
|
||||
cmd::inspect::_section "Peer Blocks"
|
||||
|
||||
local blocked_direct
|
||||
blocked_direct=$(block::is_blocked_direct "$name")
|
||||
[[ "$blocked_direct" == "true" ]] && \
|
||||
printf " \033[1;31m🚫\033[0m blocked directly\n"
|
||||
|
||||
local blocked_groups
|
||||
blocked_groups=$(block::get_groups "$name")
|
||||
[[ -n "$blocked_groups" ]] && \
|
||||
printf " \033[1;31m🚫\033[0m blocked by groups: %s\n" "$blocked_groups"
|
||||
|
||||
block::format_rules "$name"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# function cmd::inspect::_rule_info() {
|
||||
# local name="$1"
|
||||
# local rule
|
||||
# rule=$(peers::get_meta "$name" "rule")
|
||||
# [[ -z "$rule" ]] && return 0
|
||||
|
||||
# rule::exists "$rule" || return 0
|
||||
|
||||
# local rule_file
|
||||
# rule_file="$(ctx::rule::path "${rule}.rule")"
|
||||
|
||||
# ui::section "Rule: ${rule}"
|
||||
|
||||
# local desc dns_redirect
|
||||
# desc=$(json::get "$rule_file" "desc")
|
||||
# dns_redirect=$(json::get "$rule_file" "dns_redirect")
|
||||
# ui::row "Description" "${desc:-—}"
|
||||
# ui::row "DNS Redirect" "${dns_redirect:-false}"
|
||||
|
||||
# local allow_ports allow_ips block_ips block_ports
|
||||
# allow_ports=$(json::get "$rule_file" "allow_ports")
|
||||
# allow_ips=$(json::get "$rule_file" "allow_ips")
|
||||
# block_ips=$(json::get "$rule_file" "block_ips")
|
||||
# block_ports=$(json::get "$rule_file" "block_ports")
|
||||
|
||||
# if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then
|
||||
# printf " %-20s\n" "Allows:"
|
||||
# ui::print_list "+" "$allow_ports"
|
||||
# ui::print_list "+" "$allow_ips"
|
||||
# else
|
||||
# ui::row "Allows" "—"
|
||||
# fi
|
||||
|
||||
# if [[ -n "$block_ips" || -n "$block_ports" ]]; then
|
||||
# printf " %-20s\n" "Blocks:"
|
||||
# ui::print_list "-" "$block_ips"
|
||||
# ui::print_list "-" "$block_ports"
|
||||
# else
|
||||
# ui::row "Blocks" "—"
|
||||
# fi
|
||||
# }
|
||||
|
||||
function cmd::inspect::_group_info() {
|
||||
local name="$1"
|
||||
|
||||
|
|
@ -144,30 +290,40 @@ function cmd::inspect::_group_info() {
|
|||
count=$(json::count "$(group::path "$g")" "peers")
|
||||
printf " %-20s %s peers\n" "$g" "$count"
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function cmd::inspect::_firewall_info() {
|
||||
local name="$1" show_nflog="${2:-false}"
|
||||
local name="${1:-}"
|
||||
local ip
|
||||
ip=$(peers::get_ip "$name")
|
||||
|
||||
ui::section "Firewall"
|
||||
local total=0 accepts=0 drops=0
|
||||
local rules_output=()
|
||||
while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
(( total++ )) || true
|
||||
[[ "$line" =~ ACCEPT ]] && (( accepts++ )) || true
|
||||
[[ "$line" =~ DROP ]] && (( drops++ )) || true
|
||||
rules_output+=("$line")
|
||||
done < <(fw::forward_rules_for_ip "$ip" | grep -v NFLOG)
|
||||
|
||||
local count=0
|
||||
while IFS=":" read -r pname pcount; do
|
||||
[[ "$pname" == "$name" ]] && count="$pcount" && break
|
||||
done < <(json::audit_fw_counts "$(ctx::clients)")
|
||||
# printf "\n \033[0;37m── Firewall (\033[0;32m+%d\033[0m \033[0;31m-%d\033[0m) \033[0m%s\n" \
|
||||
# "$accepts" "$drops" "$(printf '─%.0s' {1..28})"
|
||||
|
||||
ui::row "Active rules" "$count"
|
||||
printf "\n \033[0;37m── Firewall (%s %s) \033[0m%s\n\n" \
|
||||
"$(color::green "+${accepts}")" \
|
||||
"$(color::red "-${drops}")" \
|
||||
"$(printf '─%.0s' {1..28})"
|
||||
|
||||
if [[ "$count" -gt 0 ]]; then
|
||||
printf "\n"
|
||||
iptables -L FORWARD -n </dev/null | grep -F "$ip" | while IFS= read -r rule; do
|
||||
[[ -z "$rule" ]] && continue
|
||||
echo "$rule" | grep -q "NFLOG" && continue # skip NFLOG
|
||||
ui::firewall_rule "$rule"
|
||||
if [[ ${#rules_output[@]} -gt 0 ]]; then
|
||||
for line in "${rules_output[@]}"; do
|
||||
fw::format_rule "$line"
|
||||
done
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function cmd::inspect::_config() {
|
||||
|
|
@ -176,6 +332,8 @@ function cmd::inspect::_config() {
|
|||
printf "\n"
|
||||
cat "$(ctx::clients)/${name}.conf"
|
||||
printf "\n"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -221,6 +379,7 @@ function cmd::inspect::run() {
|
|||
cmd::inspect::_rule_info "$name"
|
||||
cmd::inspect::_group_info "$name"
|
||||
cmd::inspect::_firewall_info "$name"
|
||||
cmd::inspect::_blocks_info "$name"
|
||||
|
||||
if $show_config; then
|
||||
cmd::inspect::_config "$name"
|
||||
|
|
|
|||
|
|
@ -157,19 +157,14 @@ function cmd::list::show_client() {
|
|||
last_seen=$(peers::format_last_seen "$name" "$public_key" \
|
||||
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
||||
|
||||
local block_file
|
||||
block_file="$(ctx::block::path "${name}.block")"
|
||||
local blocks=""
|
||||
if [[ -f "$block_file" ]] && [[ -s "$block_file" ]]; then
|
||||
while IFS=" " read -r client_ip target port proto; do
|
||||
if [[ -z "$target" ]]; then
|
||||
blocks+=" all traffic blocked\n"
|
||||
else
|
||||
local rule=" ${target}"
|
||||
[[ -n "$port" ]] && rule+=":${port}/${proto}"
|
||||
blocks+="${rule}\n"
|
||||
if block::has_file "$name" && block::is_blocked "$name"; then
|
||||
if [[ "$(block::is_blocked_direct "$name")" == "true" ]]; then
|
||||
blocks="all traffic blocked"
|
||||
fi
|
||||
done < "$block_file"
|
||||
local rule_lines
|
||||
rule_lines=$(block::format_rules "$name")
|
||||
[[ -n "$rule_lines" ]] && blocks+="$rule_lines"
|
||||
fi
|
||||
|
||||
ui::section "Client: ${name}"
|
||||
|
|
@ -389,8 +384,11 @@ function cmd::list::_precompute_all() {
|
|||
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
|
||||
if block::is_blocked "$name" 2>/dev/null; then
|
||||
p_restricted["$name"]=true
|
||||
else
|
||||
p_restricted["$name"]=false
|
||||
fi
|
||||
local pubkey
|
||||
pubkey=$(keys::public "$name" 2>/dev/null || echo "")
|
||||
if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then
|
||||
|
|
@ -434,6 +432,32 @@ function cmd::list::_precompute_all() {
|
|||
done < <(json::peer_transfer "$(config::interface)")
|
||||
}
|
||||
|
||||
function cmd::list::_precompute_block_status() {
|
||||
local -n _blocked="$1"
|
||||
local -n _restricted="$2"
|
||||
|
||||
local wg_peers
|
||||
wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null)
|
||||
|
||||
while IFS= read -r name; do
|
||||
# Restricted = has block rules but still in server (partial block)
|
||||
if block::is_blocked "$name" 2>/dev/null; then
|
||||
_restricted["$name"]=true
|
||||
else
|
||||
_restricted["$name"]=false
|
||||
fi
|
||||
|
||||
# Blocked = removed from WG server
|
||||
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)
|
||||
}
|
||||
|
||||
function cmd::list::_build_filter_desc() {
|
||||
filter_desc=""
|
||||
[[ -n "$filter_type" ]] && filter_desc+="type=${filter_type} "
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ function cmd::logs::on_load() {
|
|||
flag::register --all
|
||||
flag::register --before
|
||||
flag::register --force
|
||||
flag::register --days
|
||||
}
|
||||
|
||||
function cmd::logs::help() {
|
||||
|
|
@ -25,6 +26,7 @@ Show or manage WireGuard and firewall activity logs.
|
|||
Subcommands:
|
||||
show (default) Show activity logs
|
||||
remove, rm Remove log entries
|
||||
rotate Remove entries older than N days
|
||||
|
||||
Options for show:
|
||||
--name <name> Filter by client name
|
||||
|
|
@ -42,6 +44,10 @@ Options for remove:
|
|||
--before <days> Remove entries older than N days
|
||||
--force Skip confirmation
|
||||
|
||||
Options for rotate:
|
||||
--days <n> Days to keep (default: 7)
|
||||
--force Skip confirmation
|
||||
|
||||
Examples:
|
||||
wgctl logs
|
||||
wgctl logs --name phone-nuno
|
||||
|
|
@ -49,8 +55,9 @@ Examples:
|
|||
wgctl logs --follow
|
||||
wgctl logs remove --name phone-nuno
|
||||
wgctl logs remove --all --force
|
||||
wgctl logs remove --before 7
|
||||
wgctl logs remove --fw --before 1
|
||||
wgctl logs rotate
|
||||
wgctl logs rotate --days 30 --force
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -65,6 +72,7 @@ function cmd::logs::run() {
|
|||
case "$subcmd" in
|
||||
show) cmd::logs::show "$@" ;;
|
||||
remove|rm|del) cmd::logs::remove "$@" ;;
|
||||
rotate) cmd::logs::rotate "$@" ;;
|
||||
help) cmd::logs::help ;;
|
||||
*)
|
||||
log::error "Unknown subcommand: '${subcmd}'"
|
||||
|
|
@ -278,3 +286,42 @@ function cmd::logs::show_fw_events() {
|
|||
$found || printf " —\n"
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
|
||||
|
||||
function cmd::logs::rotate() {
|
||||
local days=7 force=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--days) days="$2"; shift 2 ;;
|
||||
--force) force=true; shift ;;
|
||||
--help) cmd::logs::help; return ;;
|
||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
$force || {
|
||||
read -r -p "Remove log entries older than ${days} days? [y/N] " confirm
|
||||
case "$confirm" in
|
||||
[yY]*) ;;
|
||||
*) log::info "Aborted"; return 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
local result
|
||||
result=$(json::remove_events_filtered \
|
||||
"$WG_EVENTS_LOG" "$FW_EVENTS_LOG" \
|
||||
"" "" "false" "false" "$days")
|
||||
|
||||
local removed_wg removed_fw
|
||||
IFS="|" read -r removed_wg removed_fw <<< "$result"
|
||||
local total=$(( removed_wg + removed_fw ))
|
||||
|
||||
if [[ "$total" -eq 0 ]]; then
|
||||
log::wg_warning "No log entries older than ${days} days"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log::wg_success "Rotated ${total} entries older than ${days} days (wg: ${removed_wg}, fw: ${removed_fw})"
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ function cmd::remove::_cleanup() {
|
|||
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
|
||||
block::remove_file "$name" 2>/dev/null || true
|
||||
peers::remove_meta "$name" 2>/dev/null || true
|
||||
peers::reload || return 1
|
||||
}
|
||||
|
|
@ -106,9 +106,7 @@ function cmd::rename::_rename_files() {
|
|||
|
||||
sed -i "s/^# ${name}$/# ${new_name}/" "$(config::config_file)"
|
||||
|
||||
local block_file
|
||||
block_file="$(ctx::block::path "${name}.block")"
|
||||
[[ -f "$block_file" ]] && mv "$block_file" "$(ctx::block::path "${new_name}.block")"
|
||||
block::rename "$name" "$new_name"
|
||||
|
||||
local old_meta new_meta
|
||||
old_meta=$(peers::meta_path "$name")
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ function cmd::rule::on_load() {
|
|||
flag::register --resolved
|
||||
flag::register --force
|
||||
flag::register --type
|
||||
flag::register --all
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
@ -38,67 +39,71 @@ function cmd::rule::help() {
|
|||
cat <<EOF
|
||||
Usage: wgctl rule <subcommand> [options]
|
||||
|
||||
Manage firewall rules for peers.
|
||||
Manage firewall rules with inheritance support.
|
||||
Rules can extend base rules to compose reusable access policies.
|
||||
|
||||
Subcommands:
|
||||
list, ls List all rules
|
||||
show Show rule details
|
||||
inspect Show full inheritance tree
|
||||
show, inspect Show rule details and inheritance
|
||||
add, new, create Create a new rule
|
||||
update, edit Update a rule and re-apply to all peers
|
||||
update, edit Update a rule and re-apply to peers
|
||||
remove, rm, del Remove a rule
|
||||
assign Assign a rule to a peer
|
||||
unassign Remove rule from a peer
|
||||
migrate Apply default rules to all unassigned peers
|
||||
reapply Re-apply a rule to all assigned peers
|
||||
reapply Re-apply rule to all assigned peers
|
||||
migrate Apply default rules to unassigned peers
|
||||
|
||||
Options for list:
|
||||
--base Show base rules section
|
||||
--no-base Hide base rules section (default shows them)
|
||||
--group <name> Filter by group name
|
||||
--base Show only base rules
|
||||
--no-base Hide base rules section
|
||||
--group <name> Filter by group (case insensitive)
|
||||
--tree Show full inheritance tree inline
|
||||
|
||||
Options for add/update:
|
||||
Options for add:
|
||||
--name <name> Rule name
|
||||
--desc <description> Human readable description
|
||||
--group <group> Display group (e.g. vm-rules, user-rules)
|
||||
--extends <rule,...> Inherit from base rules (add only)
|
||||
--add-extends <rule,...> Add base rules (update only)
|
||||
--remove-extends <rule,...> Remove base rules (update only)
|
||||
--desc <description> Description
|
||||
--group <group> Display group (e.g. VM Rules, Users)
|
||||
--extends <rule,...> Inherit from base rules (comma-separated)
|
||||
--base Create as base rule (not directly assignable)
|
||||
--allow-ip <ip/cidr> Allow IP or subnet (repeatable)
|
||||
--allow-port <ip:port:proto> Allow specific port (repeatable)
|
||||
--block-ip <ip/cidr> Block IP or subnet (repeatable)
|
||||
--block-port <ip:port:proto> Block specific port (repeatable)
|
||||
--dns-redirect Force DNS through Pi-hole
|
||||
|
||||
Options for update:
|
||||
(same as add, plus:)
|
||||
--add-extends <rule,...> Add inherited base rules
|
||||
--remove-extends <rule,...> Remove inherited base rules
|
||||
--remove-allow-ip <ip> Remove allow IP entry
|
||||
--remove-allow-port <entry> Remove allow port entry
|
||||
--remove-block-ip <ip> Remove block IP entry
|
||||
--remove-block-port <entry> Remove block port entry
|
||||
|
||||
Options for inspect:
|
||||
Options for show/inspect:
|
||||
--name <name> Rule name
|
||||
--peers Show assigned peers
|
||||
--resolved Show resolved/merged rule entries
|
||||
--resolved Show resolved/merged entries
|
||||
--no-peers Hide assigned peers
|
||||
|
||||
Options for assign/unassign:
|
||||
--name <rule> Rule name
|
||||
--peer <peer> Peer name
|
||||
--type <type> Peer device type (optional)
|
||||
Options for reapply:
|
||||
--name <name> Rule name
|
||||
--all Reapply all rules
|
||||
|
||||
Examples:
|
||||
wgctl rule list
|
||||
wgctl rule list --base
|
||||
wgctl rule list --group vm-rules
|
||||
wgctl rule list --tree
|
||||
wgctl rule list --group "VM Rules"
|
||||
wgctl rule list --base
|
||||
wgctl rule show --name guest
|
||||
wgctl rule inspect --name moonlight-02
|
||||
wgctl rule inspect --name moonlight-02 --resolved --peers
|
||||
wgctl rule add --name dev-01 --desc "Dev VM" --group vm-rules --extends no-lan
|
||||
wgctl rule update --name dev-01 --add-extends no-nginx
|
||||
wgctl rule update --name dev-01 --remove-extends no-nginx
|
||||
wgctl rule update --name dev-01 --group infra-rules
|
||||
wgctl rule show --name moonlight-02 --resolved
|
||||
wgctl rule add --name dev-01 --desc "Dev access" --group "Dev" --extends no-lan
|
||||
wgctl rule add --name no-npm --base --block-ip 10.0.0.101/32
|
||||
wgctl rule update --name user --add-extends no-nginx
|
||||
wgctl rule update --name dev-01 --allow-ip 10.0.0.50/32
|
||||
wgctl rule assign --name dev-01 --peer laptop-nuno
|
||||
wgctl rule unassign --peer laptop-nuno
|
||||
wgctl rule reapply --name user
|
||||
wgctl rule reapply --all
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -507,81 +512,6 @@ function cmd::rule::show() {
|
|||
printf "\n"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Inspect
|
||||
# ============================================
|
||||
|
||||
function cmd::rule::inspect() {
|
||||
local name="" show_peers=false show_resolved=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name) util::require_flag "--name" "${2:-}" || return 1
|
||||
name="$2"; shift 2 ;;
|
||||
--peers) show_peers=true; shift ;;
|
||||
--resolved) show_resolved=true; shift ;;
|
||||
--help) cmd::rule::help; return ;;
|
||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
|
||||
rule::require_exists "$name" || return 1
|
||||
|
||||
log::section "Rule Inspect: ${name}"
|
||||
|
||||
local prev_section=""
|
||||
local show_resolved_flag="$show_resolved"
|
||||
|
||||
while IFS="|" read -r section key value; do
|
||||
[[ -z "$section" ]] && continue
|
||||
|
||||
# Skip resolved section unless requested
|
||||
if [[ "$section" == "resolved" ]] && ! $show_resolved_flag; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$section" != "$prev_section" ]]; then
|
||||
case "$section" in
|
||||
own) cmd::rule::_show_section "Own Rules" ;;
|
||||
dns) cmd::rule::_show_section "DNS" ;;
|
||||
resolved) cmd::rule::_show_section "Resolved (applied)" ;;
|
||||
inherited:*)
|
||||
local base_name="${section#inherited:}"
|
||||
cmd::rule::_show_section "Inherited: ${base_name}" ;;
|
||||
esac
|
||||
prev_section="$section"
|
||||
fi
|
||||
|
||||
case "$key" in
|
||||
allow_ip|allow_port)
|
||||
printf " \033[0;32m+\033[0m %s\n" "$value" ;;
|
||||
block_ip|block_port)
|
||||
printf " \033[0;31m-\033[0m %s\n" "$value" ;;
|
||||
dns_redirect)
|
||||
printf " Redirect all DNS → %s\n" "$(config::dns)" ;;
|
||||
esac
|
||||
|
||||
done < <(json::rule_inspect "$(ctx::rules)" "$name")
|
||||
|
||||
if $show_peers; then
|
||||
cmd::rule::_show_section "Peers" "white" false
|
||||
local peer_list=()
|
||||
mapfile -t peer_list < <(peers::with_rule "$name")
|
||||
ui::row "Assigned" "${#peer_list[@]}"
|
||||
if [[ ${#peer_list[@]} -gt 0 ]]; then
|
||||
printf "\n"
|
||||
for peer_name in "${peer_list[@]}"; do
|
||||
local ip
|
||||
ip=$(peers::get_ip "$peer_name")
|
||||
printf " %-28s %s\n" "$peer_name" "$ip"
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Add
|
||||
# ============================================
|
||||
|
|
@ -868,14 +798,33 @@ function cmd::rule::migrate() {
|
|||
}
|
||||
|
||||
function cmd::rule::reapply() {
|
||||
local name=""
|
||||
local name="" all=false
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name) name="$2"; shift 2 ;;
|
||||
--all) all=true; shift ;;
|
||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||
esac
|
||||
done
|
||||
[[ -z "$name" ]] && log::error "Missing --name" && return 1
|
||||
|
||||
if $all; then
|
||||
log::section "Reapplying all rules"
|
||||
local count=0
|
||||
while IFS= read -r rule_file; do
|
||||
local rname
|
||||
rname=$(basename "$rule_file" .rule)
|
||||
# Skip if no peers assigned
|
||||
local peer_list=()
|
||||
mapfile -t peer_list < <(peers::with_rule "$rname")
|
||||
[[ ${#peer_list[@]} -eq 0 ]] && continue
|
||||
rule::reapply_all "$rname"
|
||||
(( count++ )) || true
|
||||
done < <(find "$(ctx::rules)" -maxdepth 1 -name "*.rule")
|
||||
log::wg_success "Reapplied ${count} assignable rules"
|
||||
return 0
|
||||
fi
|
||||
|
||||
[[ -z "$name" ]] && log::error "Missing --name or --all" && return 1
|
||||
rule::require_exists "$name" || return 1
|
||||
rule::reapply_all "$name"
|
||||
log::wg_success "Rule '${name}' reapplied"
|
||||
|
|
|
|||
|
|
@ -59,6 +59,12 @@ function cmd::service::run() {
|
|||
function cmd::service::start() {
|
||||
log::wg_start "Starting WireGuard..."
|
||||
systemctl start "wg-quick@$(config::interface)"
|
||||
|
||||
block::restore_all
|
||||
rule::restore_all
|
||||
|
||||
cmd::service::_auto_rotate_logs
|
||||
|
||||
log::wg_success "WireGuard started"
|
||||
}
|
||||
|
||||
|
|
@ -72,18 +78,26 @@ function cmd::service::restart() {
|
|||
log::wg_start "Restarting WireGuard..."
|
||||
|
||||
# Flush firewall rules before restart so restore starts clean
|
||||
iptables -F FORWARD
|
||||
iptables -t nat -F PREROUTING
|
||||
fw::flush_all
|
||||
|
||||
systemctl restart "wg-quick@$(config::interface)"
|
||||
fw::restore_blocks
|
||||
|
||||
block::restore_all
|
||||
rule::restore_all
|
||||
|
||||
cmd::service::_auto_rotate_logs
|
||||
|
||||
log::wg_success "WireGuard restarted"
|
||||
}
|
||||
|
||||
function cmd::service::reload() {
|
||||
log::wg_start "Reloading WireGuard config..."
|
||||
|
||||
peers::reload
|
||||
fw::restore_blocks
|
||||
block::restore_all
|
||||
rule::restore_all
|
||||
|
||||
log::wg_success "WireGuard config reloaded"
|
||||
}
|
||||
|
||||
function cmd::service::status() {
|
||||
|
|
@ -102,6 +116,18 @@ function cmd::service::logs() {
|
|||
journalctl -u "wg-quick@$(config::interface)" -f --no-pager
|
||||
}
|
||||
|
||||
function cmd::service::_auto_rotate_logs() {
|
||||
local max_size=10485760 # 10MB
|
||||
local fw_size wg_size
|
||||
fw_size=$(stat -c%s "$(ctx::fw_events_log)" 2>/dev/null || echo 0)
|
||||
wg_size=$(stat -c%s "$(ctx::events_log)" 2>/dev/null || echo 0)
|
||||
|
||||
if (( fw_size > max_size || wg_size > max_size )); then
|
||||
log::wg_warning "Log files exceed 10MB, auto-rotating (keeping 7 days)..."
|
||||
cmd::logs::rotate --days 7 --force
|
||||
fi
|
||||
}
|
||||
|
||||
function cmd::service::enable() {
|
||||
systemctl enable "wg-quick@$(config::interface)"
|
||||
log::wg_success "WireGuard enabled on boot"
|
||||
|
|
|
|||
|
|
@ -14,12 +14,11 @@ function cmd::shell::_prompt() {
|
|||
}
|
||||
|
||||
function cmd::shell::_is_wgctl_command() {
|
||||
local cmd="$1"
|
||||
# Check against known wgctl commands
|
||||
local cmd="${1:-}"
|
||||
local known=(
|
||||
list add remove rm inspect block unblock
|
||||
rule group audit logs watch fw config qr
|
||||
rename keys ip service shell help
|
||||
rename keys ip service shell help test
|
||||
)
|
||||
local c
|
||||
for c in "${known[@]}"; do
|
||||
|
|
@ -29,7 +28,7 @@ function cmd::shell::_is_wgctl_command() {
|
|||
}
|
||||
|
||||
function cmd::shell::_handle_builtin() {
|
||||
local input="$1"
|
||||
local input="${1:-}"
|
||||
local first="${input%% *}"
|
||||
|
||||
case "$first" in
|
||||
|
|
@ -39,36 +38,32 @@ function cmd::shell::_handle_builtin() {
|
|||
cd "$dir" 2>/dev/null || log::error "cd: $dir: No such file or directory"
|
||||
return 0
|
||||
;;
|
||||
cd*) eval "$input" ;; # eval preserves shell state for cd
|
||||
export|unset|source|.)
|
||||
eval "$input"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1 # not a builtin
|
||||
return 1
|
||||
}
|
||||
|
||||
function cmd::shell::_execute() {
|
||||
local input="$1"
|
||||
local input="${1:-}"
|
||||
local first="${input%% *}"
|
||||
local rest="${input#"$first"}"
|
||||
rest="${rest# }"
|
||||
|
||||
# Handle shell builtins first
|
||||
cmd::shell::_handle_builtin "$input" && return 0
|
||||
|
||||
# Try as wgctl command via dispatcher
|
||||
if cmd::shell::_is_wgctl_command "$first"; then
|
||||
if [[ -n "$rest" ]]; then
|
||||
wgctl::dispatch "$first" $rest || true # never exit REPL on failure
|
||||
wgctl::dispatch "$first" $rest || true
|
||||
else
|
||||
wgctl::dispatch "$first" || true
|
||||
fi
|
||||
return 0 # Always 0 to keep REPL running
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Fall back to bash
|
||||
bash -c "$input" || true # same for bash commands
|
||||
bash -c "$input" || true
|
||||
}
|
||||
|
||||
function cmd::shell::_setup_history() {
|
||||
|
|
@ -85,14 +80,30 @@ function cmd::shell::_save_history() {
|
|||
function cmd::shell::_banner() {
|
||||
ui::section "wgctl shell"
|
||||
printf "\n"
|
||||
printf " Type wgctl commands directly, or any bash command.\n"
|
||||
printf " Type \033[1mexit\033[0m or \033[1mquit\033[0m to leave.\n"
|
||||
printf " Type \033[1mhelp\033[0m for wgctl commands.\n"
|
||||
printf "\n"
|
||||
printf " Type wgctl commands directly (no 'wgctl' prefix).\n"
|
||||
printf " Bash commands work too: ls, cat, systemctl, vim...\n\n"
|
||||
printf " \033[1;37mCommon commands:\033[0m\n"
|
||||
printf " list List all peers\n"
|
||||
printf " list --blocked Show blocked peers\n"
|
||||
printf " list --rule user Filter by rule\n"
|
||||
printf " inspect --name <peer> Full peer details\n"
|
||||
printf " block/unblock --name <peer> Block or restore a peer\n"
|
||||
printf " rule list Show firewall rules\n"
|
||||
printf " rule list --tree Show with inheritance tree\n"
|
||||
printf " rule show --name <rule> Rule details\n"
|
||||
printf " group list Show groups\n"
|
||||
printf " group block --name <group> Block all peers in group\n"
|
||||
printf " logs --follow Live activity log\n"
|
||||
printf " logs rotate Clean old log entries\n"
|
||||
printf " watch Live WG + firewall monitor\n"
|
||||
printf " fw list Show iptables rules\n"
|
||||
printf " audit Verify firewall state\n"
|
||||
printf " audit --fix Auto-repair firewall rules\n\n"
|
||||
printf " \033[1mexit\033[0m or \033[1mquit\033[0m to leave · \033[1mhelp\033[0m for full command list\n\n"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Run
|
||||
# Lifecycle
|
||||
# ============================================
|
||||
|
||||
function cmd::shell::on_load() {
|
||||
|
|
@ -103,62 +114,65 @@ function cmd::shell::help() {
|
|||
cat <<EOF
|
||||
Usage: wgctl shell
|
||||
|
||||
Start an interactive wgctl shell. Supports all wgctl commands
|
||||
directly (no 'wgctl' prefix needed), plus any bash command.
|
||||
Start an interactive wgctl shell.
|
||||
All wgctl commands work directly (no 'wgctl' prefix needed).
|
||||
Bash commands (ls, cat, systemctl, vim, etc.) also work.
|
||||
|
||||
Builtins handled: cd, export, unset, source
|
||||
Everything else: passed to bash
|
||||
Shell builtins handled natively: cd, export, unset, source
|
||||
History saved to: ~/.wgctl_history
|
||||
|
||||
Examples:
|
||||
wgctl shell
|
||||
wgctl> list
|
||||
wgctl> list --blocked
|
||||
wgctl> inspect --name phone-nuno
|
||||
wgctl> ls /etc/wireguard
|
||||
wgctl> rule list --tree
|
||||
wgctl> group block --name family
|
||||
wgctl> logs --follow
|
||||
wgctl> ls /etc/wireguard/.wgctl/rules/
|
||||
wgctl> exit
|
||||
EOF
|
||||
}
|
||||
|
||||
function cmd::shell::run() {
|
||||
cmd::shell::_banner
|
||||
cmd::shell::_setup_history
|
||||
|
||||
while true; do
|
||||
local input
|
||||
# Read with readline support (-e) and custom prompt
|
||||
IFS= read -r -e -p "$(cmd::shell::_prompt)" input || break
|
||||
|
||||
# Handle empty input
|
||||
[[ -z "${input// }" ]] && continue
|
||||
|
||||
# Add to history
|
||||
history -s "$input"
|
||||
|
||||
# Handle exit
|
||||
case "${input%% *}" in
|
||||
exit|quit) break ;;
|
||||
esac
|
||||
|
||||
# Execute
|
||||
cmd::shell::_execute "$input"
|
||||
done
|
||||
|
||||
cmd::shell::_save_history
|
||||
printf "\n Goodbye!\n\n"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Tab completion (loaded when shell starts)
|
||||
# Tab completion
|
||||
# ============================================
|
||||
|
||||
function cmd::shell::_setup_completion() {
|
||||
local commands="list add remove inspect block unblock rule group audit logs watch fw config qr rename shell help"
|
||||
local commands="list add remove rm inspect block unblock rule group audit logs watch fw config qr rename service shell help test"
|
||||
|
||||
function _wgctl_shell_complete() {
|
||||
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
|
||||
}
|
||||
|
||||
# Bind completion to the read prompt
|
||||
bind 'set show-all-if-ambiguous on' 2>/dev/null || true
|
||||
bind 'set completion-ignore-case on' 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Run
|
||||
# ============================================
|
||||
|
||||
function cmd::shell::run() {
|
||||
cmd::shell::_banner
|
||||
cmd::shell::_setup_history
|
||||
cmd::shell::_setup_completion
|
||||
|
||||
while true; do
|
||||
local input
|
||||
IFS= read -r -e -p "$(cmd::shell::_prompt)" input || break
|
||||
|
||||
[[ -z "${input// }" ]] && continue
|
||||
|
||||
history -s "$input"
|
||||
|
||||
case "${input%% *}" in
|
||||
exit|quit) break ;;
|
||||
esac
|
||||
|
||||
cmd::shell::_execute "$input"
|
||||
done
|
||||
|
||||
cmd::shell::_save_history
|
||||
printf "\n Goodbye!\n\n"
|
||||
}
|
||||
164
commands/shell.command.sh.bak
Normal file
164
commands/shell.command.sh.bak
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# ============================================
|
||||
# Private helpers
|
||||
# ============================================
|
||||
|
||||
function cmd::shell::_prompt() {
|
||||
local user host dir
|
||||
user=$(whoami)
|
||||
host=$(hostname -s)
|
||||
dir=$(basename "$PWD")
|
||||
printf "\033[1;32m%s@%s\033[0m:\033[0;36m%s\033[0m \033[1;34mwgctl\033[0m> " \
|
||||
"$user" "$host" "$dir"
|
||||
}
|
||||
|
||||
function cmd::shell::_is_wgctl_command() {
|
||||
local cmd="$1"
|
||||
# Check against known wgctl commands
|
||||
local known=(
|
||||
list add remove rm inspect block unblock
|
||||
rule group audit logs watch fw config qr
|
||||
rename keys ip service shell help
|
||||
)
|
||||
local c
|
||||
for c in "${known[@]}"; do
|
||||
[[ "$c" == "$cmd" ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
function cmd::shell::_handle_builtin() {
|
||||
local input="$1"
|
||||
local first="${input%% *}"
|
||||
|
||||
case "$first" in
|
||||
cd)
|
||||
local dir="${input#cd }"
|
||||
[[ "$dir" == "$input" ]] && dir="$HOME"
|
||||
cd "$dir" 2>/dev/null || log::error "cd: $dir: No such file or directory"
|
||||
return 0
|
||||
;;
|
||||
cd*) eval "$input" ;; # eval preserves shell state for cd
|
||||
export|unset|source|.)
|
||||
eval "$input"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
return 1 # not a builtin
|
||||
}
|
||||
|
||||
function cmd::shell::_execute() {
|
||||
local input="$1"
|
||||
local first="${input%% *}"
|
||||
local rest="${input#"$first"}"
|
||||
rest="${rest# }"
|
||||
|
||||
# Handle shell builtins first
|
||||
cmd::shell::_handle_builtin "$input" && return 0
|
||||
|
||||
# Try as wgctl command via dispatcher
|
||||
if cmd::shell::_is_wgctl_command "$first"; then
|
||||
if [[ -n "$rest" ]]; then
|
||||
wgctl::dispatch "$first" $rest || true # never exit REPL on failure
|
||||
else
|
||||
wgctl::dispatch "$first" || true
|
||||
fi
|
||||
return 0 # Always 0 to keep REPL running
|
||||
fi
|
||||
|
||||
# Fall back to bash
|
||||
bash -c "$input" || true # same for bash commands
|
||||
}
|
||||
|
||||
function cmd::shell::_setup_history() {
|
||||
HISTFILE="${HOME}/.wgctl_history"
|
||||
HISTSIZE=1000
|
||||
HISTFILESIZE=2000
|
||||
history -r 2>/dev/null || true
|
||||
}
|
||||
|
||||
function cmd::shell::_save_history() {
|
||||
history -w 2>/dev/null || true
|
||||
}
|
||||
|
||||
function cmd::shell::_banner() {
|
||||
ui::section "wgctl shell"
|
||||
printf "\n"
|
||||
printf " Type wgctl commands directly, or any bash command.\n"
|
||||
printf " Type \033[1mexit\033[0m or \033[1mquit\033[0m to leave.\n"
|
||||
printf " Type \033[1mhelp\033[0m for wgctl commands.\n"
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Run
|
||||
# ============================================
|
||||
|
||||
function cmd::shell::on_load() {
|
||||
: # no flags needed
|
||||
}
|
||||
|
||||
function cmd::shell::help() {
|
||||
cat <<EOF
|
||||
Usage: wgctl shell
|
||||
|
||||
Start an interactive wgctl shell. Supports all wgctl commands
|
||||
directly (no 'wgctl' prefix needed), plus any bash command.
|
||||
|
||||
Builtins handled: cd, export, unset, source
|
||||
Everything else: passed to bash
|
||||
|
||||
Examples:
|
||||
wgctl shell
|
||||
wgctl> list
|
||||
wgctl> inspect --name phone-nuno
|
||||
wgctl> ls /etc/wireguard
|
||||
wgctl> exit
|
||||
EOF
|
||||
}
|
||||
|
||||
function cmd::shell::run() {
|
||||
cmd::shell::_banner
|
||||
cmd::shell::_setup_history
|
||||
|
||||
while true; do
|
||||
local input
|
||||
# Read with readline support (-e) and custom prompt
|
||||
IFS= read -r -e -p "$(cmd::shell::_prompt)" input || break
|
||||
|
||||
# Handle empty input
|
||||
[[ -z "${input// }" ]] && continue
|
||||
|
||||
# Add to history
|
||||
history -s "$input"
|
||||
|
||||
# Handle exit
|
||||
case "${input%% *}" in
|
||||
exit|quit) break ;;
|
||||
esac
|
||||
|
||||
# Execute
|
||||
cmd::shell::_execute "$input"
|
||||
done
|
||||
|
||||
cmd::shell::_save_history
|
||||
printf "\n Goodbye!\n\n"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Tab completion (loaded when shell starts)
|
||||
# ============================================
|
||||
|
||||
function cmd::shell::_setup_completion() {
|
||||
local commands="list add remove inspect block unblock rule group audit logs watch fw config qr rename shell help"
|
||||
|
||||
function _wgctl_shell_complete() {
|
||||
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
|
||||
}
|
||||
|
||||
# Bind completion to the read prompt
|
||||
bind 'set show-all-if-ambiguous on' 2>/dev/null || true
|
||||
bind 'set completion-ignore-case on' 2>/dev/null || true
|
||||
}
|
||||
|
|
@ -166,7 +166,6 @@ function cmd::test::section_rules() {
|
|||
test::section "Rules"
|
||||
cmd::test::run_cmd "rule list" "guest" rule list
|
||||
cmd::test::run_cmd "rule show --name guest" "Description" rule show --name guest
|
||||
cmd::test::run_cmd "rule show --name guest --peers" "Assigned:" rule show --name guest --peers
|
||||
cmd::test::run_cmd "rule show --name user" "Description" rule show --name user
|
||||
cmd::test::run_cmd "rule show --name admin" "Description" rule show --name admin
|
||||
cmd::test::run_cmd_fails "rule show nonexistent" rule show --name nonexistent
|
||||
|
|
@ -209,14 +208,16 @@ function cmd::test::section_fw() {
|
|||
function cmd::test::section_destructive() {
|
||||
test::section "Destructive (modifying state)"
|
||||
|
||||
# Cleanup from any previous failed run
|
||||
# ── Cleanup from any previous failed run ──
|
||||
"$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
|
||||
"$WGCTL_BINARY" group remove --name testgroup --force > /dev/null 2>&1 || true
|
||||
"$WGCTL_BINARY" group remove --name testgroup2 --force > /dev/null 2>&1 || true
|
||||
|
||||
# Add test peer
|
||||
# ── Add test peer ──────────────────────────
|
||||
cmd::test::run_cmd "add phone peer" "added successfully" \
|
||||
add --name testunit --type phone
|
||||
# Block/unblock
|
||||
|
||||
# ── Direct block/unblock ───────────────────
|
||||
cmd::test::run_cmd "block peer" "blocked" \
|
||||
block --name phone-testunit
|
||||
cmd::test::run_cmd "list shows blocked" "blocked" \
|
||||
|
|
@ -224,28 +225,58 @@ function cmd::test::section_destructive() {
|
|||
cmd::test::run_cmd "unblock peer" "unblocked" \
|
||||
unblock --name phone-testunit
|
||||
|
||||
# Rule assign/unassign
|
||||
# ── Rule assign/unassign ───────────────────
|
||||
cmd::test::run_cmd "rule assign" "Assigned" \
|
||||
rule assign --name user --peer phone-testunit
|
||||
cmd::test::run_cmd "rule unassign" "Unassigned" \
|
||||
rule unassign --peer phone-testunit
|
||||
# Re-assign user rule (default) for cleanup
|
||||
/usr/local/bin/wgctl rule assign --name user --peer phone-testunit \
|
||||
"$WGCTL_BINARY" rule assign --name user --peer phone-testunit \
|
||||
> /dev/null 2>&1 || true
|
||||
|
||||
# Group operations
|
||||
# ── Group basic operations ─────────────────
|
||||
cmd::test::run_cmd "group add" "created" \
|
||||
group add --name testgroup --desc "Test group"
|
||||
cmd::test::run_cmd "group peer add" "Added" \
|
||||
group peer add --name testgroup --peer phone-testunit
|
||||
cmd::test::run_cmd "group block" "have been blocked" \
|
||||
cmd::test::run_cmd "group block" "blocked" \
|
||||
group block --name testgroup
|
||||
cmd::test::run_cmd "group unblock" "have been unblocked" \
|
||||
cmd::test::run_cmd "group unblock" "unblocked" \
|
||||
group unblock --name testgroup
|
||||
|
||||
# ── M:N group block tracking ───────────────
|
||||
# Setup: add testunit to a second group
|
||||
"$WGCTL_BINARY" group add --name testgroup2 \
|
||||
--desc "Test group 2" > /dev/null 2>&1
|
||||
"$WGCTL_BINARY" group peer add --name testgroup2 \
|
||||
--peer phone-testunit > /dev/null 2>&1
|
||||
|
||||
# Block from both groups
|
||||
cmd::test::run_cmd "group block first group" "blocked" \
|
||||
group block --name testgroup
|
||||
cmd::test::run_cmd "group block second group" "blocked" \
|
||||
group block --name testgroup2
|
||||
|
||||
# Unblock from first — should stay blocked (second group still blocking)
|
||||
"$WGCTL_BINARY" group unblock --name testgroup > /dev/null 2>&1
|
||||
cmd::test::run_cmd "peer stays blocked after partial unblock" "blocked" \
|
||||
list --blocked
|
||||
|
||||
# Unblock from second — should now be fully unblocked
|
||||
"$WGCTL_BINARY" group unblock --name testgroup2 > /dev/null 2>&1
|
||||
cmd::test::run_cmd "peer unblocked after all groups unblock" "phone-testunit" \
|
||||
list --allowed
|
||||
|
||||
# ── Direct block overrides group block ─────
|
||||
"$WGCTL_BINARY" group block --name testgroup > /dev/null 2>&1
|
||||
cmd::test::run_cmd "direct unblock overrides group block" "unblocked" \
|
||||
unblock --name phone-testunit
|
||||
|
||||
# ── Cleanup groups ─────────────────────────
|
||||
cmd::test::run_cmd "group remove" "removed" \
|
||||
group remove --name testgroup --force
|
||||
"$WGCTL_BINARY" group remove --name testgroup2 --force > /dev/null 2>&1 || true
|
||||
|
||||
# Remove test peer
|
||||
# ── Remove test peer ───────────────────────
|
||||
cmd::test::run_cmd "remove phone peer" "removed" \
|
||||
remove --name phone-testunit --force
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ function cmd::unblock::run() {
|
|||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||
|
||||
# Check if actually blocked
|
||||
if ! peers::is_blocked "$name" && [[ ! -f "$(ctx::block::path "${name}.block")" ]]; then
|
||||
if ! peers::is_blocked "$name" && ! block::has_file "$name"; then
|
||||
log::wg_warning "Client is not blocked: ${name}"
|
||||
return 0
|
||||
fi
|
||||
|
|
@ -108,6 +108,7 @@ function cmd::unblock::run() {
|
|||
# Unblock specific IPs
|
||||
for ip in "${ips[@]}"; do
|
||||
fw::unblock_ip "$client_ip" "$ip"
|
||||
block::remove_rule "$name" "ip" "$ip"
|
||||
done
|
||||
|
||||
# Unblock specific subnets
|
||||
|
|
@ -124,23 +125,34 @@ function cmd::unblock::run() {
|
|||
done
|
||||
|
||||
$quiet || log::wg_success "Unblock rules applied for: ${name}"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
function cmd::unblock::_unblock_all() {
|
||||
local name="${1:-}"
|
||||
local client_ip="${2:-}"
|
||||
local quiet="${3:-false}"
|
||||
local name="${1:?}" client_ip="${2:?}" quiet="${3:-false}"
|
||||
|
||||
fw::unblock_all "$client_ip"
|
||||
fw::remove_block_file "$name"
|
||||
monitor::unwatch_client "$name"
|
||||
# Direct unblock overrides everything — clear all block state
|
||||
block::set_direct "$name" "$client_ip" "false"
|
||||
|
||||
if ! peers::exists_in_server "$name"; then
|
||||
local public_key
|
||||
public_key=$(keys::public "$name") || return 1
|
||||
peers::add_to_server "$name" "$public_key" "$client_ip"
|
||||
peers::reload
|
||||
# Force full unblock regardless of group blocks
|
||||
# (direct unblock = admin override)
|
||||
block::restore_peer "$name" "$client_ip"
|
||||
block::remove_file "$name"
|
||||
|
||||
local rule
|
||||
rule=$(peers::get_meta "$name" "rule")
|
||||
|
||||
[[ -n "$rule" ]] && rule::exists "$rule" && \
|
||||
rule::apply "$rule" "$client_ip" "$name"
|
||||
|
||||
local groups
|
||||
groups=$(block::get_groups "$name")
|
||||
if [[ -n "$groups" ]]; then
|
||||
log::wg_warning "${name} was blocked by group(s): ${groups} — unblocking anyway"
|
||||
fi
|
||||
|
||||
$quiet || log::wg_success "${name} has been unblocked."
|
||||
|
||||
return 0
|
||||
}
|
||||
|
|
@ -21,16 +21,18 @@ function cmd::watch::help() {
|
|||
cat <<EOF
|
||||
Usage: wgctl watch [options]
|
||||
|
||||
Live monitor of WireGuard activity.
|
||||
Shows handshakes from active clients and connection attempts from blocked clients.
|
||||
Live monitor of WireGuard activity. Shows:
|
||||
- Handshakes from connected peers (green)
|
||||
- Connection attempts from blocked peers (red, SOURCE: wg)
|
||||
- Firewall drops from rule-restricted peers (red, SOURCE: fw)
|
||||
|
||||
Options:
|
||||
--type <type> Filter by device type
|
||||
--name <name> Filter by client name
|
||||
--peers <list> Filter by comma-separated peer names (used by group watch)
|
||||
--allowed Show only allowed client handshakes
|
||||
--restricted Show only restricted client events
|
||||
--blocked Show only blocked client attempts
|
||||
--type <type> Filter by device type
|
||||
--peers <list> Comma-separated peer names (used internally by group watch)
|
||||
--blocked Show only blocked peer attempts
|
||||
--allowed Show only handshakes (allowed peers)
|
||||
--restricted Show only firewall drop events
|
||||
|
||||
Examples:
|
||||
wgctl watch
|
||||
|
|
@ -38,6 +40,7 @@ Examples:
|
|||
wgctl watch --allowed
|
||||
wgctl watch --type phone
|
||||
wgctl watch --name phone-nuno
|
||||
wgctl watch --name phone-nuno --type phone
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -164,6 +167,9 @@ function cmd::watch::tail_events() {
|
|||
local blocked_only="${3:-false}" restricted_only="${4:-false}"
|
||||
local allowed_only="${5:-false}" filter_peers="${6:-}"
|
||||
|
||||
declare -A _WATCH_LAST_ATTEMPT=()
|
||||
declare -A _WATCH_LAST_FW=()
|
||||
|
||||
local peer_set=()
|
||||
[[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers"
|
||||
|
||||
|
|
@ -259,6 +265,19 @@ function cmd::watch::tail_events() {
|
|||
|
||||
[[ -z "$src_ip" ]] && continue
|
||||
|
||||
local fw_key="${src_ip}:${dst_ip}:${dst_port}:${proto}"
|
||||
local now
|
||||
now=$(date +%s)
|
||||
local last_fw="${_WATCH_LAST_FW[$fw_key]:-0}"
|
||||
|
||||
local window=30
|
||||
[[ "$proto" == "17" || "$proto" == "udp" ]] && window=10
|
||||
[[ "$proto" == "1" || "$proto" == "icmp" ]] && window=5
|
||||
|
||||
local diff=$(( now - last_fw ))
|
||||
(( diff < window )) && continue
|
||||
_WATCH_LAST_FW["$fw_key"]="$now"
|
||||
|
||||
local client="${ip_to_name[$src_ip]:-$src_ip}"
|
||||
|
||||
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
|
||||
|
|
|
|||
1
core.sh
1
core.sh
|
|
@ -13,5 +13,6 @@ source "${WGCTL_DIR}/core/command.sh"
|
|||
source "${WGCTL_DIR}/core/flag.sh"
|
||||
source "${WGCTL_DIR}/core/json.sh"
|
||||
source "${WGCTL_DIR}/core/ui.sh"
|
||||
source "${WGCTL_DIR}/core/color.sh"
|
||||
source "${WGCTL_DIR}/core/fmt.sh"
|
||||
source "${WGCTL_DIR}/core/test/test.sh"
|
||||
15
core/color.sh
Normal file
15
core/color.sh
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Color module to use for display functions (layouts, etc...)
|
||||
# For loops, avoid them, as they spawn subshells
|
||||
|
||||
# Precompute the variables in loops
|
||||
# local GREEN="\033[0;32m" RED="\033[0;31m" RESET="\033[0m"
|
||||
# printf "${GREEN}+${accepts}${RESET} ${RED}-${drops}${RESET}\n"
|
||||
|
||||
function color::green() { printf "\033[0;32m%s\033[0m" "${1:-}"; }
|
||||
function color::red() { printf "\033[0;31m%s\033[0m" "${1:-}"; }
|
||||
function color::gray() { printf "\033[0;37m%s\033[0m" "${1:-}"; }
|
||||
function color::bold() { printf "\033[1;37m%s\033[0m" "${1:-}"; }
|
||||
function color::cyan() { printf "\033[0;36m%s\033[0m" "${1:-}"; }
|
||||
function color::yellow() { printf "\033[1;33m%s\033[0m" "${1:-}"; }
|
||||
11
core/json.sh
11
core/json.sh
|
|
@ -39,6 +39,17 @@ function json::rule_resolve_field() { python3 "$JSON_HELPER" rule_resolve_fi
|
|||
function json::rule_inspect() { python3 "$JSON_HELPER" rule_inspect "$@" </dev/null; }
|
||||
function json::find_rule_file() { python3 "$JSON_HELPER" find_rule_file "$@" </dev/null; }
|
||||
function json::get_raw() { python3 "$JSON_HELPER" get_raw "$@" </dev/null; }
|
||||
function json::count_resolved() { python3 "$JSON_HELPER" count_resolved "$(ctx::rules)" "$@" </dev/null; }
|
||||
function json::block_get() { python3 "$JSON_HELPER" block_get "$@" </dev/null; }
|
||||
function json::block_is_blocked() { python3 "$JSON_HELPER" block_is_blocked "$@" </dev/null; }
|
||||
function json::block_set_direct() { python3 "$JSON_HELPER" block_set_direct "$@" </dev/null; }
|
||||
function json::block_add_group() { python3 "$JSON_HELPER" block_add_group "$@" </dev/null; }
|
||||
function json::block_remove_group() { python3 "$JSON_HELPER" block_remove_group "$@" </dev/null; }
|
||||
function json::block_add_rule() { python3 "$JSON_HELPER" block_add_rule "$@" </dev/null; }
|
||||
function json::block_remove_rule() { python3 "$JSON_HELPER" block_remove_rule "$@" </dev/null; }
|
||||
function json::block_get_rules() { python3 "$JSON_HELPER" block_get_rules "$@" </dev/null; }
|
||||
function json::block_get_groups() { python3 "$JSON_HELPER" block_get_groups "$@" </dev/null; }
|
||||
function json::block_get_direct() { python3 "$JSON_HELPER" block_get_direct "$@" </dev/null; }
|
||||
|
||||
function json::peer_transfer() {
|
||||
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \
|
||||
|
|
|
|||
|
|
@ -1132,6 +1132,186 @@ def get_raw(file, key):
|
|||
except:
|
||||
pass
|
||||
|
||||
def count_resolved(rules_dir, rule_name, key):
|
||||
"""Count entries in resolved rule field"""
|
||||
try:
|
||||
resolved = _rule_resolve_internal(rules_dir, rule_name)
|
||||
print(len(resolved.get(key, [])))
|
||||
except:
|
||||
print(0)
|
||||
|
||||
def _block_init(peer_ip):
|
||||
"""Return empty block structure"""
|
||||
return {
|
||||
"peer_ip": peer_ip,
|
||||
"blocked_direct": False,
|
||||
"blocked_by_groups": [],
|
||||
"rules": []
|
||||
}
|
||||
|
||||
def _block_read(file):
|
||||
try:
|
||||
with open(file) as f:
|
||||
content = f.read().strip()
|
||||
if not content:
|
||||
return None # empty file = no block data
|
||||
try:
|
||||
return json.loads(content)
|
||||
except json.JSONDecodeError:
|
||||
# Old format — migrate
|
||||
lines = content.split('\n')
|
||||
peer_ip = lines[0].split()[0] if lines else ''
|
||||
new_data = {
|
||||
"peer_ip": peer_ip,
|
||||
"blocked_direct": True,
|
||||
"blocked_by_groups": [],
|
||||
"rules": [{"name": "full block", "type": "full"}]
|
||||
}
|
||||
with open(file, 'w') as f:
|
||||
json.dump(new_data, f, indent=2)
|
||||
return new_data
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _block_write(file, data):
|
||||
"""Write block file"""
|
||||
with open(file, 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
|
||||
def block_get(file):
|
||||
"""Read and print block file as JSON"""
|
||||
data = _block_read(file)
|
||||
if data:
|
||||
print(json.dumps(data))
|
||||
|
||||
def block_is_blocked(file):
|
||||
"""Return true if peer is effectively blocked"""
|
||||
data = _block_read(file)
|
||||
if not data:
|
||||
print("false")
|
||||
return
|
||||
blocked = data.get("blocked_direct", False) or \
|
||||
bool(data.get("blocked_by_groups", []))
|
||||
print("true" if blocked else "false")
|
||||
|
||||
def block_set_direct(file, peer_ip, value):
|
||||
"""Set blocked_direct"""
|
||||
try:
|
||||
data = _block_read(file) or _block_init(peer_ip)
|
||||
data["blocked_direct"] = value.lower() == "true"
|
||||
data["peer_ip"] = peer_ip
|
||||
_block_write(file, data)
|
||||
remaining = data["blocked_direct"] or bool(data.get("blocked_by_groups", []))
|
||||
pass
|
||||
# print("true" if remaining else "false")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def block_add_group(file, peer_ip, group):
|
||||
"""Add group to blocked_by_groups"""
|
||||
try:
|
||||
data = _block_read(file) or _block_init(peer_ip)
|
||||
data["peer_ip"] = peer_ip
|
||||
groups = data.get("blocked_by_groups", [])
|
||||
if group not in groups:
|
||||
groups.append(group)
|
||||
data["blocked_by_groups"] = groups
|
||||
_block_write(file, data)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def block_remove_group(file, peer_ip, group):
|
||||
"""Remove group from blocked_by_groups, return whether still blocked"""
|
||||
try:
|
||||
data = _block_read(file) or _block_init(peer_ip)
|
||||
groups = data.get("blocked_by_groups", [])
|
||||
if group in groups:
|
||||
groups.remove(group)
|
||||
data["blocked_by_groups"] = groups
|
||||
_block_write(file, data)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
# print("true" if remaining else "false")
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def block_add_rule(file, peer_ip, rule_type, name="", target="",
|
||||
port="", proto=""):
|
||||
"""Add a block rule entry"""
|
||||
try:
|
||||
data = _block_read(file) or _block_init(peer_ip)
|
||||
data["peer_ip"] = peer_ip
|
||||
rule = {"type": rule_type}
|
||||
if name: rule["name"] = name
|
||||
if target: rule["target"] = target
|
||||
if port: rule["port"] = port
|
||||
if proto: rule["proto"] = proto
|
||||
|
||||
rules = data.get("rules", [])
|
||||
for existing in rules:
|
||||
if existing.get("type") == rule_type and \
|
||||
existing.get("target","") == target and \
|
||||
existing.get("port","") == port and \
|
||||
existing.get("proto","") == proto:
|
||||
return # already exists, skip
|
||||
|
||||
rules.append(rule)
|
||||
data["rules"] = rules
|
||||
_block_write(file, data)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def block_remove_rule(file, rule_type, target="", port="", proto=""):
|
||||
"""Remove matching block rule entry"""
|
||||
try:
|
||||
data = _block_read(file)
|
||||
if not data:
|
||||
return
|
||||
rules = data.get("rules", [])
|
||||
filtered = []
|
||||
for r in rules:
|
||||
if r.get("type") == rule_type and \
|
||||
r.get("target", "") == target and \
|
||||
r.get("port", "") == port and \
|
||||
r.get("proto", "") == proto:
|
||||
continue # remove this one
|
||||
filtered.append(r)
|
||||
data["rules"] = filtered
|
||||
_block_write(file, data)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def block_get_rules(file):
|
||||
"""Print rules as pipe-separated lines: name|type|target|port|proto"""
|
||||
data = _block_read(file)
|
||||
if not data:
|
||||
return
|
||||
for r in data.get("rules", []):
|
||||
print(f"{r.get('name','')}|{r.get('type','')}|"
|
||||
f"{r.get('target','')}|{r.get('port','')}|{r.get('proto','')}")
|
||||
|
||||
def block_get_groups(file):
|
||||
data = _block_read(file)
|
||||
if not data:
|
||||
return
|
||||
print(','.join(data.get('blocked_by_groups', [])))
|
||||
|
||||
def block_get_direct(file):
|
||||
data = _block_read(file)
|
||||
if not data:
|
||||
print('false')
|
||||
return
|
||||
print('true' if data.get('blocked_direct', False) else 'false')
|
||||
|
||||
commands = {
|
||||
'get': lambda args: get(args[0], args[1]),
|
||||
'set': lambda args: set_key(args[0], args[1], args[2]),
|
||||
|
|
@ -1179,6 +1359,28 @@ commands = {
|
|||
'rule_inspect': lambda args: rule_inspect(args[0], args[1]),
|
||||
'find_rule_file': lambda args: print(find_rule_file(args[0], args[1])),
|
||||
'get_raw': lambda args: print(get_raw(args[0], args[1])),
|
||||
'count_resolved': lambda args: count_resolved(args[0], args[1], args[2]),
|
||||
'block_get': lambda args: block_get(args[0]),
|
||||
'block_is_blocked': lambda args: block_is_blocked(args[0]),
|
||||
'block_set_direct': lambda args: block_set_direct(args[0], args[1], args[2]),
|
||||
'block_add_group': lambda args: block_add_group(args[0], args[1], args[2]),
|
||||
'block_remove_group': lambda args: block_remove_group(args[0], args[1], args[2]),
|
||||
'block_add_rule': lambda args: block_add_rule(
|
||||
args[0], args[1], args[2],
|
||||
args[3] if len(args) > 3 else '',
|
||||
args[4] if len(args) > 4 else '',
|
||||
args[5] if len(args) > 5 else '',
|
||||
args[6] if len(args) > 6 else ''
|
||||
),
|
||||
'block_remove_rule': lambda args: block_remove_rule(
|
||||
args[0], args[1],
|
||||
args[2] if len(args) > 2 else '',
|
||||
args[3] if len(args) > 3 else '',
|
||||
args[4] if len(args) > 4 else ''
|
||||
),
|
||||
'block_get_rules': lambda args: block_get_rules(args[0]),
|
||||
'block_get_groups': lambda args: block_get_groups(args[0]),
|
||||
'block_get_direct': lambda args: block_get_direct(args[0]),
|
||||
}
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
{
|
||||
"phone-fred": "94.63.0.129",
|
||||
"phone-helena": "148.69.46.73",
|
||||
"phone-nuno": "94.63.0.129",
|
||||
"phone-nuno": "148.69.51.201",
|
||||
"tablet-nuno": "148.69.202.5",
|
||||
"guest-zephyr": "5.13.82.5",
|
||||
"guest-zephyr-test": "94.63.0.129",
|
||||
"desktop-roboclean": "46.189.215.231"
|
||||
"desktop-roboclean": "46.189.215.231",
|
||||
"laptop-nuno": "94.63.0.129"
|
||||
}
|
||||
167
modules/block.module.sh
Normal file
167
modules/block.module.sh
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# ============================================
|
||||
# Block file management
|
||||
# ============================================
|
||||
|
||||
# ── Core state queries ─────────────────────
|
||||
|
||||
function block::file() {
|
||||
local name="${1:?name required}"
|
||||
ctx::block::path "${name}.block"
|
||||
}
|
||||
|
||||
function block::has_file() {
|
||||
local name="${1:?}"
|
||||
[[ -f "$(block::file "$name")" ]]
|
||||
}
|
||||
|
||||
function block::is_blocked() {
|
||||
local name="${1:?}"
|
||||
block::has_file "$name" || return 1
|
||||
local result
|
||||
result=$(json::block_is_blocked "$(block::file "$name")")
|
||||
[[ "$result" == "true" ]]
|
||||
}
|
||||
|
||||
function block::is_blocked_direct() {
|
||||
local name="${1:?}"
|
||||
block::has_file "$name" || { echo "false"; return 0; }
|
||||
json::block_get_direct "$(block::file "$name")"
|
||||
}
|
||||
|
||||
function block::get_groups() {
|
||||
local name="${1:?}"
|
||||
block::has_file "$name" || return 0
|
||||
json::block_get_groups "$(block::file "$name")"
|
||||
}
|
||||
|
||||
function block::get_rules() {
|
||||
local name="${1:?}"
|
||||
block::has_file "$name" || return 0
|
||||
json::block_get_rules "$(block::file "$name")"
|
||||
}
|
||||
|
||||
# ── State mutations ────────────────────────
|
||||
|
||||
function block::set_direct() {
|
||||
local name="${1:?}" client_ip="${2:?}" value="${3:-true}"
|
||||
local file
|
||||
file=$(block::file "$name")
|
||||
json::block_set_direct "$file" "$client_ip" "$value"
|
||||
}
|
||||
|
||||
function block::add_group() {
|
||||
local name="${1:?}" client_ip="${2:?}" group="${3:?}"
|
||||
local file
|
||||
file=$(block::file "$name")
|
||||
json::block_add_group "$file" "$client_ip" "$group"
|
||||
}
|
||||
|
||||
function block::remove_group() {
|
||||
local name="${1:?}" client_ip="${2:?}" group="${3:?}"
|
||||
local file
|
||||
file=$(block::file "$name")
|
||||
json::block_remove_group "$file" "$client_ip" "$group"
|
||||
}
|
||||
|
||||
function block::add_rule() {
|
||||
local name="${1:?}" client_ip="${2:?}"
|
||||
local rule_type="${3:?}" rule_name="${4:-}"
|
||||
local target="${5:-}" port="${6:-}" proto="${7:-}"
|
||||
local file
|
||||
file=$(block::file "$name")
|
||||
json::block_add_rule "$file" "$client_ip" "$rule_type" \
|
||||
"$rule_name" "$target" "$port" "$proto"
|
||||
}
|
||||
|
||||
function block::remove_rule() {
|
||||
local name="${1:?}"
|
||||
local rule_type="${2:?}" target="${3:-}" port="${4:-}" proto="${5:-}"
|
||||
local file
|
||||
file=$(block::file "$name")
|
||||
json::block_remove_rule "$file" "$rule_type" "$target" "$port" "$proto"
|
||||
}
|
||||
|
||||
function block::remove_file() {
|
||||
local name="${1:?}"
|
||||
rm -f "$(block::file "$name")"
|
||||
}
|
||||
|
||||
function block::rename() {
|
||||
local name="${1:?}" new_name="${2:?}"
|
||||
local old_file new_file
|
||||
old_file=$(block::file "$name")
|
||||
new_file=$(block::file "$new_name")
|
||||
[[ -f "$old_file" ]] && mv "$old_file" "$new_file"
|
||||
}
|
||||
|
||||
# ── High level operations ──────────────────
|
||||
|
||||
function block::apply_full() {
|
||||
local name="${1:?}" client_ip="${2:?}"
|
||||
|
||||
fw::flush_peer "$client_ip"
|
||||
fw::block_all "$client_ip" "$name"
|
||||
block::add_rule "$name" "$client_ip" "full" "full block"
|
||||
|
||||
local public_key endpoint
|
||||
public_key=$(keys::public "$name") || return 1
|
||||
endpoint=$(monitor::endpoint_for_key "$public_key")
|
||||
[[ -n "$endpoint" ]] && monitor::watch "$endpoint" "$name"
|
||||
peers::remove_from_server "$name"
|
||||
peers::reload
|
||||
}
|
||||
|
||||
|
||||
function block::restore_peer() {
|
||||
local name="${1:?}" client_ip="${2:?}"
|
||||
fw::unblock_all "$client_ip"
|
||||
fw::flush_peer "$client_ip"
|
||||
monitor::unwatch_client "$name"
|
||||
if ! peers::exists_in_server "$name"; then
|
||||
local public_key
|
||||
public_key=$(keys::public "$name") || return 1
|
||||
peers::add_to_server "$name" "$public_key" "$client_ip"
|
||||
peers::reload
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
function block::restore_all() {
|
||||
while IFS= read -r peer_name; do
|
||||
block::has_file "$peer_name" || continue
|
||||
local client_ip
|
||||
client_ip=$(peers::get_ip "$peer_name")
|
||||
[[ -z "$client_ip" ]] && continue
|
||||
while IFS="|" read -r bname btype target port proto; do
|
||||
[[ -z "$btype" ]] && continue
|
||||
case "$btype" in
|
||||
full) fw::block_all "$client_ip" "$peer_name" ;;
|
||||
ip) fw::block_ip "$client_ip" "$target" ;;
|
||||
port) fw::block_port "$client_ip" "$target" "$port" "${proto:-tcp}" ;;
|
||||
subnet) fw::block_subnet "$client_ip" "$target" ;;
|
||||
esac
|
||||
done < <(block::get_rules "$peer_name")
|
||||
done < <(peers::all)
|
||||
}
|
||||
|
||||
# ── Display helpers ────────────────────────
|
||||
|
||||
function block::format_rules() {
|
||||
local name="${1:?}"
|
||||
block::has_file "$name" || return 0
|
||||
while IFS="|" read -r bname btype target port proto; do
|
||||
[[ -z "$btype" ]] && continue
|
||||
local display
|
||||
case "$btype" in
|
||||
full) display="all traffic" ;;
|
||||
ip) display="$target" ;;
|
||||
port) display="${target}:${port}:${proto}" ;;
|
||||
subnet) display="$target" ;;
|
||||
esac
|
||||
local label="${bname:-$btype}"
|
||||
printf " \033[0;31m-\033[0m %-30s \033[0;37m%s\033[0m\n" \
|
||||
"$display" "$label"
|
||||
done < <(block::get_rules "$name")
|
||||
}
|
||||
|
|
@ -96,6 +96,20 @@ function fw::unblock_subnet() {
|
|||
log::wg_unblock "Unblocked ${client_ip} → subnet ${target_subnet}"
|
||||
}
|
||||
|
||||
function fw::allow_subnet() {
|
||||
local client_ip="${1:-}" target_subnet="${2:-}"
|
||||
|
||||
fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j ACCEPT \
|
||||
|| iptables -I FORWARD 1 -s "$client_ip" -d "$target_subnet" -j ACCEPT
|
||||
}
|
||||
|
||||
function fw::unallow_subnet() {
|
||||
local client_ip="${1:-}" target_subnet="${2:-}"
|
||||
|
||||
fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j ACCEPT \
|
||||
&& iptables -D FORWARD -s "$client_ip" -d "$target_subnet" -j ACCEPT 2>/dev/null || true
|
||||
}
|
||||
|
||||
function fw::allow_ip() {
|
||||
local client_ip="${1:-}" target_ip="${2:-}"
|
||||
|
||||
|
|
@ -192,57 +206,46 @@ function fw::remove_dns_redirect() {
|
|||
}
|
||||
|
||||
# ============================================
|
||||
# Persistence — block files
|
||||
# Peer related
|
||||
# ============================================
|
||||
|
||||
function fw::save_block() {
|
||||
local name="$1"
|
||||
local client_ip="$2"
|
||||
local target="${3:-}"
|
||||
local port="${4:-}"
|
||||
local proto="${5:-}"
|
||||
function fw::list_peer_rules() {
|
||||
local ip="${1:-}" show_nflog="${2:-false}"
|
||||
|
||||
local block_file
|
||||
block_file="$(ctx::block::path "${name}.block")"
|
||||
|
||||
echo "${client_ip} ${target} ${port} ${proto}" >> "$block_file"
|
||||
log::debug "Persisted block rule for: ${name}"
|
||||
}
|
||||
|
||||
function fw::remove_block_file() {
|
||||
local name="$1"
|
||||
local block_file
|
||||
block_file="$(ctx::block::path "${name}.block")"
|
||||
|
||||
rm -f "$block_file"
|
||||
log::debug "Removed block file for: ${name}"
|
||||
}
|
||||
|
||||
function fw::restore_blocks() {
|
||||
local blocks_dir
|
||||
blocks_dir="$(ctx::blocks)"
|
||||
|
||||
# Restore rules from meta files (new system)
|
||||
rule::restore_all
|
||||
|
||||
# Restore per-client full-blocks (wgctl block/unblock system)
|
||||
for block_file in "${blocks_dir}"/*.block; do
|
||||
[[ -f "$block_file" ]] || continue
|
||||
local name
|
||||
name=$(basename "$block_file" .block)
|
||||
while IFS=" " read -r client_ip target port proto; do
|
||||
if [[ -z "$target" ]]; then
|
||||
fw::block_all "$client_ip" "$name"
|
||||
elif [[ -n "$port" ]]; then
|
||||
fw::block_port "$client_ip" "$target" "$port" "${proto:-tcp}"
|
||||
else
|
||||
fw::block_ip "$client_ip" "$target"
|
||||
fi
|
||||
done < "$block_file"
|
||||
log::debug "Restored block rules for: ${name}"
|
||||
fw::forward_rules_for_ip "$ip" | while IFS= read -r line; do
|
||||
[[ -z "$line" ]] && continue
|
||||
! $show_nflog && [[ "$line" =~ NFLOG ]] && continue
|
||||
fw::format_rule "$line"
|
||||
done
|
||||
}
|
||||
|
||||
function fw::format_rule() {
|
||||
local line="${1:-}"
|
||||
[[ -z "$line" ]] && return 0
|
||||
|
||||
# Parse verbose iptables format:
|
||||
# pkts bytes target prot opt in out src dst [extra]
|
||||
local target prot src dst extra
|
||||
target=$(awk '{print $3}' <<< "$line")
|
||||
prot=$(awk '{print $4}' <<< "$line")
|
||||
src=$(awk '{print $8}' <<< "$line")
|
||||
dst=$(awk '{print $9}' <<< "$line")
|
||||
extra=$(awk '{for(i=10;i<=NF;i++) printf $i" "}' <<< "$line" | xargs)
|
||||
|
||||
local prot_name
|
||||
prot_name=$(fw::proto_name "$prot")
|
||||
|
||||
local dst_fmt="$dst"
|
||||
if [[ "$extra" =~ dpt:([0-9]+) ]]; then
|
||||
local port="${BASH_REMATCH[1]}"
|
||||
dst_fmt="${dst}:${port}:${prot_name}"
|
||||
fi
|
||||
|
||||
local formatted
|
||||
formatted=$(printf " %-8s %-15s → %s" "$target" "$src" "$dst_fmt")
|
||||
ui::firewall_rule "$formatted"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Helpers
|
||||
# ============================================
|
||||
|
|
@ -251,6 +254,63 @@ function fw::_nat_exists() {
|
|||
fw::_rule_exists nat PREROUTING "$@"
|
||||
}
|
||||
|
||||
function fw::forward_rules_for_ip() {
|
||||
local ip="${1:-}"
|
||||
iptables -L FORWARD -n -v </dev/null | grep -F "$ip"
|
||||
}
|
||||
|
||||
function fw::proto_name() {
|
||||
local proto="${1:-0}"
|
||||
case "$proto" in
|
||||
6) echo "tcp" ;;
|
||||
17) echo "udp" ;;
|
||||
1) echo "icmp" ;;
|
||||
0) echo "all" ;;
|
||||
tcp|udp|icmp|all) echo "$proto" ;;
|
||||
*) echo "$proto" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
function fw::flush_forward() {
|
||||
iptables -F FORWARD
|
||||
log::debug "Flushed FORWARD chain"
|
||||
}
|
||||
|
||||
function fw::flush_nat() {
|
||||
iptables -t nat -F PREROUTING
|
||||
log::debug "Flushed NAT PREROUTING chain"
|
||||
}
|
||||
|
||||
function fw::flush_all() {
|
||||
fw::flush_forward
|
||||
fw::flush_nat
|
||||
}
|
||||
|
||||
function fw::nat_add_dns_redirect() {
|
||||
local subnet="${1:-}" dns="${2:-}" interface="${3:-wg0}"
|
||||
iptables -t nat -A PREROUTING -i "$interface" -s "$subnet" \
|
||||
-p udp --dport 53 ! -d "$dns" \
|
||||
-j LOG --log-prefix "wgctl-dns-redirect: " --log-level 4
|
||||
iptables -t nat -A PREROUTING -i "$interface" -s "$subnet" \
|
||||
-p udp --dport 53 -j DNAT --to-destination "${dns}:53"
|
||||
iptables -t nat -A PREROUTING -i "$interface" -s "$subnet" \
|
||||
-p tcp --dport 53 -j DNAT --to-destination "${dns}:53"
|
||||
}
|
||||
|
||||
function fw::nat_remove_dns_redirect() {
|
||||
local subnet="${1:-}" dns="${2:-}" interface="${3:-wg0}"
|
||||
iptables -t nat -D PREROUTING -i "$interface" -s "$subnet" \
|
||||
-p udp --dport 53 ! -d "$dns" \
|
||||
-j LOG --log-prefix "wgctl-dns-redirect: " \
|
||||
--log-level 4 2>/dev/null || true
|
||||
iptables -t nat -D PREROUTING -i "$interface" -s "$subnet" \
|
||||
-p udp --dport 53 -j DNAT \
|
||||
--to-destination "${dns}:53" 2>/dev/null || true
|
||||
iptables -t nat -D PREROUTING -i "$interface" -s "$subnet" \
|
||||
-p tcp --dport 53 -j DNAT \
|
||||
--to-destination "${dns}:53" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# private
|
||||
# ============================================
|
||||
|
|
|
|||
|
|
@ -160,9 +160,14 @@ function peers::exists_in_server() {
|
|||
grep -q "^# ${name}$" "$(config::config_file)"
|
||||
}
|
||||
|
||||
# function peers::is_blocked() {
|
||||
# local name="${1:-}"
|
||||
# peers::exists_in_server "$name" && return 1 || return 0
|
||||
# }
|
||||
|
||||
function peers::is_blocked() {
|
||||
local name="${1:-}"
|
||||
peers::exists_in_server "$name" && return 1 || return 0
|
||||
block::is_blocked "$name"
|
||||
}
|
||||
|
||||
# ============================================
|
||||
|
|
|
|||
|
|
@ -192,7 +192,11 @@ function rule::unapply() {
|
|||
# Remove allow_ips
|
||||
while IFS= read -r allow_ip; do
|
||||
[[ -z "$allow_ip" ]] && continue
|
||||
if [[ "$allow_ip" == *"/"* ]]; then
|
||||
fw::unallow_subnet "$client_ip" "$allow_ip"
|
||||
else
|
||||
fw::unallow_ip "$client_ip" "$allow_ip"
|
||||
fi
|
||||
done < <(rule::get "$rule_name" "allow_ips")
|
||||
|
||||
# Remove block_ports
|
||||
|
|
@ -207,7 +211,11 @@ function rule::unapply() {
|
|||
# Remove block_ips
|
||||
while IFS= read -r block_ip; do
|
||||
[[ -z "$block_ip" ]] && continue
|
||||
if [[ "$block_ip" == *"/"* ]]; then
|
||||
fw::unblock_subnet "$client_ip" "$block_ip"
|
||||
else
|
||||
fw::unblock_ip "$client_ip" "$block_ip"
|
||||
fi
|
||||
done < <(rule::get "$rule_name" "block_ips")
|
||||
|
||||
# Remove DNS redirect if applicable
|
||||
|
|
@ -247,7 +255,8 @@ function rule::reapply_all() {
|
|||
local client_ip
|
||||
client_ip=$(peers::get_ip "$peer_name")
|
||||
[[ -z "$client_ip" ]] && continue
|
||||
rule::unapply "$rule_name" "$client_ip"
|
||||
# FLUSH first to ensure clean ordering
|
||||
fw::flush_peer "$client_ip"
|
||||
rule::apply "$rule_name" "$client_ip" "$peer_name"
|
||||
(( count++ )) || true
|
||||
done
|
||||
|
|
@ -257,8 +266,12 @@ function rule::reapply_all() {
|
|||
|
||||
function rule::restore_all() {
|
||||
while IFS= read -r peer_name; do
|
||||
# Skip blocked peers - no fw rules needed when blocked
|
||||
block::is_blocked "$peer_name" && continue
|
||||
|
||||
local rule_name
|
||||
rule_name=$(peers::get_meta "$peer_name" "rule")
|
||||
|
||||
[[ -z "$rule_name" ]] && continue
|
||||
|
||||
if ! rule::exists "$rule_name"; then
|
||||
|
|
@ -281,27 +294,10 @@ function rule::restore_all() {
|
|||
|
||||
function rule::apply_dns_redirect() {
|
||||
local client_subnet="${1:-}"
|
||||
local dns
|
||||
dns="$(config::dns)"
|
||||
|
||||
iptables -t nat -A PREROUTING -i wg0 -s "$client_subnet" -p udp --dport 53 \
|
||||
! -d "$dns" -j LOG --log-prefix "wgctl-dns-redirect: " --log-level 4
|
||||
iptables -t nat -A PREROUTING -i wg0 -s "$client_subnet" -p udp --dport 53 \
|
||||
-j DNAT --to-destination "${dns}:53"
|
||||
iptables -t nat -A PREROUTING -i wg0 -s "$client_subnet" -p tcp --dport 53 \
|
||||
-j DNAT --to-destination "${dns}:53"
|
||||
fw::nat_add_dns_redirect "$client_subnet" "$(config::dns)" "$(config::interface)"
|
||||
}
|
||||
|
||||
function rule::remove_dns_redirect() {
|
||||
local client_subnet="${1:-}"
|
||||
local dns
|
||||
dns="$(config::dns)"
|
||||
|
||||
iptables -t nat -D PREROUTING -i wg0 -s "$client_subnet" -p udp --dport 53 \
|
||||
! -d "$dns" -j LOG --log-prefix "wgctl-dns-redirect: " \
|
||||
--log-level 4 2>/dev/null || true
|
||||
iptables -t nat -D PREROUTING -i wg0 -s "$client_subnet" -p udp --dport 53 \
|
||||
-j DNAT --to-destination "${dns}:53" 2>/dev/null || true
|
||||
iptables -t nat -D PREROUTING -i wg0 -s "$client_subnet" -p tcp --dport 53 \
|
||||
-j DNAT --to-destination "${dns}:53" 2>/dev/null || true
|
||||
fw::nat_remove_dns_redirect "$client_subnet" "$(config::dns)" "$(config::interface)"
|
||||
}
|
||||
39
wgctl
39
wgctl
|
|
@ -17,6 +17,7 @@ load_module peers
|
|||
load_module firewall
|
||||
load_module monitor
|
||||
load_module rule
|
||||
load_module block
|
||||
load_module group
|
||||
|
||||
# ============================================
|
||||
|
|
@ -102,47 +103,53 @@ $(log::section "wgctl — WireGuard Management" 2>/dev/null || printf "\n wgctl
|
|||
Usage: wgctl <command> [options]
|
||||
|
||||
Client Commands:
|
||||
add, new Add a new client
|
||||
add, new Add a new client (--type, --subtype, --rule, --group)
|
||||
remove, rm Remove a client
|
||||
rename, mv Rename a client
|
||||
list, ls List all clients
|
||||
inspect Show detailed client info
|
||||
list, ls List all clients (--type, --rule, --group, --blocked...)
|
||||
inspect Show detailed client info (--config, --qr)
|
||||
config Show client config
|
||||
qr Show QR code for a client
|
||||
|
||||
Access Control:
|
||||
block, ban Block a client entirely
|
||||
block, ban Block a client entirely or restrict specific IPs/ports
|
||||
unblock, unban Restore client access
|
||||
rule Manage firewall rules (list, show, add, assign...)
|
||||
rule Manage firewall rules with inheritance
|
||||
list, show, inspect, add, update, assign, reapply...
|
||||
|
||||
Organization:
|
||||
group Manage peer groups (list, show, block, watch...)
|
||||
group Manage peer groups
|
||||
list, show, add, block, unblock, watch, logs...
|
||||
|
||||
Monitoring:
|
||||
watch Live monitor of WireGuard activity
|
||||
logs Show activity and firewall logs
|
||||
audit Verify firewall rules are correctly applied
|
||||
fw Inspect firewall rules
|
||||
watch Live monitor — handshakes, fw drops, blocked attempts
|
||||
logs Show and manage activity logs
|
||||
show, remove, rotate, --follow
|
||||
audit Verify firewall rules match configuration
|
||||
fw Inspect iptables firewall rules
|
||||
|
||||
Service:
|
||||
service Manage WireGuard service (start/stop/restart/status)
|
||||
restart Restart WireGuard
|
||||
shell Start interactive wgctl shell
|
||||
shell Interactive wgctl shell
|
||||
|
||||
Development:
|
||||
test Run the wgctl test suite
|
||||
|
||||
Common examples:
|
||||
wgctl add --name nuno --type phone
|
||||
wgctl add --name visitor --type guest --subtype phone --group family
|
||||
wgctl add --name nuno --type phone --rule admin --group family
|
||||
wgctl list --blocked
|
||||
wgctl list --group family
|
||||
wgctl list --rule user --group family
|
||||
wgctl block --name phone-nuno
|
||||
wgctl inspect --name phone-nuno
|
||||
wgctl rule assign --name admin --peer laptop-nuno
|
||||
wgctl rule list --tree
|
||||
wgctl rule show --name guest
|
||||
wgctl rule add --name dev-01 --group vm-rules --extends no-lan
|
||||
wgctl group block --name family
|
||||
wgctl logs --follow
|
||||
wgctl audit
|
||||
wgctl logs rotate --days 7
|
||||
wgctl audit --fix
|
||||
wgctl fw list --rule guest
|
||||
|
||||
Run 'wgctl <command> --help' for command-specific help.
|
||||
EOF
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue