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
|
cat <<EOF
|
||||||
Usage: wgctl audit [options]
|
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:
|
Options:
|
||||||
--fix Attempt to fix detected issues
|
--peer <name> Audit specific peer only
|
||||||
--peer <name> Audit specific peer only
|
--type <type> Audit peers of specific device type only
|
||||||
--type <type> Audit peers of specific 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:
|
Examples:
|
||||||
wgctl audit
|
wgctl audit
|
||||||
wgctl audit --peer phone-nuno
|
wgctl audit --peer phone-nuno
|
||||||
|
wgctl audit --type guest
|
||||||
wgctl audit --fix
|
wgctl audit --fix
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
@ -98,10 +105,10 @@ function cmd::audit::check_peer() {
|
||||||
local rule_file
|
local rule_file
|
||||||
rule_file="$(ctx::rule::path "${rule}.rule")"
|
rule_file="$(ctx::rule::path "${rule}.rule")"
|
||||||
local block_ports block_ips allow_ports allow_ips expected
|
local block_ports block_ips allow_ports allow_ips expected
|
||||||
block_ports=$(json::count "$rule_file" "block_ports")
|
block_ports=$(json::count_resolved "$rule" "block_ports")
|
||||||
block_ips=$(json::count "$rule_file" "block_ips")
|
block_ips=$(json::count_resolved "$rule" "block_ips")
|
||||||
allow_ports=$(json::count "$rule_file" "allow_ports")
|
allow_ports=$(json::count_resolved "$rule" "allow_ports")
|
||||||
allow_ips=$(json::count "$rule_file" "allow_ips")
|
allow_ips=$(json::count_resolved "$rule" "allow_ips")
|
||||||
expected=$(( (block_ports + block_ips) * 2 + allow_ports + allow_ips ))
|
expected=$(( (block_ports + block_ips) * 2 + allow_ports + allow_ips ))
|
||||||
|
|
||||||
# actual is passed in as $3 — no iptables call here
|
# actual is passed in as $3 — no iptables call here
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ function cmd::block::on_load() {
|
||||||
flag::register --port
|
flag::register --port
|
||||||
flag::register --proto
|
flag::register --proto
|
||||||
flag::register --subnet
|
flag::register --subnet
|
||||||
|
flag::register --block-name
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -52,6 +53,7 @@ EOF
|
||||||
function cmd::block::run() {
|
function cmd::block::run() {
|
||||||
local name=""
|
local name=""
|
||||||
local type=""
|
local type=""
|
||||||
|
local block_name=""
|
||||||
local ips=()
|
local ips=()
|
||||||
local subnets=()
|
local subnets=()
|
||||||
local ports=()
|
local ports=()
|
||||||
|
|
@ -59,13 +61,14 @@ function cmd::block::run() {
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--name) name="$2"; shift 2 ;;
|
--name) name="$2"; shift 2 ;;
|
||||||
--type) type="$2"; shift 2 ;;
|
--type) type="$2"; shift 2 ;;
|
||||||
--ip) ips+=("$2"); shift 2 ;;
|
--ip) ips+=("$2"); shift 2 ;;
|
||||||
--force) force=true; shift ;;
|
--block-name) block_name="$2"; shift 2 ;;
|
||||||
--quiet) quiet=true; shift ;;
|
--force) force=true; shift ;;
|
||||||
--subnet) subnets+=("$2"); shift 2 ;;
|
--quiet) quiet=true; shift ;;
|
||||||
--port) ports+=("$2"); shift 2 ;;
|
--subnet) subnets+=("$2"); shift 2 ;;
|
||||||
|
--port) ports+=("$2"); shift 2 ;;
|
||||||
--help) cmd::block::help; return ;;
|
--help) cmd::block::help; return ;;
|
||||||
*)
|
*)
|
||||||
log::error "Unknown flag: $1"
|
log::error "Unknown flag: $1"
|
||||||
|
|
@ -84,7 +87,7 @@ function cmd::block::run() {
|
||||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||||
|
|
||||||
# Check if actually blocked
|
# 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}"
|
log::wg_warning "Client is already blocked: ${name}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
@ -101,33 +104,38 @@ function cmd::block::run() {
|
||||||
local client_ip
|
local client_ip
|
||||||
client_ip=$(peers::get_ip "$name") || return 1
|
client_ip=$(peers::get_ip "$name") || return 1
|
||||||
|
|
||||||
# $quiet || log::section "Blocking client: ${name} (${client_ip})"
|
|
||||||
|
|
||||||
# No specific target — block everything
|
# No specific target — block everything
|
||||||
cmd::block::_block_all "$name" "$client_ip" "$quiet"
|
# 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
|
# Block specific IPs
|
||||||
for ip in "${ips[@]}"; do
|
for ip in "${ips[@]}"; do
|
||||||
ip::require_valid "$ip"
|
ip::require_valid "$ip"
|
||||||
fw::block_ip "$client_ip" "$ip"
|
fw::block_ip "$client_ip" "$ip"
|
||||||
fw::save_block "$name" "$client_ip" "$ip"
|
block::add_rule "$name" "$client_ip" "ip" "" "$ip"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Block specific subnets
|
# Block specific subnets
|
||||||
for subnet in "${subnets[@]}"; do
|
for subnet in "${subnets[@]}"; do
|
||||||
ip::require_valid "$subnet"
|
ip::require_valid "$subnet"
|
||||||
fw::block_subnet "$client_ip" "$subnet"
|
fw::block_subnet "$client_ip" "$subnet"
|
||||||
fw::save_block "$name" "$client_ip" "$subnet"
|
block::add_rule "$name" "$client_ip" "subnet" "" "$target_ip"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Block specific ports
|
# Block specific ports
|
||||||
for entry in "${ports[@]}"; do
|
for entry in "${ports[@]}"; do
|
||||||
local target port proto
|
local target port proto
|
||||||
IFS=":" read -r target port proto <<< "$entry"
|
IFS=":" read -r target port proto <<< "$entry"
|
||||||
proto="${proto:-tcp}"
|
|
||||||
ip::require_valid "$target"
|
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
|
done
|
||||||
|
|
||||||
log::debug "Block rules applied for: ${name}"
|
log::debug "Block rules applied for: ${name}"
|
||||||
|
|
@ -144,27 +152,15 @@ function cmd::block::_get_endpoint() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::block::_block_all() {
|
function cmd::block::_block_all() {
|
||||||
local name="${1:-}"
|
local name="${1:?name required}"
|
||||||
local client_ip="${2:-}"
|
local client_ip="${2:?client_ip required}"
|
||||||
local quiet="${3:-false}"
|
local quiet="${3:-false}"
|
||||||
|
|
||||||
[[ -z "$name" ]] && log::error "name required" && return 1
|
|
||||||
[[ -z "$client_ip" ]] && log::error "client_ip required" && return 1
|
|
||||||
|
|
||||||
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"
|
# Apply fw rules and remove from server
|
||||||
fw::save_block "$name" "$client_ip"
|
block::apply_full "$name" "$client_ip"
|
||||||
|
|
||||||
if [[ -n "$endpoint" ]]; then
|
# Mark as directly blocked
|
||||||
monitor::unwatch "$client_ip"
|
block::set_direct "$name" "$client_ip" "true"
|
||||||
monitor::watch "$endpoint" "$name"
|
|
||||||
fi
|
|
||||||
|
|
||||||
peers::remove_from_server "$name"
|
|
||||||
peers::reload
|
|
||||||
|
|
||||||
$quiet || log::wg_success "${name} has been blocked."
|
$quiet || log::wg_success "${name} has been blocked."
|
||||||
}
|
}
|
||||||
|
|
@ -21,26 +21,27 @@ function cmd::fw::help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: wgctl fw [subcommand] [options]
|
Usage: wgctl fw [subcommand] [options]
|
||||||
|
|
||||||
Inspect and manage firewall rules.
|
Inspect iptables firewall rules applied by wgctl.
|
||||||
|
|
||||||
Subcommands:
|
Subcommands:
|
||||||
list Show FORWARD chain rules (default)
|
list (default) Show FORWARD chain rules
|
||||||
nat Show NAT/PREROUTING rules
|
nat Show NAT/PREROUTING rules (DNS redirects)
|
||||||
flush-nat Flush NAT rules for a subnet
|
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:
|
Options for list:
|
||||||
--peer <name> Filter by peer name
|
--peer <name> Filter rules for a specific peer
|
||||||
--rule <rule> Filter by rule name (shows all peers with that rule)
|
--rule <rule> Filter by rule — shows all peers with that rule
|
||||||
--no-nflog Hide NFLOG rules
|
--no-nflog Hide NFLOG logging rules
|
||||||
--no-accept Hide ACCEPT rules
|
--no-accept Hide ACCEPT rules
|
||||||
--no-drop Hide DROP rules
|
--no-drop Hide DROP rules
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
wgctl fw list
|
wgctl fw
|
||||||
wgctl fw list --peer phone-nuno
|
wgctl fw list --peer phone-nuno
|
||||||
wgctl fw list --rule guest
|
wgctl fw list --rule guest
|
||||||
wgctl fw list --no-nflog
|
wgctl fw list --no-nflog
|
||||||
|
wgctl fw list --peer phone-nuno --no-nflog
|
||||||
wgctl fw nat
|
wgctl fw nat
|
||||||
wgctl fw count
|
wgctl fw count
|
||||||
wgctl fw flush-nat --subnet 10.1.103.0/24
|
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}"
|
log::section "Firewall Rules (FORWARD) — rule: ${rule}"
|
||||||
printf "\n"
|
printf "\n"
|
||||||
local found=false
|
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}"
|
$found || log::wg_warning "No peers found with rule: ${rule}"
|
||||||
|
fw::list_peer_rules "$ip" "$show_nflog"
|
||||||
printf "\n"
|
printf "\n"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -22,39 +22,47 @@ function cmd::group::help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: wgctl group <subcommand> [options]
|
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:
|
Subcommands:
|
||||||
list, ls List all groups
|
list, ls List all groups with status
|
||||||
show Show group details and members
|
show Show group members and their status
|
||||||
add, new, create Create a new group
|
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
|
rename Rename a group
|
||||||
peer add Add a peer to a group
|
peer add Add a peer to a group
|
||||||
peer remove, peer rm Remove a peer from 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
|
block Block all peers in group
|
||||||
unblock Unblock all peers in group
|
unblock Unblock all peers in group
|
||||||
rule assign Assign a rule to all peers in group
|
rule assign Assign a rule to all peers in group
|
||||||
audit Audit all peers in group
|
audit Audit firewall rules for all peers in group
|
||||||
logs Show logs for all peers in group
|
logs Show activity logs for all peers in group
|
||||||
watch Live monitor for all peers in group
|
watch Live monitor for all peers in group
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--name <name> Group name
|
--name <name> Group name
|
||||||
--desc <description> Group description
|
--desc <description> Group description (for add)
|
||||||
--peer <peer> Peer name
|
--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)
|
--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
|
--force Skip confirmation prompts
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
wgctl group list
|
||||||
wgctl group add --name family --desc "Family devices"
|
wgctl group add --name family --desc "Family devices"
|
||||||
wgctl group peer add --name family --peer phone-nuno
|
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 block --name family
|
||||||
|
wgctl group unblock --name family
|
||||||
wgctl group rule assign --name family --rule user
|
wgctl group rule assign --name family --rule user
|
||||||
wgctl group audit --name family
|
wgctl group audit --name family
|
||||||
|
wgctl group logs --name family --limit 20
|
||||||
|
wgctl group watch --name family
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -585,37 +593,28 @@ function cmd::group::block() {
|
||||||
return 0
|
return 0
|
||||||
fi
|
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 "${filtered[@]}"; do
|
||||||
for peer_name in "${peers_list[@]}"; do
|
if cmd::group::_block_peer "$peer_name" "$name"; then
|
||||||
[[ -z "$peer_name" ]] && continue
|
(( count++ )) || true
|
||||||
# Skip if peer no longer exists
|
else
|
||||||
if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
|
(( skipped++ )) || true
|
||||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
|
|
||||||
continue
|
|
||||||
fi
|
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")
|
|
||||||
(( count++ )) || true
|
|
||||||
done
|
done
|
||||||
|
|
||||||
[[ "$count" -eq 0 ]] && return 0
|
if [[ "$count" -gt 0 ]]; then
|
||||||
|
log::wg_block "All peers from ${name} have been blocked (${count} peers)."
|
||||||
|
fi
|
||||||
|
|
||||||
# if [[ "$count" -gt 0 ]]; then
|
if [[ "$skipped" -gt 0 ]]; then
|
||||||
# printf "\n"
|
log::wg_warning "${skipped} peers already blocked"
|
||||||
# for n in "${blocked_names[@]}"; do
|
fi
|
||||||
# log::wg " Blocked: ${n}"
|
|
||||||
# done
|
|
||||||
# fi
|
|
||||||
|
|
||||||
# log::wg_block "Blocked ${count} peers in group '${name}'"
|
|
||||||
log::wg_block "All peers from ${name} have been blocked (${count} peers)."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::group::unblock() {
|
function cmd::group::unblock() {
|
||||||
|
|
@ -635,38 +634,114 @@ function cmd::group::unblock() {
|
||||||
|
|
||||||
local peers_list=()
|
local peers_list=()
|
||||||
mapfile -t peers_list < <(group::peers "$name")
|
mapfile -t peers_list < <(group::peers "$name")
|
||||||
[[ -z "${peers_list[0]}" ]] && log::wg_warning "Group '${name}' has no peers" && return 0
|
|
||||||
|
|
||||||
# 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=()
|
local count=0 skipped=0
|
||||||
for peer_name in "${peers_list[@]}"; do
|
|
||||||
[[ -z "$peer_name" ]] && continue
|
for peer_name in "${filtered[@]}"; do
|
||||||
# Skip if peer no longer exists
|
if cmd::group::_unblock_peer "$peer_name" "$name"; then
|
||||||
if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
|
(( count++ )) || true
|
||||||
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
|
else
|
||||||
continue
|
(( skipped++ )) || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
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")
|
|
||||||
(( count++ )) || true
|
|
||||||
done
|
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
|
if [[ "$skipped" -gt 0 ]]; then
|
||||||
# printf "\n"
|
log::wg_warning "${skipped} peer(s) remain blocked (blocked directly or by other groups)"
|
||||||
# for n in "${blocked_names[@]}"; do
|
fi
|
||||||
# log::wg " Unblocked: ${n}"
|
}
|
||||||
# done
|
|
||||||
# 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() {
|
function cmd::inspect::help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: wgctl inspect --name <name> [--type <type>]
|
Usage: wgctl inspect --name <name> [options]
|
||||||
wgctl inspect <full-name>
|
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:
|
Options:
|
||||||
--name <name> Client name
|
--name <name> Client name (e.g. phone-nuno)
|
||||||
--type <type> Device type (optional, combines with --name)
|
--type <type> Device type — combines with --name (e.g. --name nuno --type phone)
|
||||||
--config Show raw client config
|
--config Also show raw WireGuard client config
|
||||||
--qr Show QR code
|
--qr Also show QR code
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
wgctl inspect --name phone-nuno
|
wgctl inspect --name phone-nuno
|
||||||
wgctl inspect --name nuno --type phone
|
wgctl inspect --name nuno --type phone
|
||||||
wgctl inspect --name phone-nuno --config
|
wgctl inspect --name phone-nuno --config
|
||||||
wgctl inspect --name phone-nuno --qr
|
wgctl inspect --name phone-nuno --qr
|
||||||
|
wgctl inspect guest-zephyr
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,61 +72,205 @@ function cmd::inspect::_peer_info() {
|
||||||
local subtype
|
local subtype
|
||||||
subtype=$(peers::get_meta "$name" "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"
|
cmd::inspect::_section "Client"
|
||||||
ui::row "Name" "$name"
|
ui::row "Name" "$name"
|
||||||
ui::row "IP" "$ip"
|
ui::row "IP" "$ip"
|
||||||
ui::row "Type" "$(peers::display_type "$type" "$subtype")"
|
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 "Status" "$(echo -e "$status")"
|
||||||
ui::row "Endpoint" "${endpoint:-—}"
|
ui::row "Endpoint" "${endpoint:-—}"
|
||||||
ui::row "Last seen" "$last_seen"
|
ui::row "Last seen" "$last_seen"
|
||||||
ui::row "AllowedIPs" "$allowed_ips"
|
ui::row "AllowedIPs" "$allowed_ips"
|
||||||
ui::row "Public key" "${public_key:-—}"
|
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() {
|
function cmd::inspect::_rule_info() {
|
||||||
local name="$1"
|
local name="${1:-}"
|
||||||
local rule
|
local rule
|
||||||
rule=$(peers::get_meta "$name" "rule")
|
rule=$(peers::get_meta "$name" "rule")
|
||||||
[[ -z "$rule" ]] && return 0
|
[[ -z "$rule" ]] && return 0
|
||||||
|
|
||||||
rule::exists "$rule" || return 0
|
rule::exists "$rule" || return 0
|
||||||
|
|
||||||
|
cmd::inspect::_section "Rule: ${rule}"
|
||||||
|
|
||||||
local rule_file
|
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
|
if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]:-}" ]]; then
|
||||||
desc=$(json::get "$rule_file" "desc")
|
# Show inheritance tree
|
||||||
dns_redirect=$(json::get "$rule_file" "dns_redirect")
|
for base_name in "${extends_raw[@]}"; do
|
||||||
ui::row "Description" "${desc:-—}"
|
[[ -z "$base_name" ]] && continue
|
||||||
ui::row "DNS Redirect" "${dns_redirect:-false}"
|
printf "\n \033[0;37m↳ %s\033[0m\n" "$base_name"
|
||||||
|
|
||||||
local allow_ports allow_ips block_ips block_ports
|
local base_allows base_blocks
|
||||||
allow_ports=$(json::get "$rule_file" "allow_ports")
|
base_allows=$(rule::get "$base_name" "allow_ports")$'\n'$(rule::get "$base_name" "allow_ips")
|
||||||
allow_ips=$(json::get "$rule_file" "allow_ips")
|
base_blocks=$(rule::get "$base_name" "block_ips")$'\n'$(rule::get "$base_name" "block_ports")
|
||||||
block_ips=$(json::get "$rule_file" "block_ips")
|
|
||||||
block_ports=$(json::get "$rule_file" "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
|
||||||
|
|
||||||
if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then
|
|
||||||
printf " %-20s\n" "Allows:"
|
|
||||||
ui::print_list "+" "$allow_ports"
|
|
||||||
ui::print_list "+" "$allow_ips"
|
|
||||||
else
|
else
|
||||||
ui::row "Allows" "—"
|
# No inheritance — flat view
|
||||||
|
local allow_ports allow_ips block_ips 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 "\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 "\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
|
fi
|
||||||
|
|
||||||
if [[ -n "$block_ips" || -n "$block_ports" ]]; then
|
return 0
|
||||||
printf " %-20s\n" "Blocks:"
|
|
||||||
ui::print_list "-" "$block_ips"
|
|
||||||
ui::print_list "-" "$block_ports"
|
|
||||||
else
|
|
||||||
ui::row "Blocks" "—"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
function cmd::inspect::_group_info() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
|
|
||||||
|
|
@ -144,30 +290,40 @@ function cmd::inspect::_group_info() {
|
||||||
count=$(json::count "$(group::path "$g")" "peers")
|
count=$(json::count "$(group::path "$g")" "peers")
|
||||||
printf " %-20s %s peers\n" "$g" "$count"
|
printf " %-20s %s peers\n" "$g" "$count"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::inspect::_firewall_info() {
|
function cmd::inspect::_firewall_info() {
|
||||||
local name="$1" show_nflog="${2:-false}"
|
local name="${1:-}"
|
||||||
local ip
|
local ip
|
||||||
ip=$(peers::get_ip "$name")
|
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
|
# printf "\n \033[0;37m── Firewall (\033[0;32m+%d\033[0m \033[0;31m-%d\033[0m) \033[0m%s\n" \
|
||||||
while IFS=":" read -r pname pcount; do
|
# "$accepts" "$drops" "$(printf '─%.0s' {1..28})"
|
||||||
[[ "$pname" == "$name" ]] && count="$pcount" && break
|
|
||||||
done < <(json::audit_fw_counts "$(ctx::clients)")
|
|
||||||
|
|
||||||
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
|
if [[ ${#rules_output[@]} -gt 0 ]]; then
|
||||||
printf "\n"
|
for line in "${rules_output[@]}"; do
|
||||||
iptables -L FORWARD -n </dev/null | grep -F "$ip" | while IFS= read -r rule; do
|
fw::format_rule "$line"
|
||||||
[[ -z "$rule" ]] && continue
|
|
||||||
echo "$rule" | grep -q "NFLOG" && continue # skip NFLOG
|
|
||||||
ui::firewall_rule "$rule"
|
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::inspect::_config() {
|
function cmd::inspect::_config() {
|
||||||
|
|
@ -176,6 +332,8 @@ function cmd::inspect::_config() {
|
||||||
printf "\n"
|
printf "\n"
|
||||||
cat "$(ctx::clients)/${name}.conf"
|
cat "$(ctx::clients)/${name}.conf"
|
||||||
printf "\n"
|
printf "\n"
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -217,10 +375,11 @@ function cmd::inspect::run() {
|
||||||
|
|
||||||
log::section "Inspect: ${name}"
|
log::section "Inspect: ${name}"
|
||||||
|
|
||||||
cmd::inspect::_peer_info "$name"
|
cmd::inspect::_peer_info "$name"
|
||||||
cmd::inspect::_rule_info "$name"
|
cmd::inspect::_rule_info "$name"
|
||||||
cmd::inspect::_group_info "$name"
|
cmd::inspect::_group_info "$name"
|
||||||
cmd::inspect::_firewall_info "$name"
|
cmd::inspect::_firewall_info "$name"
|
||||||
|
cmd::inspect::_blocks_info "$name"
|
||||||
|
|
||||||
if $show_config; then
|
if $show_config; then
|
||||||
cmd::inspect::_config "$name"
|
cmd::inspect::_config "$name"
|
||||||
|
|
|
||||||
|
|
@ -157,19 +157,14 @@ function cmd::list::show_client() {
|
||||||
last_seen=$(peers::format_last_seen "$name" "$public_key" \
|
last_seen=$(peers::format_last_seen "$name" "$public_key" \
|
||||||
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
||||||
|
|
||||||
local block_file
|
|
||||||
block_file="$(ctx::block::path "${name}.block")"
|
|
||||||
local blocks=""
|
local blocks=""
|
||||||
if [[ -f "$block_file" ]] && [[ -s "$block_file" ]]; then
|
if block::has_file "$name" && block::is_blocked "$name"; then
|
||||||
while IFS=" " read -r client_ip target port proto; do
|
if [[ "$(block::is_blocked_direct "$name")" == "true" ]]; then
|
||||||
if [[ -z "$target" ]]; then
|
blocks="all traffic blocked"
|
||||||
blocks+=" all traffic blocked\n"
|
fi
|
||||||
else
|
local rule_lines
|
||||||
local rule=" ${target}"
|
rule_lines=$(block::format_rules "$name")
|
||||||
[[ -n "$port" ]] && rule+=":${port}/${proto}"
|
[[ -n "$rule_lines" ]] && blocks+="$rule_lines"
|
||||||
blocks+="${rule}\n"
|
|
||||||
fi
|
|
||||||
done < "$block_file"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ui::section "Client: ${name}"
|
ui::section "Client: ${name}"
|
||||||
|
|
@ -389,8 +384,11 @@ function cmd::list::_precompute_all() {
|
||||||
local wg_peers
|
local wg_peers
|
||||||
wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null)
|
wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null)
|
||||||
while IFS= read -r name; do
|
while IFS= read -r name; do
|
||||||
[[ -f "$(ctx::block::path "${name}.block")" ]] \
|
if block::is_blocked "$name" 2>/dev/null; then
|
||||||
&& p_restricted["$name"]=true || p_restricted["$name"]=false
|
p_restricted["$name"]=true
|
||||||
|
else
|
||||||
|
p_restricted["$name"]=false
|
||||||
|
fi
|
||||||
local pubkey
|
local pubkey
|
||||||
pubkey=$(keys::public "$name" 2>/dev/null || echo "")
|
pubkey=$(keys::public "$name" 2>/dev/null || echo "")
|
||||||
if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then
|
if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then
|
||||||
|
|
@ -434,6 +432,32 @@ function cmd::list::_precompute_all() {
|
||||||
done < <(json::peer_transfer "$(config::interface)")
|
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() {
|
function cmd::list::_build_filter_desc() {
|
||||||
filter_desc=""
|
filter_desc=""
|
||||||
[[ -n "$filter_type" ]] && filter_desc+="type=${filter_type} "
|
[[ -n "$filter_type" ]] && filter_desc+="type=${filter_type} "
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ function cmd::logs::on_load() {
|
||||||
flag::register --all
|
flag::register --all
|
||||||
flag::register --before
|
flag::register --before
|
||||||
flag::register --force
|
flag::register --force
|
||||||
|
flag::register --days
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::logs::help() {
|
function cmd::logs::help() {
|
||||||
|
|
@ -25,6 +26,7 @@ Show or manage WireGuard and firewall activity logs.
|
||||||
Subcommands:
|
Subcommands:
|
||||||
show (default) Show activity logs
|
show (default) Show activity logs
|
||||||
remove, rm Remove log entries
|
remove, rm Remove log entries
|
||||||
|
rotate Remove entries older than N days
|
||||||
|
|
||||||
Options for show:
|
Options for show:
|
||||||
--name <name> Filter by client name
|
--name <name> Filter by client name
|
||||||
|
|
@ -42,6 +44,10 @@ Options for remove:
|
||||||
--before <days> Remove entries older than N days
|
--before <days> Remove entries older than N days
|
||||||
--force Skip confirmation
|
--force Skip confirmation
|
||||||
|
|
||||||
|
Options for rotate:
|
||||||
|
--days <n> Days to keep (default: 7)
|
||||||
|
--force Skip confirmation
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
wgctl logs
|
wgctl logs
|
||||||
wgctl logs --name phone-nuno
|
wgctl logs --name phone-nuno
|
||||||
|
|
@ -49,8 +55,9 @@ Examples:
|
||||||
wgctl logs --follow
|
wgctl logs --follow
|
||||||
wgctl logs remove --name phone-nuno
|
wgctl logs remove --name phone-nuno
|
||||||
wgctl logs remove --all --force
|
wgctl logs remove --all --force
|
||||||
wgctl logs remove --before 7
|
|
||||||
wgctl logs remove --fw --before 1
|
wgctl logs remove --fw --before 1
|
||||||
|
wgctl logs rotate
|
||||||
|
wgctl logs rotate --days 30 --force
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,6 +72,7 @@ function cmd::logs::run() {
|
||||||
case "$subcmd" in
|
case "$subcmd" in
|
||||||
show) cmd::logs::show "$@" ;;
|
show) cmd::logs::show "$@" ;;
|
||||||
remove|rm|del) cmd::logs::remove "$@" ;;
|
remove|rm|del) cmd::logs::remove "$@" ;;
|
||||||
|
rotate) cmd::logs::rotate "$@" ;;
|
||||||
help) cmd::logs::help ;;
|
help) cmd::logs::help ;;
|
||||||
*)
|
*)
|
||||||
log::error "Unknown subcommand: '${subcmd}'"
|
log::error "Unknown subcommand: '${subcmd}'"
|
||||||
|
|
@ -221,7 +229,7 @@ function cmd::logs::remove() {
|
||||||
log::wg_warning "No log entries found matching the criteria"
|
log::wg_warning "No log entries found matching the criteria"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log::wg_success "Removed ${total} log entries (wg: ${removed_wg}, fw: ${removed_fw})"
|
log::wg_success "Removed ${total} log entries (wg: ${removed_wg}, fw: ${removed_fw})"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -277,4 +285,43 @@ function cmd::logs::show_fw_events() {
|
||||||
|
|
||||||
$found || printf " —\n"
|
$found || printf " —\n"
|
||||||
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
|
group::remove_peer_from_all "$name" || return 1
|
||||||
|
|
||||||
[[ -n "$client_ip" ]] && $was_blocked && fw::unblock_all "$client_ip"
|
[[ -n "$client_ip" ]] && $was_blocked && fw::unblock_all "$client_ip"
|
||||||
fw::remove_block_file "$name" 2>/dev/null || true
|
block::remove_file "$name" 2>/dev/null || true
|
||||||
peers::remove_meta "$name" 2>/dev/null || true
|
peers::remove_meta "$name" 2>/dev/null || true
|
||||||
peers::reload || return 1
|
peers::reload || return 1
|
||||||
}
|
}
|
||||||
|
|
@ -106,9 +106,7 @@ function cmd::rename::_rename_files() {
|
||||||
|
|
||||||
sed -i "s/^# ${name}$/# ${new_name}/" "$(config::config_file)"
|
sed -i "s/^# ${name}$/# ${new_name}/" "$(config::config_file)"
|
||||||
|
|
||||||
local block_file
|
block::rename "$name" "$new_name"
|
||||||
block_file="$(ctx::block::path "${name}.block")"
|
|
||||||
[[ -f "$block_file" ]] && mv "$block_file" "$(ctx::block::path "${new_name}.block")"
|
|
||||||
|
|
||||||
local old_meta new_meta
|
local old_meta new_meta
|
||||||
old_meta=$(peers::meta_path "$name")
|
old_meta=$(peers::meta_path "$name")
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ function cmd::rule::on_load() {
|
||||||
flag::register --resolved
|
flag::register --resolved
|
||||||
flag::register --force
|
flag::register --force
|
||||||
flag::register --type
|
flag::register --type
|
||||||
|
flag::register --all
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -38,67 +39,71 @@ function cmd::rule::help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: wgctl rule <subcommand> [options]
|
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:
|
Subcommands:
|
||||||
list, ls List all rules
|
list, ls List all rules
|
||||||
show Show rule details
|
show, inspect Show rule details and inheritance
|
||||||
inspect Show full inheritance tree
|
|
||||||
add, new, create Create a new rule
|
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
|
remove, rm, del Remove a rule
|
||||||
assign Assign a rule to a peer
|
assign Assign a rule to a peer
|
||||||
unassign Remove rule from a peer
|
unassign Remove rule from a peer
|
||||||
migrate Apply default rules to all unassigned peers
|
reapply Re-apply rule to all assigned peers
|
||||||
reapply Re-apply a rule to all assigned peers
|
migrate Apply default rules to unassigned peers
|
||||||
|
|
||||||
Options for list:
|
Options for list:
|
||||||
--base Show base rules section
|
--base Show only base rules
|
||||||
--no-base Hide base rules section (default shows them)
|
--no-base Hide base rules section
|
||||||
--group <name> Filter by group name
|
--group <name> Filter by group (case insensitive)
|
||||||
--tree Show full inheritance tree inline
|
--tree Show full inheritance tree inline
|
||||||
|
|
||||||
Options for add/update:
|
Options for add:
|
||||||
--name <name> Rule name
|
--name <name> Rule name
|
||||||
--desc <description> Human readable description
|
--desc <description> Description
|
||||||
--group <group> Display group (e.g. vm-rules, user-rules)
|
--group <group> Display group (e.g. VM Rules, Users)
|
||||||
--extends <rule,...> Inherit from base rules (add only)
|
--extends <rule,...> Inherit from base rules (comma-separated)
|
||||||
--add-extends <rule,...> Add base rules (update only)
|
--base Create as base rule (not directly assignable)
|
||||||
--remove-extends <rule,...> Remove base rules (update only)
|
|
||||||
--allow-ip <ip/cidr> Allow IP or subnet (repeatable)
|
--allow-ip <ip/cidr> Allow IP or subnet (repeatable)
|
||||||
--allow-port <ip:port:proto> Allow specific port (repeatable)
|
--allow-port <ip:port:proto> Allow specific port (repeatable)
|
||||||
--block-ip <ip/cidr> Block IP or subnet (repeatable)
|
--block-ip <ip/cidr> Block IP or subnet (repeatable)
|
||||||
--block-port <ip:port:proto> Block specific port (repeatable)
|
--block-port <ip:port:proto> Block specific port (repeatable)
|
||||||
--dns-redirect Force DNS through Pi-hole
|
--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-ip <ip> Remove allow IP entry
|
||||||
--remove-allow-port <entry> Remove allow port entry
|
--remove-allow-port <entry> Remove allow port entry
|
||||||
--remove-block-ip <ip> Remove block IP entry
|
--remove-block-ip <ip> Remove block IP entry
|
||||||
--remove-block-port <entry> Remove block port entry
|
--remove-block-port <entry> Remove block port entry
|
||||||
|
|
||||||
Options for inspect:
|
Options for show/inspect:
|
||||||
--name <name> Rule name
|
--name <name> Rule name
|
||||||
--peers Show assigned peers
|
--resolved Show resolved/merged entries
|
||||||
--resolved Show resolved/merged rule entries
|
--no-peers Hide assigned peers
|
||||||
|
|
||||||
Options for assign/unassign:
|
Options for reapply:
|
||||||
--name <rule> Rule name
|
--name <name> Rule name
|
||||||
--peer <peer> Peer name
|
--all Reapply all rules
|
||||||
--type <type> Peer device type (optional)
|
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
wgctl rule list
|
wgctl rule list
|
||||||
wgctl rule list --base
|
|
||||||
wgctl rule list --group vm-rules
|
|
||||||
wgctl rule list --tree
|
wgctl rule list --tree
|
||||||
|
wgctl rule list --group "VM Rules"
|
||||||
|
wgctl rule list --base
|
||||||
wgctl rule show --name guest
|
wgctl rule show --name guest
|
||||||
wgctl rule inspect --name moonlight-02
|
wgctl rule show --name moonlight-02 --resolved
|
||||||
wgctl rule inspect --name moonlight-02 --resolved --peers
|
wgctl rule add --name dev-01 --desc "Dev access" --group "Dev" --extends no-lan
|
||||||
wgctl rule add --name dev-01 --desc "Dev VM" --group vm-rules --extends no-lan
|
wgctl rule add --name no-npm --base --block-ip 10.0.0.101/32
|
||||||
wgctl rule update --name dev-01 --add-extends no-nginx
|
wgctl rule update --name user --add-extends no-nginx
|
||||||
wgctl rule update --name dev-01 --remove-extends no-nginx
|
wgctl rule update --name dev-01 --allow-ip 10.0.0.50/32
|
||||||
wgctl rule update --name dev-01 --group infra-rules
|
|
||||||
wgctl rule assign --name dev-01 --peer laptop-nuno
|
wgctl rule assign --name dev-01 --peer laptop-nuno
|
||||||
wgctl rule unassign --peer laptop-nuno
|
wgctl rule unassign --peer laptop-nuno
|
||||||
|
wgctl rule reapply --name user
|
||||||
|
wgctl rule reapply --all
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -507,81 +512,6 @@ function cmd::rule::show() {
|
||||||
printf "\n"
|
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
|
# Add
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -868,14 +798,33 @@ function cmd::rule::migrate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::rule::reapply() {
|
function cmd::rule::reapply() {
|
||||||
local name=""
|
local name="" all=false
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--name) name="$2"; shift 2 ;;
|
--name) name="$2"; shift 2 ;;
|
||||||
|
--all) all=true; shift ;;
|
||||||
*) log::error "Unknown flag: $1"; return 1 ;;
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
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::require_exists "$name" || return 1
|
||||||
rule::reapply_all "$name"
|
rule::reapply_all "$name"
|
||||||
log::wg_success "Rule '${name}' reapplied"
|
log::wg_success "Rule '${name}' reapplied"
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,12 @@ function cmd::service::run() {
|
||||||
function cmd::service::start() {
|
function cmd::service::start() {
|
||||||
log::wg_start "Starting WireGuard..."
|
log::wg_start "Starting WireGuard..."
|
||||||
systemctl start "wg-quick@$(config::interface)"
|
systemctl start "wg-quick@$(config::interface)"
|
||||||
|
|
||||||
|
block::restore_all
|
||||||
|
rule::restore_all
|
||||||
|
|
||||||
|
cmd::service::_auto_rotate_logs
|
||||||
|
|
||||||
log::wg_success "WireGuard started"
|
log::wg_success "WireGuard started"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,18 +78,26 @@ function cmd::service::restart() {
|
||||||
log::wg_start "Restarting WireGuard..."
|
log::wg_start "Restarting WireGuard..."
|
||||||
|
|
||||||
# Flush firewall rules before restart so restore starts clean
|
# Flush firewall rules before restart so restore starts clean
|
||||||
iptables -F FORWARD
|
fw::flush_all
|
||||||
iptables -t nat -F PREROUTING
|
|
||||||
|
|
||||||
systemctl restart "wg-quick@$(config::interface)"
|
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"
|
log::wg_success "WireGuard restarted"
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::service::reload() {
|
function cmd::service::reload() {
|
||||||
log::wg_start "Reloading WireGuard config..."
|
log::wg_start "Reloading WireGuard config..."
|
||||||
|
|
||||||
peers::reload
|
peers::reload
|
||||||
fw::restore_blocks
|
block::restore_all
|
||||||
|
rule::restore_all
|
||||||
|
|
||||||
|
log::wg_success "WireGuard config reloaded"
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::service::status() {
|
function cmd::service::status() {
|
||||||
|
|
@ -102,6 +116,18 @@ function cmd::service::logs() {
|
||||||
journalctl -u "wg-quick@$(config::interface)" -f --no-pager
|
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() {
|
function cmd::service::enable() {
|
||||||
systemctl enable "wg-quick@$(config::interface)"
|
systemctl enable "wg-quick@$(config::interface)"
|
||||||
log::wg_success "WireGuard enabled on boot"
|
log::wg_success "WireGuard enabled on boot"
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,11 @@ function cmd::shell::_prompt() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::shell::_is_wgctl_command() {
|
function cmd::shell::_is_wgctl_command() {
|
||||||
local cmd="$1"
|
local cmd="${1:-}"
|
||||||
# Check against known wgctl commands
|
|
||||||
local known=(
|
local known=(
|
||||||
list add remove rm inspect block unblock
|
list add remove rm inspect block unblock
|
||||||
rule group audit logs watch fw config qr
|
rule group audit logs watch fw config qr
|
||||||
rename keys ip service shell help
|
rename keys ip service shell help test
|
||||||
)
|
)
|
||||||
local c
|
local c
|
||||||
for c in "${known[@]}"; do
|
for c in "${known[@]}"; do
|
||||||
|
|
@ -29,9 +28,9 @@ function cmd::shell::_is_wgctl_command() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::shell::_handle_builtin() {
|
function cmd::shell::_handle_builtin() {
|
||||||
local input="$1"
|
local input="${1:-}"
|
||||||
local first="${input%% *}"
|
local first="${input%% *}"
|
||||||
|
|
||||||
case "$first" in
|
case "$first" in
|
||||||
cd)
|
cd)
|
||||||
local dir="${input#cd }"
|
local dir="${input#cd }"
|
||||||
|
|
@ -39,36 +38,32 @@ function cmd::shell::_handle_builtin() {
|
||||||
cd "$dir" 2>/dev/null || log::error "cd: $dir: No such file or directory"
|
cd "$dir" 2>/dev/null || log::error "cd: $dir: No such file or directory"
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
cd*) eval "$input" ;; # eval preserves shell state for cd
|
|
||||||
export|unset|source|.)
|
export|unset|source|.)
|
||||||
eval "$input"
|
eval "$input"
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
return 1 # not a builtin
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::shell::_execute() {
|
function cmd::shell::_execute() {
|
||||||
local input="$1"
|
local input="${1:-}"
|
||||||
local first="${input%% *}"
|
local first="${input%% *}"
|
||||||
local rest="${input#"$first"}"
|
local rest="${input#"$first"}"
|
||||||
rest="${rest# }"
|
rest="${rest# }"
|
||||||
|
|
||||||
# Handle shell builtins first
|
|
||||||
cmd::shell::_handle_builtin "$input" && return 0
|
cmd::shell::_handle_builtin "$input" && return 0
|
||||||
|
|
||||||
# Try as wgctl command via dispatcher
|
|
||||||
if cmd::shell::_is_wgctl_command "$first"; then
|
if cmd::shell::_is_wgctl_command "$first"; then
|
||||||
if [[ -n "$rest" ]]; then
|
if [[ -n "$rest" ]]; then
|
||||||
wgctl::dispatch "$first" $rest || true # never exit REPL on failure
|
wgctl::dispatch "$first" $rest || true
|
||||||
else
|
else
|
||||||
wgctl::dispatch "$first" || true
|
wgctl::dispatch "$first" || true
|
||||||
fi
|
fi
|
||||||
return 0 # Always 0 to keep REPL running
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Fall back to bash
|
bash -c "$input" || true
|
||||||
bash -c "$input" || true # same for bash commands
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::shell::_setup_history() {
|
function cmd::shell::_setup_history() {
|
||||||
|
|
@ -85,14 +80,30 @@ function cmd::shell::_save_history() {
|
||||||
function cmd::shell::_banner() {
|
function cmd::shell::_banner() {
|
||||||
ui::section "wgctl shell"
|
ui::section "wgctl shell"
|
||||||
printf "\n"
|
printf "\n"
|
||||||
printf " Type wgctl commands directly, or any bash command.\n"
|
printf " Type wgctl commands directly (no 'wgctl' prefix).\n"
|
||||||
printf " Type \033[1mexit\033[0m or \033[1mquit\033[0m to leave.\n"
|
printf " Bash commands work too: ls, cat, systemctl, vim...\n\n"
|
||||||
printf " Type \033[1mhelp\033[0m for wgctl commands.\n"
|
printf " \033[1;37mCommon commands:\033[0m\n"
|
||||||
printf "\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() {
|
function cmd::shell::on_load() {
|
||||||
|
|
@ -103,62 +114,65 @@ function cmd::shell::help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: wgctl shell
|
Usage: wgctl shell
|
||||||
|
|
||||||
Start an interactive wgctl shell. Supports all wgctl commands
|
Start an interactive wgctl shell.
|
||||||
directly (no 'wgctl' prefix needed), plus any bash command.
|
All wgctl commands work directly (no 'wgctl' prefix needed).
|
||||||
|
Bash commands (ls, cat, systemctl, vim, etc.) also work.
|
||||||
|
|
||||||
Builtins handled: cd, export, unset, source
|
Shell builtins handled natively: cd, export, unset, source
|
||||||
Everything else: passed to bash
|
History saved to: ~/.wgctl_history
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
wgctl shell
|
wgctl shell
|
||||||
wgctl> list
|
wgctl> list --blocked
|
||||||
wgctl> inspect --name phone-nuno
|
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
|
wgctl> exit
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Tab completion
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::shell::_setup_completion() {
|
||||||
|
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 '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() {
|
function cmd::shell::run() {
|
||||||
cmd::shell::_banner
|
cmd::shell::_banner
|
||||||
cmd::shell::_setup_history
|
cmd::shell::_setup_history
|
||||||
|
cmd::shell::_setup_completion
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
local input
|
local input
|
||||||
# Read with readline support (-e) and custom prompt
|
|
||||||
IFS= read -r -e -p "$(cmd::shell::_prompt)" input || break
|
IFS= read -r -e -p "$(cmd::shell::_prompt)" input || break
|
||||||
|
|
||||||
# Handle empty input
|
|
||||||
[[ -z "${input// }" ]] && continue
|
[[ -z "${input// }" ]] && continue
|
||||||
|
|
||||||
# Add to history
|
|
||||||
history -s "$input"
|
history -s "$input"
|
||||||
|
|
||||||
# Handle exit
|
|
||||||
case "${input%% *}" in
|
case "${input%% *}" in
|
||||||
exit|quit) break ;;
|
exit|quit) break ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Execute
|
|
||||||
cmd::shell::_execute "$input"
|
cmd::shell::_execute "$input"
|
||||||
done
|
done
|
||||||
|
|
||||||
cmd::shell::_save_history
|
cmd::shell::_save_history
|
||||||
printf "\n Goodbye!\n\n"
|
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
|
|
||||||
}
|
}
|
||||||
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"
|
test::section "Rules"
|
||||||
cmd::test::run_cmd "rule list" "guest" rule list
|
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" "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 user" "Description" rule show --name user
|
||||||
cmd::test::run_cmd "rule show --name admin" "Description" rule show --name admin
|
cmd::test::run_cmd "rule show --name admin" "Description" rule show --name admin
|
||||||
cmd::test::run_cmd_fails "rule show nonexistent" rule show --name nonexistent
|
cmd::test::run_cmd_fails "rule show nonexistent" rule show --name nonexistent
|
||||||
|
|
@ -209,45 +208,77 @@ function cmd::test::section_fw() {
|
||||||
function cmd::test::section_destructive() {
|
function cmd::test::section_destructive() {
|
||||||
test::section "Destructive (modifying state)"
|
test::section "Destructive (modifying state)"
|
||||||
|
|
||||||
# Cleanup from any previous failed run
|
# ── Cleanup from any previous failed run ──
|
||||||
"$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
|
"$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
|
||||||
"$WGCTL_BINARY" group remove --name testgroup --force > /dev/null 2>&1 || true
|
"$WGCTL_BINARY" group remove --name testgroup --force > /dev/null 2>&1 || true
|
||||||
|
"$WGCTL_BINARY" group remove --name testgroup2 --force > /dev/null 2>&1 || true
|
||||||
|
|
||||||
# Add test peer
|
# ── Add test peer ──────────────────────────
|
||||||
cmd::test::run_cmd "add phone peer" "added successfully" \
|
cmd::test::run_cmd "add phone peer" "added successfully" \
|
||||||
add --name testunit --type phone
|
add --name testunit --type phone
|
||||||
# Block/unblock
|
|
||||||
cmd::test::run_cmd "block peer" "blocked" \
|
|
||||||
block --name phone-testunit
|
|
||||||
cmd::test::run_cmd "list shows blocked" "blocked" \
|
|
||||||
list --blocked
|
|
||||||
cmd::test::run_cmd "unblock peer" "unblocked" \
|
|
||||||
unblock --name phone-testunit
|
|
||||||
|
|
||||||
# Rule assign/unassign
|
# ── Direct block/unblock ───────────────────
|
||||||
|
cmd::test::run_cmd "block peer" "blocked" \
|
||||||
|
block --name phone-testunit
|
||||||
|
cmd::test::run_cmd "list shows blocked" "blocked" \
|
||||||
|
list --blocked
|
||||||
|
cmd::test::run_cmd "unblock peer" "unblocked" \
|
||||||
|
unblock --name phone-testunit
|
||||||
|
|
||||||
|
# ── Rule assign/unassign ───────────────────
|
||||||
cmd::test::run_cmd "rule assign" "Assigned" \
|
cmd::test::run_cmd "rule assign" "Assigned" \
|
||||||
rule assign --name user --peer phone-testunit
|
rule assign --name user --peer phone-testunit
|
||||||
cmd::test::run_cmd "rule unassign" "Unassigned" \
|
cmd::test::run_cmd "rule unassign" "Unassigned" \
|
||||||
rule unassign --peer phone-testunit
|
rule unassign --peer phone-testunit
|
||||||
# Re-assign user rule (default) for cleanup
|
"$WGCTL_BINARY" rule assign --name user --peer phone-testunit \
|
||||||
/usr/local/bin/wgctl rule assign --name user --peer phone-testunit \
|
|
||||||
> /dev/null 2>&1 || true
|
> /dev/null 2>&1 || true
|
||||||
|
|
||||||
# Group operations
|
# ── Group basic operations ─────────────────
|
||||||
cmd::test::run_cmd "group add" "created" \
|
cmd::test::run_cmd "group add" "created" \
|
||||||
group add --name testgroup --desc "Test group"
|
group add --name testgroup --desc "Test group"
|
||||||
cmd::test::run_cmd "group peer add" "Added" \
|
cmd::test::run_cmd "group peer add" "Added" \
|
||||||
group peer add --name testgroup --peer phone-testunit
|
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
|
group block --name testgroup
|
||||||
cmd::test::run_cmd "group unblock" "have been unblocked" \
|
cmd::test::run_cmd "group unblock" "unblocked" \
|
||||||
group unblock --name testgroup
|
group unblock --name testgroup
|
||||||
cmd::test::run_cmd "group remove" "removed" \
|
|
||||||
group remove --name testgroup --force
|
|
||||||
|
|
||||||
# Remove test peer
|
# ── M:N group block tracking ───────────────
|
||||||
cmd::test::run_cmd "remove phone peer" "removed" \
|
# Setup: add testunit to a second group
|
||||||
remove --name phone-testunit --force
|
"$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 ───────────────────────
|
||||||
|
cmd::test::run_cmd "remove phone peer" "removed" \
|
||||||
|
remove --name phone-testunit --force
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,8 @@ function cmd::unblock::run() {
|
||||||
|
|
||||||
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
name=$(peers::resolve_and_require "$name" "$type") || return 1
|
||||||
|
|
||||||
# Check if actually blocked
|
# 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}"
|
log::wg_warning "Client is not blocked: ${name}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
@ -108,6 +108,7 @@ function cmd::unblock::run() {
|
||||||
# Unblock specific IPs
|
# Unblock specific IPs
|
||||||
for ip in "${ips[@]}"; do
|
for ip in "${ips[@]}"; do
|
||||||
fw::unblock_ip "$client_ip" "$ip"
|
fw::unblock_ip "$client_ip" "$ip"
|
||||||
|
block::remove_rule "$name" "ip" "$ip"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Unblock specific subnets
|
# Unblock specific subnets
|
||||||
|
|
@ -124,23 +125,34 @@ function cmd::unblock::run() {
|
||||||
done
|
done
|
||||||
|
|
||||||
$quiet || log::wg_success "Unblock rules applied for: ${name}"
|
$quiet || log::wg_success "Unblock rules applied for: ${name}"
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmd::unblock::_unblock_all() {
|
function cmd::unblock::_unblock_all() {
|
||||||
local name="${1:-}"
|
local name="${1:?}" client_ip="${2:?}" quiet="${3:-false}"
|
||||||
local client_ip="${2:-}"
|
|
||||||
local quiet="${3:-false}"
|
|
||||||
|
|
||||||
fw::unblock_all "$client_ip"
|
# Direct unblock overrides everything — clear all block state
|
||||||
fw::remove_block_file "$name"
|
block::set_direct "$name" "$client_ip" "false"
|
||||||
monitor::unwatch_client "$name"
|
|
||||||
|
|
||||||
if ! peers::exists_in_server "$name"; then
|
# Force full unblock regardless of group blocks
|
||||||
local public_key
|
# (direct unblock = admin override)
|
||||||
public_key=$(keys::public "$name") || return 1
|
block::restore_peer "$name" "$client_ip"
|
||||||
peers::add_to_server "$name" "$public_key" "$client_ip"
|
block::remove_file "$name"
|
||||||
peers::reload
|
|
||||||
|
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
|
fi
|
||||||
|
|
||||||
$quiet || log::wg_success "${name} has been unblocked."
|
$quiet || log::wg_success "${name} has been unblocked."
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
@ -21,16 +21,18 @@ function cmd::watch::help() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: wgctl watch [options]
|
Usage: wgctl watch [options]
|
||||||
|
|
||||||
Live monitor of WireGuard activity.
|
Live monitor of WireGuard activity. Shows:
|
||||||
Shows handshakes from active clients and connection attempts from blocked clients.
|
- Handshakes from connected peers (green)
|
||||||
|
- Connection attempts from blocked peers (red, SOURCE: wg)
|
||||||
|
- Firewall drops from rule-restricted peers (red, SOURCE: fw)
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--type <type> Filter by device type
|
|
||||||
--name <name> Filter by client name
|
--name <name> Filter by client name
|
||||||
--peers <list> Filter by comma-separated peer names (used by group watch)
|
--type <type> Filter by device type
|
||||||
--allowed Show only allowed client handshakes
|
--peers <list> Comma-separated peer names (used internally by group watch)
|
||||||
--restricted Show only restricted client events
|
--blocked Show only blocked peer attempts
|
||||||
--blocked Show only blocked client attempts
|
--allowed Show only handshakes (allowed peers)
|
||||||
|
--restricted Show only firewall drop events
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
wgctl watch
|
wgctl watch
|
||||||
|
|
@ -38,6 +40,7 @@ Examples:
|
||||||
wgctl watch --allowed
|
wgctl watch --allowed
|
||||||
wgctl watch --type phone
|
wgctl watch --type phone
|
||||||
wgctl watch --name phone-nuno
|
wgctl watch --name phone-nuno
|
||||||
|
wgctl watch --name phone-nuno --type phone
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,6 +167,9 @@ function cmd::watch::tail_events() {
|
||||||
local blocked_only="${3:-false}" restricted_only="${4:-false}"
|
local blocked_only="${3:-false}" restricted_only="${4:-false}"
|
||||||
local allowed_only="${5:-false}" filter_peers="${6:-}"
|
local allowed_only="${5:-false}" filter_peers="${6:-}"
|
||||||
|
|
||||||
|
declare -A _WATCH_LAST_ATTEMPT=()
|
||||||
|
declare -A _WATCH_LAST_FW=()
|
||||||
|
|
||||||
local peer_set=()
|
local peer_set=()
|
||||||
[[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers"
|
[[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers"
|
||||||
|
|
||||||
|
|
@ -259,6 +265,19 @@ function cmd::watch::tail_events() {
|
||||||
|
|
||||||
[[ -z "$src_ip" ]] && continue
|
[[ -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}"
|
local client="${ip_to_name[$src_ip]:-$src_ip}"
|
||||||
|
|
||||||
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
|
[[ -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/flag.sh"
|
||||||
source "${WGCTL_DIR}/core/json.sh"
|
source "${WGCTL_DIR}/core/json.sh"
|
||||||
source "${WGCTL_DIR}/core/ui.sh"
|
source "${WGCTL_DIR}/core/ui.sh"
|
||||||
|
source "${WGCTL_DIR}/core/color.sh"
|
||||||
source "${WGCTL_DIR}/core/fmt.sh"
|
source "${WGCTL_DIR}/core/fmt.sh"
|
||||||
source "${WGCTL_DIR}/core/test/test.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::rule_inspect() { python3 "$JSON_HELPER" rule_inspect "$@" </dev/null; }
|
||||||
function json::find_rule_file() { python3 "$JSON_HELPER" find_rule_file "$@" </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::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() {
|
function json::peer_transfer() {
|
||||||
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \
|
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \
|
||||||
|
|
|
||||||
|
|
@ -1132,6 +1132,186 @@ def get_raw(file, key):
|
||||||
except:
|
except:
|
||||||
pass
|
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 = {
|
commands = {
|
||||||
'get': lambda args: get(args[0], args[1]),
|
'get': lambda args: get(args[0], args[1]),
|
||||||
'set': lambda args: set_key(args[0], args[1], args[2]),
|
'set': lambda args: set_key(args[0], args[1], args[2]),
|
||||||
|
|
@ -1179,6 +1359,28 @@ commands = {
|
||||||
'rule_inspect': lambda args: rule_inspect(args[0], args[1]),
|
'rule_inspect': lambda args: rule_inspect(args[0], args[1]),
|
||||||
'find_rule_file': lambda args: print(find_rule_file(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])),
|
'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__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
{
|
{
|
||||||
"phone-fred": "94.63.0.129",
|
"phone-fred": "94.63.0.129",
|
||||||
"phone-helena": "148.69.46.73",
|
"phone-helena": "148.69.46.73",
|
||||||
"phone-nuno": "94.63.0.129",
|
"phone-nuno": "148.69.51.201",
|
||||||
"tablet-nuno": "148.69.202.5",
|
"tablet-nuno": "148.69.202.5",
|
||||||
"guest-zephyr": "5.13.82.5",
|
"guest-zephyr": "5.13.82.5",
|
||||||
"guest-zephyr-test": "94.63.0.129",
|
"guest-zephyr-test": "94.63.0.129",
|
||||||
"desktop-roboclean": "46.189.215.231"
|
"desktop-roboclean": "46.189.215.231",
|
||||||
|
"laptop-nuno": "94.63.0.129"
|
||||||
}
|
}
|
||||||
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}"
|
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() {
|
function fw::allow_ip() {
|
||||||
local client_ip="${1:-}" target_ip="${2:-}"
|
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() {
|
function fw::list_peer_rules() {
|
||||||
local name="$1"
|
local ip="${1:-}" show_nflog="${2:-false}"
|
||||||
local client_ip="$2"
|
|
||||||
local target="${3:-}"
|
|
||||||
local port="${4:-}"
|
|
||||||
local proto="${5:-}"
|
|
||||||
|
|
||||||
local block_file
|
fw::forward_rules_for_ip "$ip" | while IFS= read -r line; do
|
||||||
block_file="$(ctx::block::path "${name}.block")"
|
[[ -z "$line" ]] && continue
|
||||||
|
! $show_nflog && [[ "$line" =~ NFLOG ]] && continue
|
||||||
echo "${client_ip} ${target} ${port} ${proto}" >> "$block_file"
|
fw::format_rule "$line"
|
||||||
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}"
|
|
||||||
done
|
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
|
# Helpers
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -251,6 +254,63 @@ function fw::_nat_exists() {
|
||||||
fw::_rule_exists nat PREROUTING "$@"
|
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
|
# private
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -160,9 +160,14 @@ function peers::exists_in_server() {
|
||||||
grep -q "^# ${name}$" "$(config::config_file)"
|
grep -q "^# ${name}$" "$(config::config_file)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# function peers::is_blocked() {
|
||||||
|
# local name="${1:-}"
|
||||||
|
# peers::exists_in_server "$name" && return 1 || return 0
|
||||||
|
# }
|
||||||
|
|
||||||
function peers::is_blocked() {
|
function peers::is_blocked() {
|
||||||
local name="${1:-}"
|
local name="${1:-}"
|
||||||
peers::exists_in_server "$name" && return 1 || return 0
|
block::is_blocked "$name"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -192,7 +192,11 @@ function rule::unapply() {
|
||||||
# Remove allow_ips
|
# Remove allow_ips
|
||||||
while IFS= read -r allow_ip; do
|
while IFS= read -r allow_ip; do
|
||||||
[[ -z "$allow_ip" ]] && continue
|
[[ -z "$allow_ip" ]] && continue
|
||||||
fw::unallow_ip "$client_ip" "$allow_ip"
|
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")
|
done < <(rule::get "$rule_name" "allow_ips")
|
||||||
|
|
||||||
# Remove block_ports
|
# Remove block_ports
|
||||||
|
|
@ -207,7 +211,11 @@ function rule::unapply() {
|
||||||
# Remove block_ips
|
# Remove block_ips
|
||||||
while IFS= read -r block_ip; do
|
while IFS= read -r block_ip; do
|
||||||
[[ -z "$block_ip" ]] && continue
|
[[ -z "$block_ip" ]] && continue
|
||||||
fw::unblock_ip "$client_ip" "$block_ip"
|
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")
|
done < <(rule::get "$rule_name" "block_ips")
|
||||||
|
|
||||||
# Remove DNS redirect if applicable
|
# Remove DNS redirect if applicable
|
||||||
|
|
@ -247,7 +255,8 @@ function rule::reapply_all() {
|
||||||
local client_ip
|
local client_ip
|
||||||
client_ip=$(peers::get_ip "$peer_name")
|
client_ip=$(peers::get_ip "$peer_name")
|
||||||
[[ -z "$client_ip" ]] && continue
|
[[ -z "$client_ip" ]] && continue
|
||||||
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"
|
rule::apply "$rule_name" "$client_ip" "$peer_name"
|
||||||
(( count++ )) || true
|
(( count++ )) || true
|
||||||
done
|
done
|
||||||
|
|
@ -257,8 +266,12 @@ function rule::reapply_all() {
|
||||||
|
|
||||||
function rule::restore_all() {
|
function rule::restore_all() {
|
||||||
while IFS= read -r peer_name; do
|
while IFS= read -r peer_name; do
|
||||||
|
# Skip blocked peers - no fw rules needed when blocked
|
||||||
|
block::is_blocked "$peer_name" && continue
|
||||||
|
|
||||||
local rule_name
|
local rule_name
|
||||||
rule_name=$(peers::get_meta "$peer_name" "rule")
|
rule_name=$(peers::get_meta "$peer_name" "rule")
|
||||||
|
|
||||||
[[ -z "$rule_name" ]] && continue
|
[[ -z "$rule_name" ]] && continue
|
||||||
|
|
||||||
if ! rule::exists "$rule_name"; then
|
if ! rule::exists "$rule_name"; then
|
||||||
|
|
@ -281,27 +294,10 @@ function rule::restore_all() {
|
||||||
|
|
||||||
function rule::apply_dns_redirect() {
|
function rule::apply_dns_redirect() {
|
||||||
local client_subnet="${1:-}"
|
local client_subnet="${1:-}"
|
||||||
local dns
|
fw::nat_add_dns_redirect "$client_subnet" "$(config::dns)" "$(config::interface)"
|
||||||
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"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function rule::remove_dns_redirect() {
|
function rule::remove_dns_redirect() {
|
||||||
local client_subnet="${1:-}"
|
local client_subnet="${1:-}"
|
||||||
local dns
|
fw::nat_remove_dns_redirect "$client_subnet" "$(config::dns)" "$(config::interface)"
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
39
wgctl
39
wgctl
|
|
@ -17,6 +17,7 @@ load_module peers
|
||||||
load_module firewall
|
load_module firewall
|
||||||
load_module monitor
|
load_module monitor
|
||||||
load_module rule
|
load_module rule
|
||||||
|
load_module block
|
||||||
load_module group
|
load_module group
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -102,47 +103,53 @@ $(log::section "wgctl — WireGuard Management" 2>/dev/null || printf "\n wgctl
|
||||||
Usage: wgctl <command> [options]
|
Usage: wgctl <command> [options]
|
||||||
|
|
||||||
Client Commands:
|
Client Commands:
|
||||||
add, new Add a new client
|
add, new Add a new client (--type, --subtype, --rule, --group)
|
||||||
remove, rm Remove a client
|
remove, rm Remove a client
|
||||||
rename, mv Rename a client
|
rename, mv Rename a client
|
||||||
list, ls List all clients
|
list, ls List all clients (--type, --rule, --group, --blocked...)
|
||||||
inspect Show detailed client info
|
inspect Show detailed client info (--config, --qr)
|
||||||
config Show client config
|
config Show client config
|
||||||
qr Show QR code for a client
|
qr Show QR code for a client
|
||||||
|
|
||||||
Access Control:
|
Access Control:
|
||||||
block, ban Block a client entirely
|
block, ban Block a client entirely or restrict specific IPs/ports
|
||||||
unblock, unban Restore client access
|
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:
|
Organization:
|
||||||
group Manage peer groups (list, show, block, watch...)
|
group Manage peer groups
|
||||||
|
list, show, add, block, unblock, watch, logs...
|
||||||
|
|
||||||
Monitoring:
|
Monitoring:
|
||||||
watch Live monitor of WireGuard activity
|
watch Live monitor — handshakes, fw drops, blocked attempts
|
||||||
logs Show activity and firewall logs
|
logs Show and manage activity logs
|
||||||
audit Verify firewall rules are correctly applied
|
show, remove, rotate, --follow
|
||||||
fw Inspect firewall rules
|
audit Verify firewall rules match configuration
|
||||||
|
fw Inspect iptables firewall rules
|
||||||
|
|
||||||
Service:
|
Service:
|
||||||
service Manage WireGuard service (start/stop/restart/status)
|
service Manage WireGuard service (start/stop/restart/status)
|
||||||
restart Restart WireGuard
|
restart Restart WireGuard
|
||||||
shell Start interactive wgctl shell
|
shell Interactive wgctl shell
|
||||||
|
|
||||||
Development:
|
Development:
|
||||||
test Run the wgctl test suite
|
test Run the wgctl test suite
|
||||||
|
|
||||||
Common examples:
|
Common examples:
|
||||||
wgctl add --name nuno --type phone
|
wgctl add --name nuno --type phone --rule admin --group family
|
||||||
wgctl add --name visitor --type guest --subtype phone --group family
|
|
||||||
wgctl list --blocked
|
wgctl list --blocked
|
||||||
wgctl list --group family
|
wgctl list --rule user --group family
|
||||||
wgctl block --name phone-nuno
|
wgctl block --name phone-nuno
|
||||||
wgctl inspect --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 group block --name family
|
||||||
wgctl logs --follow
|
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.
|
Run 'wgctl <command> --help' for command-specific help.
|
||||||
EOF
|
EOF
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue