diff --git a/ochenstarik-server-monitor-manager.sh b/ochenstarik-server-monitor-manager.sh index 784ebaa..8fe9188 100644 --- a/ochenstarik-server-monitor-manager.sh +++ b/ochenstarik-server-monitor-manager.sh @@ -196,6 +196,8 @@ readonly TOKENS_DIR="${STATE_DIR}/tokens" readonly HUB_CONFIG="${STATE_DIR}/hub.conf" readonly HUB_PRIVATE_KEY="${STATE_DIR}/hub.key" readonly LINKS_FILE="${STATE_DIR}/links" +readonly AUDIT_FILE="${STATE_DIR}/audit.jsonl" +readonly VERSION_FILE="${STATE_DIR}/policy.version" readonly WG_INTERFACE="smm0" readonly WG_CONFIG="/etc/wireguard/${WG_INTERFACE}.conf" readonly NFT_TABLE="ochenstarik_smm" @@ -214,6 +216,26 @@ node_file() { printf '%s/%s.node' "$NODES_DIR" "$1"; } node_value() { config_value "$2" "$(node_file "$1")"; } node_exists() { [[ -f "$(node_file "$1")" ]]; } +next_policy_version() { + local version=0 tmp + [[ ! -r "$VERSION_FILE" ]] || read -r version < "$VERSION_FILE" + [[ "$version" =~ ^[0-9]+$ ]] || version=0 + version=$((version + 1)) + tmp="$(mktemp)" + printf '%s\n' "$version" > "$tmp" + install -m 0600 -o root -g root "$tmp" "$VERSION_FILE" + rm -f -- "$tmp" + printf '%s' "$version" +} + +audit_link() { + local action="$1" state="$2" source="$3" target="$4" protocol="$5" port="$6" version="$7" + printf '{"time":"%s","action":"%s","state":"%s","source":"%s","target":"%s","protocol":"%s","port":%s,"version":%s}\n' \ + "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$action" "$state" "$source" "$target" "$protocol" "$port" "$version" \ + >> "$AUDIT_FILE" + chmod 0600 "$AUDIT_FILE" +} + render_wireguard_config() { local tmp name file public_key address tmp="$(mktemp)" @@ -375,11 +397,11 @@ node_enroll() { } prune_links() { - local tmp source target cidr protocol port expires now target_ip + local tmp source target cidr protocol port expires version now target_ip tmp="$(mktemp)" now="$(date +%s)" if [[ -f "$LINKS_FILE" ]]; then - while read -r source target cidr protocol port expires extra; do + while read -r source target cidr protocol port expires version extra; do [[ -z "${extra:-}" ]] || continue valid_name "$source" && valid_name "$target" || continue node_exists "$source" && node_exists "$target" || continue @@ -388,8 +410,12 @@ prune_links() { [[ "$protocol" == tcp || "$protocol" == udp ]] || continue [[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )) || continue [[ "$expires" =~ ^[0-9]+$ ]] || continue - (( expires == 0 || expires > now )) || continue - printf '%s %s %s %s %s %s\n' "$source" "$target" "$cidr" "$protocol" "$port" "$expires" >> "$tmp" + [[ "${version:-}" =~ ^[0-9]+$ ]] || version=0 + if (( expires != 0 && expires <= now )); then + audit_link expire Expired "$source" "$target" "$protocol" "$port" "$version" + continue + fi + printf '%s %s %s %s %s %s %s\n' "$source" "$target" "$cidr" "$protocol" "$port" "$expires" "$version" >> "$tmp" done < "$LINKS_FILE" fi sort -u -o "$tmp" "$tmp" @@ -398,15 +424,16 @@ prune_links() { } restore_firewall() { - local tmp source target cidr protocol port expires source_ip + local tmp body source target cidr protocol port expires version source_ip prune_links tmp="$(mktemp)" + body="$(mktemp)" { printf 'table inet %s {\n' "$NFT_TABLE" printf ' chain forward {\n' printf ' type filter hook forward priority 10; policy accept;\n' printf ' iifname "%s" oifname "%s" ct state established,related accept\n' "$WG_INTERFACE" "$WG_INTERFACE" - while read -r source target cidr protocol port expires; do + while read -r source target cidr protocol port expires version; do [[ -n "$source" ]] || continue source_ip="$(node_value "$source" ADDRESS)" printf ' iifname "%s" oifname "%s" ip saddr %s ip daddr %s %s dport %s accept\n' \ @@ -414,16 +441,22 @@ restore_firewall() { done < "$LINKS_FILE" printf ' iifname "%s" oifname "%s" drop\n' "$WG_INTERFACE" "$WG_INTERFACE" printf ' }\n}\n' - } > "$tmp" - nft --check -f "$tmp" - nft delete table inet "$NFT_TABLE" >/dev/null 2>&1 || true - nft -f "$tmp" + } > "$body" + if nft list table inet "$NFT_TABLE" >/dev/null 2>&1; then + printf 'delete table inet %s\n' "$NFT_TABLE" > "$tmp" + fi + cat "$body" >> "$tmp" + rm -f -- "$body" + if ! nft --check -f "$tmp" || ! nft -f "$tmp"; then + rm -f -- "$tmp" + return 1 + fi rm -f -- "$tmp" } link_connect() { local source="$1" target="$2" protocol="$3" port="$4" ttl_minutes="$5" - local target_ip cidr expires tmp + local target_ip cidr expires version tmp valid_name "$source" && valid_name "$target" || die "Некорректное имя узла" [[ "$source" != "$target" ]] || die "Источник и назначение совпадают" node_exists "$source" || die "Узел $source не найден" @@ -436,24 +469,33 @@ link_connect() { cidr="${target_ip}/32" expires=0 (( ttl_minutes == 0 )) || expires=$(( $(date +%s) + ttl_minutes * 60 )) + version="$(next_policy_version)" + audit_link connect Connecting "$source" "$target" "$protocol" "$port" "$version" tmp="$(mktemp)" if [[ -f "$LINKS_FILE" ]]; then awk -v source="$source" -v target="$target" -v protocol="$protocol" -v port="$port" \ '!($1 == source && $2 == target && $4 == protocol && $5 == port)' "$LINKS_FILE" > "$tmp" fi - printf '%s %s %s %s %s %s\n' "$source" "$target" "$cidr" "$protocol" "$port" "$expires" >> "$tmp" + printf '%s %s %s %s %s %s %s\n' "$source" "$target" "$cidr" "$protocol" "$port" "$expires" "$version" >> "$tmp" sort -u -o "$tmp" "$tmp" install -m 0600 -o root -g root "$tmp" "$LINKS_FILE" rm -f -- "$tmp" - restore_firewall + if ! restore_firewall; then + audit_link connect Failed "$source" "$target" "$protocol" "$port" "$version" + die "Не удалось применить nftables policy" + fi + audit_link connect Active "$source" "$target" "$protocol" "$port" "$version" log "Связь $source → $target: $protocol/$port включена" + printf 'LINK_STATE=Active|VERSION=%s\n' "$version" } link_disconnect() { - local source="$1" target="$2" protocol="$3" port="$4" tmp + local source="$1" target="$2" protocol="$3" port="$4" version tmp valid_name "$source" && valid_name "$target" || die "Некорректное имя узла" [[ "$protocol" == tcp || "$protocol" == udp ]] || die "Протокол должен быть tcp или udp" [[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )) || die "Некорректный порт" + version="$(next_policy_version)" + audit_link disconnect Disconnecting "$source" "$target" "$protocol" "$port" "$version" tmp="$(mktemp)" if [[ -f "$LINKS_FILE" ]]; then awk -v source="$source" -v target="$target" -v protocol="$protocol" -v port="$port" \ @@ -461,8 +503,13 @@ link_disconnect() { fi install -m 0600 -o root -g root "$tmp" "$LINKS_FILE" rm -f -- "$tmp" - restore_firewall + if ! restore_firewall; then + audit_link disconnect Failed "$source" "$target" "$protocol" "$port" "$version" + die "Не удалось применить отключение в nftables" + fi + audit_link disconnect Disabled "$source" "$target" "$protocol" "$port" "$version" log "Связь $source → $target: $protocol/$port отключена" + printf 'LINK_STATE=Disabled|VERSION=%s\n' "$version" } remove_node() { @@ -504,13 +551,13 @@ list_nodes() { } list_links() { - local source target cidr protocol port expires + local source target cidr protocol port expires version prune_links [[ -f "$LINKS_FILE" ]] || return 0 - while read -r source target cidr protocol port expires; do + while read -r source target cidr protocol port expires version; do [[ -n "$source" && -n "$target" ]] || continue - printf 'LINK=%s|%s|%s|%s|%s|%s|enabled\n' \ - "$source" "$target" "$cidr" "$protocol" "$port" "$expires" + printf 'LINK=%s|%s|%s|%s|%s|%s|Active|%s\n' \ + "$source" "$target" "$cidr" "$protocol" "$port" "$expires" "$version" done < "$LINKS_FILE" } @@ -637,6 +684,10 @@ install_hub() { chmod 0600 "$HUB_CONFIG" touch "$LINKS_FILE" chmod 0600 "$LINKS_FILE" + touch "${MESH_DIR}/audit.jsonl" + chmod 0600 "${MESH_DIR}/audit.jsonl" + [[ -f "${MESH_DIR}/policy.version" ]] || printf '0\n' > "${MESH_DIR}/policy.version" + chmod 0600 "${MESH_DIR}/policy.version" create_hub_helper "$HUB_HELPER" render "$HUB_HELPER" firewall-restore diff --git a/tests/test-monitor-manager-links.sh b/tests/test-monitor-manager-links.sh index 0a98698..5a1c411 100644 --- a/tests/test-monitor-manager-links.sh +++ b/tests/test-monitor-manager-links.sh @@ -58,14 +58,17 @@ printf 'legacy-policy home\n' > "$state/links" PATH="$mock_bin:$PATH" NFT_CAPTURE="$nft_capture" \ "$helper" link-connect ai-agent home tcp 2222 120 -read -r source target cidr protocol port expires < "$state/links" +read -r source target cidr protocol port expires version < "$state/links" [[ "$source" == ai-agent ]] [[ "$target" == home ]] [[ "$cidr" == 10.77.0.3/32 ]] [[ "$protocol" == tcp ]] [[ "$port" == 2222 ]] (( expires > $(date +%s) )) +[[ "$version" =~ ^[0-9]+$ ]] grep -Fq 'ip saddr 10.77.0.2 ip daddr 10.77.0.3/32 tcp dport 2222 accept' "$nft_capture" +grep -Fq '"state":"Connecting"' "$state/audit.jsonl" +grep -Fq '"state":"Active"' "$state/audit.jsonl" if PATH="$mock_bin:$PATH" NFT_CAPTURE="$nft_capture" \ "$helper" link-connect ai-agent home tcp 70000 120 >/dev/null 2>&1; then @@ -73,13 +76,16 @@ if PATH="$mock_bin:$PATH" NFT_CAPTURE="$nft_capture" \ exit 1 fi -printf 'ai-agent home 10.77.0.3/32 udp 53 1\n' >> "$state/links" +printf 'ai-agent home 10.77.0.3/32 udp 53 1 99\n' >> "$state/links" PATH="$mock_bin:$PATH" NFT_CAPTURE="$nft_capture" "$helper" firewall-restore [[ "$(wc -l < "$state/links")" -eq 1 ]] PATH="$mock_bin:$PATH" NFT_CAPTURE="$nft_capture" \ "$helper" link-disconnect ai-agent home tcp 2222 [[ ! -s "$state/links" ]] +grep -Fq '"state":"Expired"' "$state/audit.jsonl" +grep -Fq '"state":"Disconnecting"' "$state/audit.jsonl" +grep -Fq '"state":"Disabled"' "$state/audit.jsonl" if grep -Fq 'dport 2222 accept' "$nft_capture"; then echo 'Disconnected policy remained in nftables.' >&2 exit 1