Add fact-first Link reconciliation, duplicate and orphan cleanup, generation-aware scheduling, retention, Desktop drift visibility, and strict helper contracts. Preserve exact B-3R batching and lock-safe finalization with Linux/native trimmed evidence. Co-authored-by: Ochenstarik <ochenstarik@inbox.ru>
262 lines
9.5 KiB
Bash
Executable file
262 lines
9.5 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
IFS=$'\n\t'
|
|
export LC_ALL=C
|
|
umask 077
|
|
|
|
readonly TABLE_FAMILY="inet"
|
|
readonly TABLE_NAME="ochenstarik_smm"
|
|
readonly CHAIN_NAME="links"
|
|
|
|
fail() { printf '%s\n' "policy helper: $*" >&2; exit 78; }
|
|
firewall_unavailable() { printf '%s\n' "mesh.firewall-unavailable" >&2; exit 79; }
|
|
node_not_activated() { printf '%s\n' "mesh.node-not-activated" >&2; exit 80; }
|
|
|
|
testing="${SMM_POLICY_TESTING:-0}"
|
|
if [[ "$testing" != "1" ]]; then
|
|
[[ ${EUID:-$(id -u)} -eq 0 ]] || fail "root is required"
|
|
readonly STATE_FILE="/var/lib/ochenstarik-server-monitor-manager/mesh/nodes.tsv"
|
|
readonly RECONCILE_MARKER="/var/lib/ochenstarik-server-monitor-manager/mesh/reconcile-requested"
|
|
readonly RECONCILE_LOCK="/var/lib/ochenstarik-server-monitor-manager/mesh/reconcile-requested.lock"
|
|
readonly FLOCK_COMMAND="/usr/bin/flock"
|
|
else
|
|
readonly STATE_FILE="${SMM_POLICY_STATE_FILE:-/dev/null}"
|
|
readonly RECONCILE_MARKER="${SMM_POLICY_RECONCILE_MARKER:-${TMPDIR:-/tmp}/smm-policy-reconcile-requested}"
|
|
readonly RECONCILE_LOCK="${SMM_POLICY_RECONCILE_LOCK:-${RECONCILE_MARKER}.lock}"
|
|
readonly FLOCK_COMMAND="${SMM_POLICY_FLOCK:-/usr/bin/flock}"
|
|
fi
|
|
|
|
node_pattern='^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$'
|
|
ipv4_pattern='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
|
|
generation_pattern='^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
# Mesh node lifecycle statuses are lowercase tokens (for example active or reserved).
|
|
node_status_pattern='^[a-z][a-z0-9-]{0,31}$'
|
|
|
|
validate_node_id() {
|
|
[[ "$1" =~ $node_pattern ]] || fail "invalid node id"
|
|
}
|
|
|
|
is_valid_ipv4() {
|
|
local address="$1" octet
|
|
local -a octets
|
|
[[ "$address" =~ $ipv4_pattern ]] || return 1
|
|
IFS='.' read -r -a octets <<<"$address"
|
|
[[ ${#octets[@]} -eq 4 ]] || return 1
|
|
for octet in "${octets[@]}"; do
|
|
(( 10#$octet <= 255 )) || return 1
|
|
done
|
|
}
|
|
|
|
lookup_node_ip() {
|
|
local node_id="$1" record field_count ip status
|
|
[[ -r "$STATE_FILE" ]] || fail "mesh node state is unavailable"
|
|
record="$(awk -F '\t' -v node="$node_id" '$1 == node { print; exit }' "$STATE_FILE")"
|
|
[[ -n "$record" ]] || node_not_activated
|
|
field_count="$(awk -F '\t' '{ print NF }' <<<"$record")"
|
|
[[ "$field_count" == "4" ]] || fail "invalid mesh node record: $node_id"
|
|
ip="$(awk -F '\t' '{ print $2 }' <<<"$record")"
|
|
status="$(awk -F '\t' '{ print $4 }' <<<"$record")"
|
|
[[ "$status" =~ $node_status_pattern ]] || fail "invalid mesh node status: $node_id"
|
|
[[ "$status" == "active" ]] || node_not_activated
|
|
is_valid_ipv4 "$ip" || fail "node has no valid mesh address: $node_id"
|
|
printf '%s\n' "$ip"
|
|
}
|
|
|
|
validate_rule() {
|
|
local action="$1"
|
|
case "$action" in
|
|
link-connect) [[ $# -eq 6 ]] || fail "invalid link-connect argument count" ;;
|
|
link-disconnect) [[ $# -eq 5 ]] || fail "invalid link-disconnect argument count" ;;
|
|
link-status) [[ $# -eq 5 ]] || fail "invalid link-status argument count" ;;
|
|
*) fail "unsupported action" ;;
|
|
esac
|
|
validate_node_id "$2"
|
|
validate_node_id "$3"
|
|
[[ "$2" != "$3" ]] || fail "source and destination must differ"
|
|
[[ "$4" == "tcp" || "$4" == "udp" ]] || fail "invalid protocol"
|
|
[[ "$5" =~ ^[0-9]+$ ]] && (( 10#$5 >= 1 && 10#$5 <= 65535 )) || fail "invalid port"
|
|
if [[ "$action" == "link-connect" ]]; then
|
|
[[ "$6" =~ ^[0-9]+$ ]] && (( 10#$6 >= 0 && 10#$6 <= 525600 )) || fail "invalid TTL"
|
|
fi
|
|
}
|
|
|
|
run_nft() {
|
|
if [[ "$testing" == "1" ]]; then
|
|
printf 'nft'
|
|
printf ' %q' "$@"
|
|
printf '\n'
|
|
else
|
|
[[ -x /usr/sbin/nft ]] || fail "nft executable is missing: /usr/sbin/nft"
|
|
/usr/sbin/nft "$@"
|
|
fi
|
|
}
|
|
|
|
inspect_firewall() {
|
|
local listing
|
|
if [[ "$testing" == "1" ]]; then
|
|
[[ "${SMM_POLICY_FIREWALL_UNAVAILABLE:-0}" != "1" ]] || firewall_unavailable
|
|
[[ -z "${SMM_POLICY_FIREWALL_ERROR:-}" ]] \
|
|
|| fail "could not inspect nftables Link policy: $SMM_POLICY_FIREWALL_ERROR"
|
|
if [[ -n "${SMM_POLICY_LISTING_FILE:-}" ]]; then
|
|
[[ -r "$SMM_POLICY_LISTING_FILE" ]] || fail "test firewall listing is unavailable"
|
|
cat -- "$SMM_POLICY_LISTING_FILE"
|
|
fi
|
|
return 0
|
|
fi
|
|
[[ -x /usr/sbin/nft ]] || fail "nft executable is missing: /usr/sbin/nft"
|
|
if listing="$(/usr/sbin/nft -a list chain "$TABLE_FAMILY" "$TABLE_NAME" "$CHAIN_NAME" 2>&1)"; then
|
|
printf '%s\n' "$listing"
|
|
return 0
|
|
fi
|
|
if grep -Eiq 'No such file or directory|does not exist' <<<"$listing"; then
|
|
firewall_unavailable
|
|
fi
|
|
fail "could not inspect nftables Link policy: ${listing%%$'\n'*}"
|
|
}
|
|
|
|
ensure_firewall_available() {
|
|
inspect_firewall >/dev/null
|
|
}
|
|
|
|
rule_exists() {
|
|
local comment="$1" listing
|
|
listing="$(inspect_firewall)"
|
|
grep -Fq "comment \"$comment\"" <<<"$listing"
|
|
}
|
|
|
|
connect_rule() {
|
|
local source_id="$1" target_id="$2" protocol="$3" port="$4"
|
|
local source_ip target_ip comment
|
|
ensure_firewall_available
|
|
source_ip="$(lookup_node_ip "$source_id")"
|
|
target_ip="$(lookup_node_ip "$target_id")"
|
|
comment="smm:${source_id}:${target_id}:${protocol}:${port}"
|
|
rule_exists "$comment" && return 0
|
|
run_nft add rule "$TABLE_FAMILY" "$TABLE_NAME" "$CHAIN_NAME" \
|
|
ip saddr "$source_ip" ip daddr "$target_ip" "$protocol" dport "$port" \
|
|
counter accept comment "$comment"
|
|
}
|
|
|
|
disconnect_rule() {
|
|
local source_id="$1" target_id="$2" protocol="$3" port="$4" comment handle
|
|
ensure_firewall_available
|
|
comment="smm:${source_id}:${target_id}:${protocol}:${port}"
|
|
if [[ "$testing" == "1" ]]; then
|
|
printf 'nft-delete-comment %q\n' "$comment"
|
|
return
|
|
fi
|
|
while IFS= read -r handle; do
|
|
[[ "$handle" =~ ^[0-9]+$ ]] || continue
|
|
run_nft delete rule "$TABLE_FAMILY" "$TABLE_NAME" "$CHAIN_NAME" handle "$handle"
|
|
done < <(
|
|
/usr/sbin/nft -a list chain "$TABLE_FAMILY" "$TABLE_NAME" "$CHAIN_NAME" \
|
|
| grep -F "comment \"$comment\"" \
|
|
| sed -n 's/.* # handle \([0-9][0-9]*\)$/\1/p'
|
|
)
|
|
}
|
|
|
|
list_rules() {
|
|
local listing line comment source_id target_id protocol port invalid
|
|
local -a fields
|
|
listing="$(inspect_firewall)"
|
|
while IFS= read -r line; do
|
|
[[ "$line" =~ comment[[:space:]]+\"([^\"]*)\" ]] || continue
|
|
comment="${BASH_REMATCH[1]}"
|
|
[[ "$comment" == smm:* ]] || continue
|
|
IFS=':' read -r -a fields <<<"$comment"
|
|
invalid=0
|
|
if [[ ${#fields[@]} -ne 5 ]]; then
|
|
invalid=1
|
|
else
|
|
source_id="${fields[1]}"
|
|
target_id="${fields[2]}"
|
|
protocol="${fields[3]}"
|
|
port="${fields[4]}"
|
|
[[ "$source_id" =~ $node_pattern && "$target_id" =~ $node_pattern \
|
|
&& "$source_id" != "$target_id" ]] || invalid=1
|
|
[[ "$protocol" == "tcp" || "$protocol" == "udp" ]] || invalid=1
|
|
if [[ ! "$port" =~ ^[0-9]+$ ]] \
|
|
|| (( 10#$port < 1 || 10#$port > 65535 )); then
|
|
invalid=1
|
|
fi
|
|
fi
|
|
if (( invalid != 0 )); then
|
|
printf '%s\n' "policy helper: forged managed comment ignored: $comment" >&2
|
|
continue
|
|
fi
|
|
printf '%s\t%s\t%s\t%s\n' "$source_id" "$target_id" "$protocol" "$port"
|
|
done <<<"$listing"
|
|
}
|
|
|
|
status_rule() {
|
|
local source_id="$1" target_id="$2" protocol="$3" port="$4" comment
|
|
ensure_firewall_available
|
|
# A status is meaningful only for identities currently known to the mesh.
|
|
lookup_node_ip "$source_id" >/dev/null
|
|
lookup_node_ip "$target_id" >/dev/null
|
|
comment="smm:${source_id}:${target_id}:${protocol}:${port}"
|
|
if rule_exists "$comment"; then
|
|
printf '%s\n' active
|
|
else
|
|
printf '%s\n' disabled
|
|
fi
|
|
}
|
|
|
|
reconcile_status() {
|
|
local generation
|
|
if [[ ! -d "$(dirname -- "$RECONCILE_MARKER")" ]]; then
|
|
printf '%s\n' complete
|
|
return
|
|
fi
|
|
exec 9>"$RECONCILE_LOCK"
|
|
chmod 0600 "$RECONCILE_LOCK"
|
|
"$FLOCK_COMMAND" -x 9
|
|
if [[ ! -f "$RECONCILE_MARKER" ]]; then
|
|
printf '%s\n' complete
|
|
return
|
|
fi
|
|
IFS= read -r generation <"$RECONCILE_MARKER" || fail "invalid reconciliation marker"
|
|
[[ "$generation" =~ $generation_pattern ]] || fail "invalid reconciliation marker"
|
|
printf 'requested:%s\n' "$generation"
|
|
}
|
|
|
|
reconcile_complete() {
|
|
local expected_generation="$1" current_generation=''
|
|
[[ "$expected_generation" =~ $generation_pattern ]] || fail "invalid reconciliation generation"
|
|
exec 9>"$RECONCILE_LOCK"
|
|
chmod 0600 "$RECONCILE_LOCK"
|
|
"$FLOCK_COMMAND" -x 9
|
|
if [[ -f "$RECONCILE_MARKER" ]]; then
|
|
IFS= read -r current_generation <"$RECONCILE_MARKER" || fail "invalid reconciliation marker"
|
|
[[ "$current_generation" =~ $generation_pattern ]] || fail "invalid reconciliation marker"
|
|
if [[ "$current_generation" == "$expected_generation" ]]; then
|
|
rm -f -- "$RECONCILE_MARKER"
|
|
fi
|
|
fi
|
|
printf '%s\n' complete
|
|
}
|
|
|
|
action="${1:-}"
|
|
case "$action" in
|
|
link-list)
|
|
[[ $# -eq 1 ]] || fail "invalid link-list argument count"
|
|
list_rules
|
|
exit 0
|
|
;;
|
|
reconcile-status)
|
|
[[ $# -eq 1 ]] || fail "invalid reconcile-status argument count"
|
|
reconcile_status
|
|
exit 0
|
|
;;
|
|
reconcile-complete)
|
|
[[ $# -eq 2 ]] || fail "invalid reconcile-complete argument count"
|
|
reconcile_complete "$2"
|
|
exit 0
|
|
;;
|
|
esac
|
|
validate_rule "$@"
|
|
case "$action" in
|
|
link-connect) connect_rule "$2" "$3" "$4" "$5" ;;
|
|
link-disconnect) disconnect_rule "$2" "$3" "$4" "$5" ;;
|
|
link-status) status_rule "$2" "$3" "$4" "$5" ;;
|
|
esac
|