Compare commits
2 commits
79769667fb
...
91593b2576
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91593b2576 | ||
|
|
0b9f113453 |
11 changed files with 365 additions and 36 deletions
|
|
@ -16,6 +16,7 @@ function cmd::block::on_load() {
|
||||||
flag::register --subnet
|
flag::register --subnet
|
||||||
flag::register --block-name
|
flag::register --block-name
|
||||||
flag::register --service
|
flag::register --service
|
||||||
|
flag::register --reason
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -61,6 +62,7 @@ function cmd::block::run() {
|
||||||
local name="" identity="" type="" block_name=""
|
local name="" identity="" type="" block_name=""
|
||||||
local ips=() subnets=() ports=() services=()
|
local ips=() subnets=() ports=() services=()
|
||||||
local quiet=false force=false
|
local quiet=false force=false
|
||||||
|
local reason=""
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
|
@ -74,6 +76,7 @@ function cmd::block::run() {
|
||||||
--quiet) quiet=true; shift ;;
|
--quiet) quiet=true; shift ;;
|
||||||
--subnet) subnets+=("$2"); shift 2 ;;
|
--subnet) subnets+=("$2"); shift 2 ;;
|
||||||
--port) ports+=("$2"); shift 2 ;;
|
--port) ports+=("$2"); shift 2 ;;
|
||||||
|
--reason) reason="$2"; shift 2 ;;
|
||||||
--help) cmd::block::help; return ;;
|
--help) cmd::block::help; return ;;
|
||||||
*)
|
*)
|
||||||
log::error "Unknown flag: $1"
|
log::error "Unknown flag: $1"
|
||||||
|
|
@ -110,6 +113,7 @@ function cmd::block::run() {
|
||||||
fi
|
fi
|
||||||
monitor::update_endpoint_cache
|
monitor::update_endpoint_cache
|
||||||
cmd::block::_block_all "$name" "$client_ip" "$quiet"
|
cmd::block::_block_all "$name" "$client_ip" "$quiet"
|
||||||
|
cmd::block::_record_history "$name" "full" "manual" "$reason"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -209,6 +213,14 @@ function cmd::block::run() {
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Record history — derive block type from what was blocked
|
||||||
|
local btype="specific"
|
||||||
|
[[ ${#services[@]} -gt 0 ]] && btype="${services[0]}"
|
||||||
|
[[ ${#ips[@]} -gt 0 ]] && btype="ip"
|
||||||
|
[[ ${#subnets[@]} -gt 0 ]] && btype="subnet"
|
||||||
|
[[ ${#ports[@]} -gt 0 ]] && btype="port"
|
||||||
|
cmd::block::_record_history "$name" "$btype" "manual" "$reason"
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,3 +283,24 @@ function cmd::block::_block_all() {
|
||||||
|
|
||||||
$quiet || log::wg_success "${name} has been blocked."
|
$quiet || log::wg_success "${name} has been blocked."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cmd::block::_record_history() {
|
||||||
|
local name="${1:-}" block_type="${2:-full}" \
|
||||||
|
triggered_by="${3:-manual}" reason="${4:-}"
|
||||||
|
|
||||||
|
local endpoint
|
||||||
|
endpoint=$(json::peer_history_lookup "$name" 2>/dev/null || true)
|
||||||
|
|
||||||
|
# endpoint_cache lookup
|
||||||
|
local ep_cache
|
||||||
|
ep_cache=$(json::endpoint_cache_get "$(ctx::endpoint_cache)" "$name" 2>/dev/null || true)
|
||||||
|
|
||||||
|
json::block_history_record \
|
||||||
|
"$(ctx::block_history)" \
|
||||||
|
"$name" \
|
||||||
|
"$block_type" \
|
||||||
|
"$triggered_by" \
|
||||||
|
"$reason" \
|
||||||
|
"${ep_cache:-}" \
|
||||||
|
2>/dev/null > /dev/null || true
|
||||||
|
}
|
||||||
|
|
@ -215,6 +215,7 @@ function cmd::export::_full() {
|
||||||
"$(ctx::identities)" \
|
"$(ctx::identities)" \
|
||||||
"$(ctx::groups)" \
|
"$(ctx::groups)" \
|
||||||
"$(ctx::blocks)" \
|
"$(ctx::blocks)" \
|
||||||
|
"$(ctx::block_history)" \
|
||||||
"$(ctx::config_file)" \
|
"$(ctx::config_file)" \
|
||||||
"$(ctx::policies)" \
|
"$(ctx::policies)" \
|
||||||
"$(ctx::subnets)" \
|
"$(ctx::subnets)" \
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,7 @@ function cmd::test::run_all_integration_sections() {
|
||||||
cmd::test::section_config
|
cmd::test::section_config
|
||||||
cmd::test::section_rules
|
cmd::test::section_rules
|
||||||
cmd::test::section_groups
|
cmd::test::section_groups
|
||||||
|
cmd::test::section_block_unblock
|
||||||
cmd::test::section_audit
|
cmd::test::section_audit
|
||||||
cmd::test::section_logs
|
cmd::test::section_logs
|
||||||
cmd::test::section_fw
|
cmd::test::section_fw
|
||||||
|
|
@ -204,6 +205,92 @@ function cmd::test::section_groups() {
|
||||||
cmd::test::run_cmd_fails "group show nonexistent" group show --name nonexistent
|
cmd::test::run_cmd_fails "group show nonexistent" group show --name nonexistent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# function cmd::test::section_blocks() {
|
||||||
|
# test::section "Blocks"
|
||||||
|
# cmd::test::run_cmd "block --reason records history" "block-history" block --name guest-test --reason "test block" --force
|
||||||
|
# cmd::test::run_cmd_succeeds "unblock clears history" unblock --name guest-test --force
|
||||||
|
# }
|
||||||
|
|
||||||
|
function cmd::test::section_block_unblock() {
|
||||||
|
test::section "Block / Unblock"
|
||||||
|
|
||||||
|
# ── Setup fixture ──
|
||||||
|
local fixture="phone-testblock"
|
||||||
|
wgctl unblock --name "$fixture" --force >/dev/null 2>&1 || true
|
||||||
|
wgctl remove --name "$fixture" --force >/dev/null 2>&1 || true
|
||||||
|
wgctl add --name testblock --type phone >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
local history_file
|
||||||
|
history_file="$(ctx::block_history)/${fixture}.json"
|
||||||
|
|
||||||
|
# ── Block ──
|
||||||
|
echo "DEBUG about to run: $WGCTL_BINARY block --name $fixture --force" >&2
|
||||||
|
cmd::test::run_cmd "block peer" "blocked" block --name "$fixture" --force
|
||||||
|
cmd::test::run_cmd "block already blocked" "already" block --name "$fixture" --force
|
||||||
|
|
||||||
|
wgctl unblock --name "$fixture" --force >/dev/null 2>&1 || true
|
||||||
|
cmd::test::run_cmd "block with reason" "blocked" block --name "$fixture" --force \
|
||||||
|
--reason "test reason"
|
||||||
|
|
||||||
|
# ── Block history file created ──
|
||||||
|
[[ -f "$history_file" ]] && test::pass "block history file created" \
|
||||||
|
|| test::fail "block history file not created"
|
||||||
|
|
||||||
|
# ── Block history fields ──
|
||||||
|
if [[ -f "$history_file" ]]; then
|
||||||
|
local has_id has_blocked_at has_endpoint has_reason
|
||||||
|
has_id=$(python3 -c "
|
||||||
|
import json
|
||||||
|
d=json.load(open('$history_file'))
|
||||||
|
print('yes' if d['history'] and 'id' in d['history'][-1] else 'no')
|
||||||
|
" 2>/dev/null)
|
||||||
|
has_blocked_at=$(python3 -c "
|
||||||
|
import json
|
||||||
|
d=json.load(open('$history_file'))
|
||||||
|
print('yes' if d['history'] and d['history'][-1].get('blocked_at') else 'no')
|
||||||
|
" 2>/dev/null)
|
||||||
|
has_endpoint=$(python3 -c "
|
||||||
|
import json
|
||||||
|
d=json.load(open('$history_file'))
|
||||||
|
print('yes' if 'endpoint_at_block' in d['history'][-1] else 'no')
|
||||||
|
" 2>/dev/null)
|
||||||
|
has_reason=$(python3 -c "
|
||||||
|
import json
|
||||||
|
d=json.load(open('$history_file'))
|
||||||
|
print('yes' if d['history'][-1].get('reason') == 'test reason' else 'no')
|
||||||
|
" 2>/dev/null)
|
||||||
|
cmd::test::assert "history has id" "$has_id" "yes"
|
||||||
|
cmd::test::assert "history has blocked_at" "$has_blocked_at" "yes"
|
||||||
|
cmd::test::assert "history has endpoint" "$has_endpoint" "yes"
|
||||||
|
cmd::test::assert "history has reason" "$has_reason" "yes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Unblock ──
|
||||||
|
cmd::test::run_cmd "unblock peer" "unblocked" unblock --name "$fixture" --force \
|
||||||
|
--reason "test cleanup"
|
||||||
|
cmd::test::run_cmd "unblock not blocked" "not blocked" unblock --name "$fixture" --force
|
||||||
|
|
||||||
|
# ── Unblock history updated ──
|
||||||
|
if [[ -f "$history_file" ]]; then
|
||||||
|
local has_unblocked has_unblock_reason
|
||||||
|
has_unblocked=$(python3 -c "
|
||||||
|
import json
|
||||||
|
d=json.load(open('$history_file'))
|
||||||
|
print('yes' if d['history'] and d['history'][-1].get('unblocked_at') else 'no')
|
||||||
|
" 2>/dev/null)
|
||||||
|
has_unblock_reason=$(python3 -c "
|
||||||
|
import json
|
||||||
|
d=json.load(open('$history_file'))
|
||||||
|
print('yes' if d['history'][-1].get('unblock_reason') == 'test cleanup' else 'no')
|
||||||
|
" 2>/dev/null)
|
||||||
|
cmd::test::assert "history has unblocked_at" "$has_unblocked" "yes"
|
||||||
|
cmd::test::assert "history has unblock_reason" "$has_unblock_reason" "yes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Teardown fixture ──
|
||||||
|
wgctl remove --name "$fixture" --force >/dev/null 2>&1 || true
|
||||||
|
rm -f "$history_file"
|
||||||
|
}
|
||||||
function cmd::test::section_audit() {
|
function cmd::test::section_audit() {
|
||||||
test::section "Audit"
|
test::section "Audit"
|
||||||
cmd::test::run_cmd_any "audit" "passed" audit
|
cmd::test::run_cmd_any "audit" "passed" audit
|
||||||
|
|
@ -424,3 +511,36 @@ function cmd::test::section_display() {
|
||||||
|
|
||||||
# Test style via unit tests (see unit section)
|
# Test style via unit tests (see unit section)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Helpers
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::test::assert() {
|
||||||
|
local desc="${1:-}" result="${2:-}" expected="${3:-}"
|
||||||
|
if [[ "$result" == "$expected" ]]; then
|
||||||
|
test::pass "$desc"
|
||||||
|
else
|
||||||
|
test::fail "${desc} (expected '${expected}', got '${result}')"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::test::assert_true() {
|
||||||
|
local desc="${1:-}"
|
||||||
|
shift
|
||||||
|
if "$@" 2>/dev/null; then
|
||||||
|
test::pass "$desc"
|
||||||
|
else
|
||||||
|
test::fail "$desc (expected true, got false)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::test::assert_false() {
|
||||||
|
local desc="${1:-}"
|
||||||
|
shift
|
||||||
|
if ! "$@" 2>/dev/null; then
|
||||||
|
test::pass "$desc"
|
||||||
|
else
|
||||||
|
test::fail "$desc (expected false, got true)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ function cmd::unblock::on_load() {
|
||||||
flag::register --subnet
|
flag::register --subnet
|
||||||
flag::register --all
|
flag::register --all
|
||||||
flag::register --service
|
flag::register --service
|
||||||
|
flag::register --reason
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -59,6 +60,7 @@ function cmd::unblock::run() {
|
||||||
local name="" identity="" type=""
|
local name="" identity="" type=""
|
||||||
local ips=() subnets=() ports=() services=()
|
local ips=() subnets=() ports=() services=()
|
||||||
local all=false quiet=false force=false
|
local all=false quiet=false force=false
|
||||||
|
local reason=""
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
|
|
@ -71,6 +73,7 @@ function cmd::unblock::run() {
|
||||||
--subnet) subnets+=("$2"); shift 2 ;;
|
--subnet) subnets+=("$2"); shift 2 ;;
|
||||||
--port) ports+=("$2"); shift 2 ;;
|
--port) ports+=("$2"); shift 2 ;;
|
||||||
--service) services+=("$2"); shift 2 ;;
|
--service) services+=("$2"); shift 2 ;;
|
||||||
|
--reason) reason="$2"; shift 2 ;;
|
||||||
--all) all=true; shift ;;
|
--all) all=true; shift ;;
|
||||||
--help) cmd::unblock::help; return ;;
|
--help) cmd::unblock::help; return ;;
|
||||||
*)
|
*)
|
||||||
|
|
@ -110,6 +113,7 @@ function cmd::unblock::run() {
|
||||||
|
|
||||||
if $all; then
|
if $all; then
|
||||||
cmd::unblock::_unblock_all "$name" "$client_ip" "$quiet"
|
cmd::unblock::_unblock_all "$name" "$client_ip" "$quiet"
|
||||||
|
cmd::unblock::_record_history "$name" "manual" "$reason"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -180,6 +184,9 @@ function cmd::unblock::run() {
|
||||||
done
|
done
|
||||||
|
|
||||||
block::cleanup "$name"
|
block::cleanup "$name"
|
||||||
|
|
||||||
|
# Record unblock for specific rules
|
||||||
|
cmd::unblock::_record_history "$name" "manual" "$reason"
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -249,3 +256,14 @@ function cmd::unblock::_unblock_all() {
|
||||||
$quiet || log::wg_success "${name} has been unblocked."
|
$quiet || log::wg_success "${name} has been unblocked."
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cmd::unblock::_record_history() {
|
||||||
|
local name="${1:-}" unblocked_by="${2:-manual}" reason="${3:-}"
|
||||||
|
|
||||||
|
json::block_history_unblock \
|
||||||
|
"$(ctx::block_history)" \
|
||||||
|
"$name" \
|
||||||
|
"$unblocked_by" \
|
||||||
|
"$reason" \
|
||||||
|
2>/dev/null > /dev/null || true
|
||||||
|
}
|
||||||
|
|
@ -86,6 +86,8 @@ function ctx::json_helper() { echo "${_CTX_CORE}/json_helper.py"; }
|
||||||
function ctx::monitor_script() { echo "${_CTX_ROOT}/daemon/wgctl-monitor.py"; }
|
function ctx::monitor_script() { echo "${_CTX_ROOT}/daemon/wgctl-monitor.py"; }
|
||||||
function ctx::lib() { echo "${_CTX_CORE}/lib"; }
|
function ctx::lib() { echo "${_CTX_CORE}/lib"; }
|
||||||
|
|
||||||
|
function ctx::block_history() { echo "${_CTX_DATA}/block-history"; }
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Path Helpers
|
# Path Helpers
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,13 @@ function json::error_envelope() {
|
||||||
# Config
|
# Config
|
||||||
function json::config_load() { python3 "$JSON_HELPER" config_load "$1" </dev/null; }
|
function json::config_load() { python3 "$JSON_HELPER" config_load "$1" </dev/null; }
|
||||||
|
|
||||||
|
function json::block_history_record() { python3 "$JSON_HELPER" block_history_record "$@" </dev/null; }
|
||||||
|
function json::block_history_unblock() { python3 "$JSON_HELPER" block_history_unblock "$@" </dev/null; }
|
||||||
|
function json::block_history_list() { python3 "$JSON_HELPER" block_history_list "$@" </dev/null; }
|
||||||
|
function json::block_history_list_all() { python3 "$JSON_HELPER" block_history_list_all "$@" </dev/null; }
|
||||||
|
|
||||||
|
function json::endpoint_cache_get() { python3 "$JSON_HELPER" endpoint_cache_get "$@" </dev/null; }
|
||||||
|
|
||||||
function json::peer_transfer() {
|
function json::peer_transfer() {
|
||||||
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \
|
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \
|
||||||
ACTIVITY_TOTAL_MED="$(config::activity_total_med)" \
|
ACTIVITY_TOTAL_MED="$(config::activity_total_med)" \
|
||||||
|
|
|
||||||
|
|
@ -1713,7 +1713,7 @@ def _export_peer_data(name, clients_dir, meta_dir, identities_dir, groups_dir, b
|
||||||
|
|
||||||
|
|
||||||
def export_full(clients_dir, meta_dir, rules_dir, identities_dir,
|
def export_full(clients_dir, meta_dir, rules_dir, identities_dir,
|
||||||
groups_dir, blocks_dir, config_file,
|
groups_dir, blocks_dir, block_history_dir, config_file,
|
||||||
policies_file, subnets_file, net_file, hosts_file,
|
policies_file, subnets_file, net_file, hosts_file,
|
||||||
no_config, no_peers, version):
|
no_config, no_peers, version):
|
||||||
"""Build full wgctl export as JSON."""
|
"""Build full wgctl export as JSON."""
|
||||||
|
|
@ -1778,6 +1778,17 @@ def export_full(clients_dir, meta_dir, rules_dir, identities_dir,
|
||||||
pass
|
pass
|
||||||
data['groups'] = groups
|
data['groups'] = groups
|
||||||
|
|
||||||
|
|
||||||
|
# Block history
|
||||||
|
block_histories = []
|
||||||
|
for path in sorted(glob.glob(f"{block_history_dir}/*.json")):
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
block_histories.append(json.load(f))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
data['block_history'] = block_histories
|
||||||
|
|
||||||
# Flat JSON files
|
# Flat JSON files
|
||||||
for key, path in [
|
for key, path in [
|
||||||
('policies', policies_file),
|
('policies', policies_file),
|
||||||
|
|
@ -1801,6 +1812,15 @@ def export_full(clients_dir, meta_dir, rules_dir, identities_dir,
|
||||||
}
|
}
|
||||||
print(json.dumps(result))
|
print(json.dumps(result))
|
||||||
|
|
||||||
|
def endpoint_cache_get(cache_file, peer):
|
||||||
|
"""Get cached endpoint IP for a peer."""
|
||||||
|
try:
|
||||||
|
with open(cache_file) as f:
|
||||||
|
cache = json.load(f)
|
||||||
|
print(cache.get(peer, ''))
|
||||||
|
except Exception:
|
||||||
|
print('')
|
||||||
|
|
||||||
# ======================================================
|
# ======================================================
|
||||||
|
|
||||||
def _net_read(file):
|
def _net_read(file):
|
||||||
|
|
@ -2174,10 +2194,10 @@ commands = {
|
||||||
'display_load': lambda args: display_load(args[0]),
|
'display_load': lambda args: display_load(args[0]),
|
||||||
'export_full': lambda args: export_full(
|
'export_full': lambda args: export_full(
|
||||||
args[0], args[1], args[2], args[3], args[4], args[5],
|
args[0], args[1], args[2], args[3], args[4], args[5],
|
||||||
args[6], args[7], args[8], args[9], args[10],
|
args[6], args[7], args[8], args[9], args[10], args[11],
|
||||||
args[11] if len(args) > 11 else 'false',
|
|
||||||
args[12] if len(args) > 12 else 'false',
|
args[12] if len(args) > 12 else 'false',
|
||||||
args[13] if len(args) > 13 else 'unknown'),
|
args[13] if len(args) > 13 else 'false',
|
||||||
|
args[14] if len(args) > 14 else 'unknown'),
|
||||||
|
|
||||||
# Import
|
# Import
|
||||||
'import_peer': lambda args: __import__('lib.importer', fromlist=['import_peer']).import_peer(
|
'import_peer': lambda args: __import__('lib.importer', fromlist=['import_peer']).import_peer(
|
||||||
|
|
@ -2191,6 +2211,20 @@ commands = {
|
||||||
args[6], args[7], args[8], args[9], args[10],
|
args[6], args[7], args[8], args[9], args[10],
|
||||||
args[11] if len(args) > 11 else 'false'),
|
args[11] if len(args) > 11 else 'false'),
|
||||||
'import_get_field': lambda args: __import__('lib.importer', fromlist=['import_get_field']).import_get_field(args[0], *args[1:]),
|
'import_get_field': lambda args: __import__('lib.importer', fromlist=['import_get_field']).import_get_field(args[0], *args[1:]),
|
||||||
|
'block_history_record': lambda args: __import__('lib.block_history',
|
||||||
|
fromlist=['block_history_record']).block_history_record(
|
||||||
|
args[0], args[1], args[2], args[3],
|
||||||
|
args[4] if len(args) > 4 else '',
|
||||||
|
args[5] if len(args) > 5 else ''),
|
||||||
|
'block_history_unblock': lambda args: __import__('lib.block_history',
|
||||||
|
fromlist=['block_history_unblock']).block_history_unblock(
|
||||||
|
args[0], args[1], args[2],
|
||||||
|
args[3] if len(args) > 3 else ''),
|
||||||
|
'block_history_list': lambda args: __import__('lib.block_history',
|
||||||
|
fromlist=['block_history_list']).block_history_list(args[0], args[1]),
|
||||||
|
'block_history_list_all': lambda args: __import__('lib.block_history',
|
||||||
|
fromlist=['block_history_list_all']).block_history_list_all(args[0]),
|
||||||
|
'endpoint_cache_get': lambda args: endpoint_cache_get(args[0], args[1]),
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Main ─────────────────────────────────────────────────────────────────────
|
# ── Main ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
BIN
core/lib/__pycache__/block_history.cpython-311.pyc
Normal file
BIN
core/lib/__pycache__/block_history.cpython-311.pyc
Normal file
Binary file not shown.
103
core/lib/block_history.py
Normal file
103
core/lib/block_history.py
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
# core/lib/block_history.py
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
|
BLOCK_HISTORY_VERSION = 1
|
||||||
|
|
||||||
|
|
||||||
|
def _history_file(history_dir, peer):
|
||||||
|
return os.path.join(history_dir, f"{peer}.json")
|
||||||
|
|
||||||
|
|
||||||
|
def _load(history_dir, peer):
|
||||||
|
path = _history_file(history_dir, peer)
|
||||||
|
if os.path.exists(path):
|
||||||
|
try:
|
||||||
|
return json.load(open(path))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"peer": peer, "version": BLOCK_HISTORY_VERSION, "history": []}
|
||||||
|
|
||||||
|
|
||||||
|
def _save(history_dir, peer, data):
|
||||||
|
os.makedirs(history_dir, exist_ok=True)
|
||||||
|
path = _history_file(history_dir, peer)
|
||||||
|
open(path, 'w').write(json.dumps(data, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def _next_id(history):
|
||||||
|
if not history:
|
||||||
|
return 1
|
||||||
|
return max(int(e.get("id", 0)) for e in history) + 1
|
||||||
|
|
||||||
|
|
||||||
|
def _now():
|
||||||
|
return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_endpoint(clients_dir, peer):
|
||||||
|
"""Try to get current endpoint from endpoint cache."""
|
||||||
|
cache_file = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(clients_dir)),
|
||||||
|
'.wgctl', 'daemon', 'endpoint_cache.json')
|
||||||
|
try:
|
||||||
|
cache = json.load(open(cache_file))
|
||||||
|
return cache.get(peer, '')
|
||||||
|
except Exception:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def block_history_record(history_dir, peer, block_type,
|
||||||
|
triggered_by, reason, endpoint_at_block):
|
||||||
|
"""Record a new block event."""
|
||||||
|
data = _load(history_dir, peer)
|
||||||
|
entry = {
|
||||||
|
"id": _next_id(data["history"]),
|
||||||
|
"blocked_at": _now(),
|
||||||
|
"unblocked_at": None,
|
||||||
|
"block_type": block_type,
|
||||||
|
"triggered_by": triggered_by,
|
||||||
|
"reason": reason or '',
|
||||||
|
"endpoint_at_block": endpoint_at_block or '',
|
||||||
|
"unblocked_by": None,
|
||||||
|
"unblock_reason": None,
|
||||||
|
}
|
||||||
|
data["history"].append(entry)
|
||||||
|
_save(history_dir, peer, data)
|
||||||
|
print(entry["id"])
|
||||||
|
|
||||||
|
|
||||||
|
def block_history_unblock(history_dir, peer, unblocked_by, unblock_reason):
|
||||||
|
"""Update the most recent open block event with unblock timestamp."""
|
||||||
|
data = _load(history_dir, peer)
|
||||||
|
# Find most recent entry without unblocked_at
|
||||||
|
for entry in reversed(data["history"]):
|
||||||
|
if entry.get("unblocked_at") is None:
|
||||||
|
entry["unblocked_at"] = _now()
|
||||||
|
entry["unblocked_by"] = unblocked_by
|
||||||
|
entry["unblock_reason"] = unblock_reason or ''
|
||||||
|
_save(history_dir, peer, data)
|
||||||
|
print(entry["id"])
|
||||||
|
return
|
||||||
|
# No open block found — not an error, peer may have been unblocked externally
|
||||||
|
|
||||||
|
|
||||||
|
def block_history_list(history_dir, peer):
|
||||||
|
"""Output block history for a peer as JSON."""
|
||||||
|
data = _load(history_dir, peer)
|
||||||
|
print(json.dumps(data))
|
||||||
|
|
||||||
|
|
||||||
|
def block_history_list_all(history_dir):
|
||||||
|
"""Output block history for all peers as JSON array."""
|
||||||
|
import glob
|
||||||
|
results = []
|
||||||
|
for path in sorted(glob.glob(os.path.join(history_dir, '*.json'))):
|
||||||
|
try:
|
||||||
|
results.append(json.load(open(path)))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
print(json.dumps(results))
|
||||||
|
|
@ -162,6 +162,17 @@ def import_full(file, clients_dir, meta_dir, rules_dir, identities_dir,
|
||||||
json.dumps(grp, indent=2))
|
json.dumps(grp, indent=2))
|
||||||
results.append('groups')
|
results.append('groups')
|
||||||
|
|
||||||
|
# Block history
|
||||||
|
bh_dir = os.path.join(os.path.dirname(groups_dir), 'block-history')
|
||||||
|
os.makedirs(bh_dir, exist_ok=True)
|
||||||
|
for bh in data.get('block_history', []):
|
||||||
|
peer_name = bh.get('peer', '')
|
||||||
|
if peer_name:
|
||||||
|
open(os.path.join(bh_dir, f"{peer_name}.json"), 'w').write(
|
||||||
|
json.dumps(bh, indent=2))
|
||||||
|
if data.get('block_history'):
|
||||||
|
results.append('block_history')
|
||||||
|
|
||||||
# Flat JSON files
|
# Flat JSON files
|
||||||
for key, path in [('policies', policies_file), ('subnets', subnets_file),
|
for key, path in [('policies', policies_file), ('subnets', subnets_file),
|
||||||
('services', net_file), ('hosts', hosts_file)]:
|
('services', net_file), ('hosts', hosts_file)]:
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
{
|
{
|
||||||
"phone-fred": "176.223.61.130",
|
"phone-fred": "176.223.61.130",
|
||||||
"phone-helena": "148.69.46.73",
|
"phone-helena": "148.69.46.73",
|
||||||
"phone-nuno": "148.69.48.20",
|
"phone-nuno": "94.63.0.129",
|
||||||
"tablet-nuno": "148.69.202.5",
|
"tablet-nuno": "148.69.202.5",
|
||||||
"guest-zephyr": "86.120.152.74",
|
"guest-zephyr": "86.120.152.74",
|
||||||
"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",
|
"laptop-nuno": "94.63.0.129",
|
||||||
"phone-luis": "176.223.61.15",
|
"phone-luis": "176.223.61.15",
|
||||||
"phone-helena-2": "148.69.202.234",
|
"phone-helena-2": "148.69.193.234",
|
||||||
"desktop-zephyr": "86.120.152.74"
|
"desktop-zephyr": "86.120.152.74"
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue