diff --git a/commands/identity.command.sh b/commands/identity.command.sh index 8cf4132..75b8685 100644 --- a/commands/identity.command.sh +++ b/commands/identity.command.sh @@ -397,7 +397,7 @@ function cmd::identity::_rule_assign() { done fi - local exit_code + local exit_code=0 identity::add_rule "$name" "$rule" || exit_code=$? if [[ $exit_code -eq 2 ]]; then diff --git a/commands/logs.command.sh b/commands/logs.command.sh index 002bd39..99b1203 100644 --- a/commands/logs.command.sh +++ b/commands/logs.command.sh @@ -187,7 +187,8 @@ function cmd::logs::show() { "$filter_ip" "$name" "$type" "$limit" \ "$collapse" "$since" "$filter_event") - if [[ -z "${fw_output// /}" && -z "${wg_output// /}" ]]; then + if [[ -z "$(echo "$fw_output" | tr -d '[:space:]')" && \ + -z "$(echo "$wg_output" | tr -d '[:space:]')" ]]; then log::wg_warning "No logs found" return 0 fi diff --git a/commands/rule.command.sh b/commands/rule.command.sh index 18d34bd..1f9cfc9 100644 --- a/commands/rule.command.sh +++ b/commands/rule.command.sh @@ -554,7 +554,7 @@ function cmd::rule::assign() { # Identity rule check local peer_identity - peer_identity=$(peers::get_meta "$peer" "identity") + peer_identity=$(peers::get_identity "$peer") if [[ -n "$peer_identity" ]]; then local identity_rules identity_rules=$(identity::rules "$peer_identity" 2>/dev/null) diff --git a/commands/test/destructive.sh b/commands/test/destructive.sh index df05fb7..772268e 100644 --- a/commands/test/destructive.sh +++ b/commands/test/destructive.sh @@ -22,6 +22,8 @@ function cmd::test::section_destructive() { cmd::test::_destructive_groups cmd::test::_destructive_identity cmd::test::_destructive_cleanup + cmd::test::_destructive_rule_duplicate + cmd::test::_destructive_peer_dns } function cmd::test::_destructive_peer() { @@ -121,6 +123,67 @@ function cmd::test::_destructive_identity() { identity show --name testunit2b } +function cmd::test::_destructive_rule_duplicate() { + # Cleanup from any previous failed run + "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true + "$WGCTL_BINARY" identity rule unassign --name testunit --all > /dev/null 2>&1 || true + "$WGCTL_BINARY" add --name testunit --type phone > /dev/null 2>&1 + + # Assign admin to identity + "$WGCTL_BINARY" identity rule assign --name testunit --rule admin > /dev/null 2>&1 || true + + # Try to assign admin directly — should fail (identity has it) + cmd::test::run_cmd_fails "rule assign blocked by identity rule" \ + rule assign --name admin --peer phone-testunit + + # Remove admin from identity first so we can assign user directly to peer + "$WGCTL_BINARY" identity rule unassign --name testunit --rule admin > /dev/null 2>&1 || true + + # Assign user directly to peer + "$WGCTL_BINARY" rule assign --name user --peer phone-testunit > /dev/null 2>&1 || true + + # Now assign user to identity with --migrate — should remove from peer + cmd::test::run_cmd "identity rule assign --migrate" "Migrated" \ + identity rule assign --name testunit --rule user --migrate + + # Cleanup + "$WGCTL_BINARY" identity rule unassign --name testunit --all > /dev/null 2>&1 || true + "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true +} + +function cmd::test::_destructive_peer_dns() { + test::section "Destructive: peer update-dns" + + "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true + "$WGCTL_BINARY" add --name testunit --type phone > /dev/null 2>&1 + + # Update DNS and verify it's in the conf + cmd::test::run_cmd "peer update-dns single peer" "Updated DNS" \ + peer update-dns --name phone-testunit --fallback-dns "9.9.9.9" + + local conf_dns + conf_dns=$(grep "^DNS" /etc/wireguard/clients/phone-testunit.conf 2>/dev/null) + [[ "$conf_dns" == *"9.9.9.9"* ]] && \ + test::pass "DNS line contains fallback" || \ + test::fail "DNS line missing fallback (got: $conf_dns)" + + # Update tunnel mode + cmd::test::run_cmd "peer update-tunnel to full" "Updated" \ + peer update-tunnel --name phone-testunit --mode full + + local conf_allowed + conf_allowed=$(grep "^AllowedIPs" /etc/wireguard/clients/phone-testunit.conf 2>/dev/null) + [[ "$conf_allowed" == *"0.0.0.0/0"* ]] && \ + test::pass "AllowedIPs set to full tunnel" || \ + test::fail "AllowedIPs not full tunnel (got: $conf_allowed)" + + cmd::test::run_cmd "peer update-tunnel to split" "Updated" \ + peer update-tunnel --name phone-testunit --mode split + + "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true +} + + function cmd::test::_destructive_cleanup() { cmd::test::run_cmd "remove phone peer" "removed" \ remove --name phone-testunit --force diff --git a/commands/test/integration.sh b/commands/test/integration.sh index 8cfbf77..de3a636 100644 --- a/commands/test/integration.sh +++ b/commands/test/integration.sh @@ -132,6 +132,8 @@ function cmd::test::run_all_integration_sections() { cmd::test::section_net cmd::test::section_subnet cmd::test::section_identity + cmd::test::section_hosts + cmd::test::section_peer_cmd } function cmd::test::section_list() { @@ -189,6 +191,11 @@ function cmd::test::section_logs() { cmd::test::run_cmd "logs --name phone-nuno" "Activity" logs --name phone-nuno cmd::test::run_cmd "logs --fw" "Firewall Drops" logs --fw cmd::test::run_cmd "logs --wg" "WireGuard Events" logs --wg + cmd::test::run_cmd "logs --since 2099-01-01" "No logs" logs --since "2099-01-01" + cmd::test::run_cmd "logs --wg --since 2099-01-01" "No logs" logs --wg --since "2099-01-01" + cmd::test::run_cmd "logs --fw --since 2099-01-01" "No logs" logs --fw --since "2099-01-01" + cmd::test::run_cmd "logs --wg --event attempt" "" logs --wg --event attempt + cmd::test::run_cmd "logs --detailed" "" logs --detailed } function cmd::test::section_fw() { @@ -249,4 +256,47 @@ function cmd::test::section_identity() { cmd::test::run_cmd "identity migrate --dry-run" "Dry run" identity migrate --dry-run cmd::test::run_cmd "identity show nuno" "nuno" identity show --name nuno cmd::test::run_cmd_fails "identity show nonexistent" identity show --name nonexistent -} \ No newline at end of file +} + +function cmd::test::section_hosts() { + test::section "Hosts" + + # Cleanup + "$WGCTL_BINARY" hosts rm --ip 192.0.2.1 --force > /dev/null 2>&1 || true + "$WGCTL_BINARY" hosts rm --port 9999 --force > /dev/null 2>&1 || true + + cmd::test::run_cmd "hosts list" "Hosts" hosts list + cmd::test::run_cmd "hosts add --ip" "Added" hosts add --ip 192.0.2.1 --name test-host --desc "Test" --tags test,unit + cmd::test::run_cmd "hosts list shows new" "test-host" hosts list + cmd::test::run_cmd "hosts show --ip" "Name" hosts show --ip 192.0.2.1 + cmd::test::run_cmd "hosts add --port" "Added" hosts add --port 9999 --name test-port + cmd::test::run_cmd "hosts list shows port" "test-port" hosts list + cmd::test::run_cmd "hosts rm --ip" "Removed" hosts rm --ip 192.0.2.1 --force + cmd::test::run_cmd "hosts rm --port" "Removed" hosts rm --port 9999 --force + cmd::test::run_cmd_fails "hosts show nonexistent" hosts show --ip 192.0.2.99 + cmd::test::run_cmd_fails "hosts add missing --name" hosts add --ip 192.0.2.1 +} + +function cmd::test::section_peer_cmd() { + test::section "Peer command" + + "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true + "$WGCTL_BINARY" add --name testunit --type phone > /dev/null 2>&1 + + # update-dns + cmd::test::run_cmd "peer update-dns --name" "Updated DNS" peer update-dns --name phone-testunit + cmd::test::run_cmd "peer update-dns applies" "10.0.0.103" config --name phone-testunit + cmd::test::run_cmd "peer update-dns --all" "peer(s)" peer update-dns --all + + # update-tunnel + cmd::test::run_cmd "peer update-tunnel split" "split" peer update-tunnel --name phone-testunit --mode split + cmd::test::run_cmd "peer update-tunnel full" "Updated" peer update-tunnel --name phone-testunit --mode full + cmd::test::run_cmd_fails "peer update-tunnel bad mode" peer update-tunnel --name phone-testunit --mode invalid + cmd::test::run_cmd_fails "peer update-tunnel missing --mode" peer update-tunnel --name phone-testunit + cmd::test::run_cmd_fails "peer update-tunnel missing --name" peer update-tunnel --mode split + + # Restore split tunnel + "$WGCTL_BINARY" peer update-tunnel --name phone-testunit --mode split > /dev/null 2>&1 || true + "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true +} + \ No newline at end of file diff --git a/commands/test/unit.sh b/commands/test/unit.sh index 61245b2..e242b43 100644 --- a/commands/test/unit.sh +++ b/commands/test/unit.sh @@ -44,6 +44,10 @@ function cmd::test::run_all_unit_sections() { cmd::test::unit_subnet cmd::test::unit_ip cmd::test::unit_identity + cmd::test::unit_fmt + cmd::test::unit_config + cmd::test::unit_parse_since + cmd::test::unit_group_status } function cmd::test::unit_subnet() { @@ -102,4 +106,117 @@ function cmd::test::unit_identity() { cmd::test::assert "infer laptop-nuno" "$(identity::infer 'laptop-nuno')" "nuno|laptop|1" cmd::test::assert "infer no convention" "$(identity::infer 'roboclean')" "" cmd::test::assert "infer guest-zephyr" "$(identity::infer 'guest-zephyr')" "" +} + +function cmd::test::unit_fmt() { + test::section "Unit: fmt::bytes" + + cmd::test::assert "fmt::bytes 0" "$(fmt::bytes 0)" "—" + cmd::test::assert "fmt::bytes bytes" "$(fmt::bytes 512)" "512B" + cmd::test::assert "fmt::bytes KB" "$(fmt::bytes 2048)" "2KB" + cmd::test::assert "fmt::bytes MB" "$(fmt::bytes 2097152)" "2MB" + cmd::test::assert "fmt::bytes GB" "$(fmt::bytes 2147483648)" "2GB" + cmd::test::assert "fmt::bytes 1023" "$(fmt::bytes 1023)" "1023B" + cmd::test::assert "fmt::bytes 1024" "$(fmt::bytes 1024)" "1KB" +} + +function cmd::test::unit_config() { + test::section "Unit: config dns" + + # config::dns_string with no fallback + local orig_fallback="${_WG_DNS_FALLBACK:-}" + _WG_DNS_FALLBACK="" + cmd::test::assert "dns_string no fallback" \ + "$(config::dns_string)" "$(config::dns)" + + # config::dns_string with fallback + _WG_DNS_FALLBACK="9.9.9.9" + cmd::test::assert "dns_string with fallback" \ + "$(config::dns_string)" "$(config::dns), 9.9.9.9" + + # config::dns_string with multiple fallbacks + _WG_DNS_FALLBACK="9.9.9.9,1.1.1.1" + cmd::test::assert "dns_string multi fallback" \ + "$(config::dns_string)" "$(config::dns), 9.9.9.9,1.1.1.1" + + # Restore + _WG_DNS_FALLBACK="$orig_fallback" +} + +function cmd::test::unit_parse_since() { + test::section "Unit: parse_since (Python)" + + # Test via Python directly + local py_result + + # Relative formats + py_result=$(python3 -c " +import sys +sys.path.insert(0, '/etc/wireguard/wgctl/core/lib') +from util import parse_since +from datetime import datetime, timezone, timedelta +dt = parse_since('2h') +now = datetime.now(timezone.utc) +diff = abs((now - dt).total_seconds() - 7200) +print('ok' if diff < 5 else f'fail: {diff}') +") + cmd::test::assert "parse_since 2h" "$py_result" "ok" + + py_result=$(python3 -c " +import sys +sys.path.insert(0, '/etc/wireguard/wgctl/core/lib') +from util import parse_since +dt = parse_since('7d') +print('ok' if dt is not None else 'fail') +") + cmd::test::assert "parse_since 7d" "$py_result" "ok" + + # EU date format + py_result=$(python3 -c " +import sys +sys.path.insert(0, '/etc/wireguard/wgctl/core/lib') +from util import parse_since +from datetime import datetime +dt = parse_since('23/05') +print('ok' if dt is not None and dt.day == 23 and dt.month == 5 else f'fail: {dt}') +") + cmd::test::assert "parse_since 23/05" "$py_result" "ok" + + # ISO format + py_result=$(python3 -c " +import sys +sys.path.insert(0, '/etc/wireguard/wgctl/core/lib') +from util import parse_since +dt = parse_since('2026-05-23') +print('ok' if dt is not None and dt.year == 2026 and dt.month == 5 and dt.day == 23 else f'fail: {dt}') +") + cmd::test::assert "parse_since ISO" "$py_result" "ok" + + # Invalid + py_result=$(python3 -c " +import sys +sys.path.insert(0, '/etc/wireguard/wgctl/core/lib') +from util import parse_since +dt = parse_since('not-a-date') +print('ok' if dt is None else f'fail: {dt}') +") + cmd::test::assert "parse_since invalid" "$py_result" "ok" +} + +function cmd::test::unit_group_status() { + test::section "Unit: ui::group::status" + load_module ui + + local color str + IFS='|' read -r color str <<< "$(ui::group::status 0 0)" + cmd::test::assert "group status inactive str" "$str" "inactive" + + IFS='|' read -r color str <<< "$(ui::group::status 4 0)" + cmd::test::assert "group status active str" "$str" "active" + + IFS='|' read -r color str <<< "$(ui::group::status 4 4)" + cmd::test::assert "group status blocked str" "$str" "blocked" + + IFS='|' read -r color str <<< "$(ui::group::status 4 2)" + cmd::test::assert "group status partial str" "$str" "partial (2/4)" } \ No newline at end of file diff --git a/daemon/endpoint_cache.json b/daemon/endpoint_cache.json index 04cae3f..d1fae4b 100644 --- a/daemon/endpoint_cache.json +++ b/daemon/endpoint_cache.json @@ -1,13 +1,13 @@ { "phone-fred": "176.223.61.130", "phone-helena": "148.69.46.73", - "phone-nuno": "94.63.0.129", + "phone-nuno": "148.69.50.62", "tablet-nuno": "148.69.202.5", "guest-zephyr": "86.120.152.74", "guest-zephyr-test": "94.63.0.129", "desktop-roboclean": "46.189.215.231", "laptop-nuno": "94.63.0.129", "phone-luis": "176.223.61.15", - "phone-helena-2": "148.69.202.127", + "phone-helena-2": "148.69.203.225", "desktop-zephyr": "86.120.152.74" } \ No newline at end of file diff --git a/modules/config.module.sh b/modules/config.module.sh index 0c24a9b..dadb7bc 100644 --- a/modules/config.module.sh +++ b/modules/config.module.sh @@ -172,7 +172,7 @@ function config::activity_current_high() { echo "$_ACTIVITY_TOTAL_HIGH_BYTES"; function config::server_public_key() { cat "$_WG_SERVER_PUBLIC_KEY_FILE"; } function config::allowed_ips_for() { - local tunnel="${2:-split}" + local tunnel="${1:-split}" case "$tunnel" in full) echo "$_WG_TUNNEL_FULL" ;; split) echo "$_WG_TUNNEL_SPLIT" ;; diff --git a/modules/peers.module.sh b/modules/peers.module.sh index 7f9a860..7c2d415 100644 --- a/modules/peers.module.sh +++ b/modules/peers.module.sh @@ -538,6 +538,24 @@ function peers::format_activity_current() { echo "${level} (↓${rx_hr}B/s ↑${tx_hr}B/s)" } +# ============================================ +# dentity +# ============================================ + +function peers::get_identity() { + local peer_name="${1:-}" + local id_dir + id_dir="$(ctx::identities)" + for id_file in "${id_dir}"/*.identity; do + [[ -f "$id_file" ]] || continue + if json::get "$id_file" "peers" 2>/dev/null | grep -qx "$peer_name"; then + basename "$id_file" .identity + return 0 + fi + done + return 1 +} + # ============================================ # Helpers - Meta File # ============================================