implement policy system

This commit is contained in:
Nuno Duque Nunes 2026-05-21 02:16:32 +00:00
parent de1a44a7e4
commit 92d829e184
9 changed files with 1120 additions and 40 deletions

View file

@ -1,3 +1,5 @@
#!/usr/bin/env bash
# ============================================ # ============================================
# Lifecycle # Lifecycle
# ============================================ # ============================================
@ -5,6 +7,7 @@
function cmd::add::on_load() { function cmd::add::on_load() {
load_module subnet load_module subnet
load_module identity load_module identity
load_module policy
flag::register --name flag::register --name
flag::register --identity flag::register --identity
@ -40,8 +43,8 @@ Options:
--type <type> Device type: desktop, laptop, phone, tablet, server, iot --type <type> Device type: desktop, laptop, phone, tablet, server, iot
--subnet <subnet> Subnet to allocate from (default: type-native) --subnet <subnet> Subnet to allocate from (default: type-native)
--ip <ip> Override auto-assigned IP (optional) --ip <ip> Override auto-assigned IP (optional)
--tunnel <mode> Tunnel mode: split|full (default: from subnet) --tunnel <mode> Tunnel mode: split|full (overrides policy)
--rule <rule> Assign rule on creation (default: from subnet or user) --rule <rule> Peer rule (default: from policy default_rule or none)
--group <group> Add to group on creation (group must exist) --group <group> Add to group on creation (group must exist)
--show-qr Show the WireGuard config as a QR code after creation --show-qr Show the WireGuard config as a QR code after creation
@ -50,11 +53,11 @@ Subnet shorthands (equivalent to --subnet <name>):
Examples: Examples:
wgctl add --name nuno --type phone wgctl add --name nuno --type phone
wgctl add --identity nuno --type phone # auto-names phone-nuno (or phone-nuno-2) wgctl add --identity nuno --type phone
wgctl add --identity nuno --type laptop
wgctl add --name zephyr --type desktop --guests wgctl add --name zephyr --type desktop --guests
wgctl add --identity zephyr --type desktop --guests wgctl add --identity zephyr --type desktop --guests
wgctl add --name visitor --type phone --guests --show-qr wgctl add --name visitor --type phone --guests --show-qr
wgctl add --name dev --type laptop --rule dev-01
EOF EOF
} }
@ -165,16 +168,25 @@ function cmd::add::run() {
resolved_cidr=$(subnet::resolve_for_add "$type" "$subnet_name") || return 1 resolved_cidr=$(subnet::resolve_for_add "$type" "$subnet_name") || return 1
resolved_type=$(subnet::type_for_add "$type" "$subnet_name") || return 1 resolved_type=$(subnet::type_for_add "$type" "$subnet_name") || return 1
# Resolve tunnel mode # Resolve effective policy
[[ -z "$tunnel" ]] && tunnel=$(subnet::tunnel_mode "${subnet_name:-$type}" "$type") local identity_name="${identity:-$(identity::get_name "$full_name")}"
local effective_policy
effective_policy=$(policy::effective "$subnet_name" "$resolved_type" "$identity_name")
# Resolve rule — subnet default_rule, then global default # Resolve tunnel mode — flag overrides policy
if [[ -z "$rule" ]]; then if [[ -z "$tunnel" ]]; then
rule=$(subnet::default_rule "$subnet_name" "$resolved_type") tunnel=$(policy::tunnel_mode "$effective_policy")
[[ -z "$rule" ]] && rule="user"
fi fi
rule::exists "$rule" || { log::error "Rule not found: ${rule}"; return 1; } # Resolve peer rule — explicit flag overrides policy default_rule
if [[ -z "$rule" ]]; then
rule=$(policy::default_rule "$effective_policy")
fi
# Validate rule if set
if [[ -n "$rule" ]]; then
rule::exists "$rule" || { log::error "Rule not found: ${rule}"; return 1; }
fi
local allowed_ips local allowed_ips
allowed_ips=$(config::allowed_ips_for "$tunnel") || return 1 allowed_ips=$(config::allowed_ips_for "$tunnel") || return 1
@ -189,28 +201,37 @@ function cmd::add::run() {
fi fi
cmd::add::_log_plan "$full_name" "$type" "$resolved_type" \ cmd::add::_log_plan "$full_name" "$type" "$resolved_type" \
"$subnet_name" "$resolved_cidr" "$ip" "$tunnel" "$allowed_ips" "$rule" "$subnet_name" "$resolved_cidr" "$ip" "$tunnel" \
"$allowed_ips" "${rule:---}" "$effective_policy"
keys::generate_pair "$full_name" || return 1 keys::generate_pair "$full_name" || return 1
peers::create_client_config "$full_name" "$resolved_type" "$ip" "$allowed_ips" || return 1 peers::create_client_config "$full_name" "$resolved_type" "$ip" "$allowed_ips" || return 1
# Write meta # Write meta — type, subnet, rule (if set)
peers::set_meta "$full_name" "type" "$resolved_type" peers::set_meta "$full_name" "type" "$resolved_type"
peers::set_meta "$full_name" "rule" "$rule"
if [[ -n "$subnet_name" ]]; then if [[ -n "$subnet_name" ]]; then
peers::set_meta "$full_name" "subnet" "$subnet_name" peers::set_meta "$full_name" "subnet" "$subnet_name"
fi fi
if [[ -n "$rule" ]]; then
peers::set_meta "$full_name" "rule" "$rule"
fi
cmd::add::_assign_group "$full_name" "$group" cmd::add::_assign_group "$full_name" "$group"
local public_key local public_key
public_key=$(keys::public "$full_name") || return 1 public_key=$(keys::public "$full_name") || return 1
peers::add_to_server "$full_name" "$public_key" "$ip" || return 1 peers::add_to_server "$full_name" "$public_key" "$ip" || return 1
rule::apply "$rule" "$ip" || return 1
peers::reload || return 1
# Auto-attach to identity # Apply peer rule if set
if [[ -n "$rule" ]]; then
rule::apply "$rule" "$ip" "$full_name" || return 1
fi
# Auto-attach to identity and apply identity rule if set
identity::auto_attach "$full_name" "$resolved_type" identity::auto_attach "$full_name" "$resolved_type"
cmd::add::_apply_identity_rule "$full_name" "$ip" "$identity_name" "$effective_policy" "$rule"
peers::reload || return 1
log::wg_success "Client added: ${full_name} (${ip}) [${tunnel} tunnel]" log::wg_success "Client added: ${full_name} (${ip}) [${tunnel} tunnel]"
cmd::add::_show_result "$full_name" "$resolved_type" "$show_qr" cmd::add::_show_result "$full_name" "$resolved_type" "$show_qr"
@ -223,7 +244,7 @@ function cmd::add::run() {
function cmd::add::_log_plan() { function cmd::add::_log_plan() {
local full_name="${1:-}" type="${2:-}" resolved_type="${3:-}" \ local full_name="${1:-}" type="${2:-}" resolved_type="${3:-}" \
subnet_name="${4:-}" resolved_cidr="${5:-}" ip="${6:-}" \ subnet_name="${4:-}" resolved_cidr="${5:-}" ip="${6:-}" \
tunnel="${7:-}" allowed_ips="${8:-}" rule="${9:-}" tunnel="${7:-}" allowed_ips="${8:-}" rule="${9:-}" policy="${10:-}"
log::wg_add "Name: ${full_name}" log::wg_add "Name: ${full_name}"
log::wg_add "Type: ${resolved_type}" log::wg_add "Type: ${resolved_type}"
@ -232,6 +253,7 @@ function cmd::add::_log_plan() {
log::wg_add "Tunnel: ${tunnel} (${allowed_ips})" log::wg_add "Tunnel: ${tunnel} (${allowed_ips})"
log::wg_add "Endpoint: $(config::endpoint)" log::wg_add "Endpoint: $(config::endpoint)"
log::wg_add "Rule: ${rule}" log::wg_add "Rule: ${rule}"
log::wg_add "Policy: ${policy}"
} }
function cmd::add::_assign_group() { function cmd::add::_assign_group() {
@ -244,6 +266,34 @@ function cmd::add::_assign_group() {
group::add_peer "$group" "$full_name" group::add_peer "$group" "$full_name"
log::wg "Added to group: ${group}" log::wg "Added to group: ${group}"
} }
function cmd::add::_apply_identity_rule() {
local full_name="${1:-}" ip="${2:-}" identity_name="${3:-}" \
effective_policy="${4:-}" peer_rule="${5:-}"
[[ -z "$identity_name" ]] && return 0
local identity_rule
identity_rule=$(identity::rule "$identity_name") || true
[[ -z "$identity_rule" ]] && {
# No identity rule — warn if no peer rule either
if [[ -z "$peer_rule" ]]; then
policy::warn_no_rule "$full_name"
fi
return 0
}
# Apply identity rule
rule::exists "$identity_rule" && rule::apply "$identity_rule" "$ip" "$full_name" || true
# Warn based on strict_rule
if policy::strict_rule "$effective_policy"; then
policy::warn_strict_rule "$identity_name" "$effective_policy" "$identity_rule"
elif [[ -n "$peer_rule" && "$peer_rule" != "$identity_rule" ]]; then
policy::warn_additive_rule "$identity_name" "$identity_rule" "$peer_rule"
fi
}
function cmd::add::_show_result() { function cmd::add::_show_result() {
local full_name="${1:-}" type="${2:-}" show_qr="${3:-false}" local full_name="${1:-}" type="${2:-}" show_qr="${3:-false}"

View file

@ -13,10 +13,23 @@
# ============================================ # ============================================
function cmd::identity::on_load() { function cmd::identity::on_load() {
load_module identity
load_module policy
flag::register --name flag::register --name
flag::register --peer flag::register --peer
flag::register --dry-run flag::register --dry-run
flag::register --force flag::register --force
# rule subcommand flags
flag::register --rule
# options subcommand flags
flag::register --policy
flag::register --set-strict-rule
flag::register --unset-strict-rule
flag::register --set-auto-apply
flag::register --unset-auto-apply
flag::register --field
flag::register --value
} }
# ============================================ # ============================================
@ -26,9 +39,9 @@ function cmd::identity::on_load() {
function cmd::identity::help() { function cmd::identity::help() {
cat <<EOF cat <<EOF
Usage: wgctl identity <subcommand> [options] Usage: wgctl identity <subcommand> [options]
Manage peer identities. Manage peer identities.
Subcommands: Subcommands:
list List all identities list List all identities
show --name <name> Show identity details and device status show --name <name> Show identity details and device status
@ -36,14 +49,24 @@ Subcommands:
--peer <peer> --peer <peer>
remove --name <name> Remove identity and all associated peers remove --name <name> Remove identity and all associated peers
migrate [--dry-run] Create identities from existing peer names migrate [--dry-run] Create identities from existing peer names
rule assign --name <name> Assign a rule to an identity
--rule <rule>
rule unassign --name <name> Remove rule from an identity
rule show --name <name> Show current identity rule
options --name <name> Set identity options
[--policy <policy>]
[--set-strict-rule | --unset-strict-rule]
[--set-auto-apply | --unset-auto-apply]
Examples: Examples:
wgctl identity list wgctl identity list
wgctl identity show --name nuno wgctl identity show --name nuno
wgctl identity add --name nuno --peer roboclean wgctl identity rule assign --name nuno --rule admin
wgctl identity remove --name zephyr wgctl identity rule unassign --name nuno
wgctl identity migrate --dry-run wgctl identity options --name guests-identity --policy guest
wgctl identity migrate wgctl identity options --name nuno --set-strict-rule
EOF EOF
} }
@ -61,9 +84,11 @@ function cmd::identity::run() {
add) cmd::identity::_add "$@" ;; add) cmd::identity::_add "$@" ;;
remove) cmd::identity::_remove "$@" ;; remove) cmd::identity::_remove "$@" ;;
migrate) cmd::identity::_migrate "$@" ;; migrate) cmd::identity::_migrate "$@" ;;
--help) cmd::identity::help ;; rule) cmd::identity::_rule "$@" ;;
options) cmd::identity::_options "$@" ;;
--help) cmd::identity::help ;;
*) *)
log::error "Unknown subcommand '${subcmd}'. Available: list, show, add, remove, migrate" log::error "Unknown subcommand '${subcmd}'. Available: list, show, add, remove, migrate, rule, options"
return 1 return 1
;; ;;
esac esac
@ -281,4 +306,158 @@ function cmd::identity::_migrate() {
done <<< "$output" done <<< "$output"
ui::identity::migrate_summary "$created" "$skipped" "$dry_run" ui::identity::migrate_summary "$created" "$skipped" "$dry_run"
}
function cmd::identity::_rule() {
local subcmd="${1:-show}"
shift || true
case "$subcmd" in
assign) cmd::identity::_rule_assign "$@" ;;
unassign) cmd::identity::_rule_unassign "$@" ;;
show) cmd::identity::_rule_show "$@" ;;
*)
log::error "Unknown rule subcommand '${subcmd}'. Available: assign, unassign, show"
return 1
;;
esac
}
function cmd::identity::_rule_assign() {
local name="" rule=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--rule) rule="$2"; shift 2 ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
[[ -z "$rule" ]] && { log::error "Missing required flag: --rule"; return 1; }
identity::require_exists "$name" || return 1
rule::exists "$rule" || { log::error "Rule not found: ${rule}"; return 1; }
identity::set_rule "$name" "$rule"
log::ok "Rule '${rule}' assigned to identity '${name}'"
# Reapply rules for all peers if auto_apply is set
local auto
auto=$(identity::rule_flags "$name" "auto_apply")
if [[ "$auto" != "false" ]]; then
log::info "Reapplying rules for all peers in identity '${name}'..."
identity::reapply_rules "$name"
log::ok "Rules reapplied"
fi
# Warn about strict_rule impact
if policy::strict_rule "$(identity::policy "$name")"; then
log::warn "Strict rule is enabled for identity '${name}' — peer rules will not be additive"
fi
}
function cmd::identity::_rule_unassign() {
local name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
identity::require_exists "$name" || return 1
local current_rule
current_rule=$(identity::rule "$name")
if [[ -z "$current_rule" ]]; then
log::warn "Identity '${name}' has no rule assigned"
return 0
fi
identity::clear_rule "$name"
log::ok "Rule removed from identity '${name}'"
log::info "Note: existing fw rules from '${current_rule}' are not automatically removed — run 'wgctl rule reapply' if needed"
}
function cmd::identity::_rule_show() {
local name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
identity::require_exists "$name" || return 1
local rule policy
rule=$(identity::rule "$name")
policy=$(identity::policy "$name")
local strict auto
strict=$(identity::rule_flags "$name" "strict_rule")
auto=$(identity::rule_flags "$name" "auto_apply")
echo ""
ui::row "Identity" "$name"
ui::row "Rule" "${rule:-}"
ui::row "Policy" "$policy"
ui::row "Strict rule" "$( [[ "$strict" == "true" ]] && echo "yes" || echo "no" )"
ui::row "Auto apply" "$( [[ "$auto" != "false" ]] && echo "yes" || echo "no" )"
echo ""
}
function cmd::identity::_options() {
local name="" new_policy=""
local set_strict="" set_auto=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--policy) new_policy="$2"; shift 2 ;;
--set-strict-rule) set_strict="true"; shift ;;
--unset-strict-rule) set_strict="false"; shift ;;
--set-auto-apply) set_auto="true"; shift ;;
--unset-auto-apply) set_auto="false"; shift ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
identity::require_exists "$name" || return 1
local changed=false
if [[ -n "$new_policy" ]]; then
policy::require_exists "$new_policy" || return 1
identity::set_policy "$name" "$new_policy"
log::ok "Policy set to '${new_policy}' for identity '${name}'"
changed=true
fi
if [[ -n "$set_strict" ]]; then
identity::set_rule_flag "$name" "strict_rule" "$set_strict"
if [[ "$set_strict" == "true" ]]; then
log::ok "Strict rule enabled for identity '${name}' — peer rules will not be additive"
else
log::ok "Strict rule disabled for identity '${name}' — peer rules will be additive"
fi
changed=true
fi
if [[ -n "$set_auto" ]]; then
identity::set_rule_flag "$name" "auto_apply" "$set_auto"
if [[ "$set_auto" == "true" ]]; then
log::ok "Auto apply enabled for identity '${name}'"
else
log::ok "Auto apply disabled for identity '${name}'"
fi
changed=true
fi
if ! $changed; then
cmd::identity::_rule_show --name "$name"
fi
} }

238
commands/policy.command.sh Normal file
View file

@ -0,0 +1,238 @@
#!/usr/bin/env bash
# policy.command.sh — manage policies
#
# Subcommands:
# wgctl policy list
# wgctl policy show --name <name>
# wgctl policy add --name <name> [--tunnel-mode split|full]
# [--default-rule <rule>] [--strict-rule] [--no-auto-apply]
# [--desc <desc>]
# wgctl policy rm --name <name>
# wgctl policy set --name <name> --field <field> --value <value>
# ============================================
# Lifecycle
# ============================================
function cmd::policy::on_load() {
load_module policy
flag::register --name
flag::register --tunnel-mode
flag::register --default-rule
flag::register --strict-rule
flag::register --no-strict-rule
flag::register --auto-apply
flag::register --no-auto-apply
flag::register --desc
flag::register --field
flag::register --value
}
# ============================================
# Help
# ============================================
function cmd::policy::help() {
cat <<EOF
Usage: wgctl policy <subcommand> [options]
Manage policies. Policies define behavioral flags for subnets and identities.
Subcommands:
list List all policies
show --name <name> Show policy details
add --name <name> Add a new policy
[--tunnel-mode split|full]
[--default-rule <rule>]
[--strict-rule]
[--no-auto-apply]
[--desc <desc>]
rm --name <name> Remove a policy (built-ins cannot be removed)
set --name <name> Set a single field on a policy
--field <field>
--value <value>
Fields:
tunnel_mode split|full
default_rule rule name (or empty to clear)
strict_rule true|false
auto_apply true|false
desc description string
Built-in policies (cannot be removed): default, guest, trusted, server, iot
Examples:
wgctl policy list
wgctl policy show --name guest
wgctl policy add --name contractor --default-rule contractor --strict-rule
wgctl policy set --name contractor --field tunnel-mode --value full
wgctl policy rm --name contractor
EOF
}
# ============================================
# Run
# ============================================
function cmd::policy::run() {
local subcmd="${1:-list}"
shift || true
case "$subcmd" in
list) cmd::policy::_list "$@" ;;
show) cmd::policy::_show "$@" ;;
add) cmd::policy::_add "$@" ;;
rm) cmd::policy::_rm "$@" ;;
set) cmd::policy::_set "$@" ;;
--help) cmd::policy::help ;;
*)
log::error "Unknown subcommand '${subcmd}'. Available: list, show, add, rm, set"
return 1
;;
esac
}
# ============================================
# Subcommands
# ============================================
function cmd::policy::_list() {
local data
data=$(policy::list_data)
if [[ -z "$data" ]]; then
log::info "No policies defined."
return 0
fi
printf " %-14s %-8s %-14s %-12s %-12s %s\n" \
"NAME" "TUNNEL" "DEFAULT RULE" "STRICT RULE" "AUTO APPLY" "DESCRIPTION"
ui::divider 84
while IFS='|' read -r name tunnel default_rule strict auto desc; do
local rule_display="${default_rule:-}"
local strict_display auto_display
[[ "$strict" == "true" ]] && strict_display="$(color::red yes)" || strict_display="no"
[[ "$auto" == "true" ]] && auto_display="yes" || auto_display="no"
printf " %-14s %-8s %-14s %-12s %-12s %s\n" \
"$name" "$tunnel" "$rule_display" "$strict_display" "$auto_display" "$desc"
done <<< "$data"
}
function cmd::policy::_show() {
local name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--help) cmd::policy::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
policy::require_exists "$name" || return 1
echo ""
ui::row "Name" "$name"
ui::row "Tunnel mode" "$(policy::tunnel_mode "$name")"
local dr
dr=$(policy::default_rule "$name")
ui::row "Default rule" "${dr:-}"
ui::row "Strict rule" "$(policy::strict_rule "$name" && echo "yes" || echo "no")"
ui::row "Auto apply" "$(policy::auto_apply "$name" && echo "yes" || echo "no")"
echo ""
}
function cmd::policy::_add() {
local name="" tunnel_mode="split" default_rule="" \
strict_rule="false" auto_apply="true" desc=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--tunnel-mode) tunnel_mode="$2"; shift 2 ;;
--default-rule) default_rule="$2"; shift 2 ;;
--strict-rule) strict_rule="true"; shift ;;
--no-strict-rule) strict_rule="false"; shift ;;
--no-auto-apply) auto_apply="false"; shift ;;
--auto-apply) auto_apply="true"; shift ;;
--desc) desc="$2"; shift 2 ;;
--help) cmd::policy::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
case "$tunnel_mode" in
split|full) ;;
*) log::error "Invalid --tunnel-mode '${tunnel_mode}'. Use: split, full"; return 1 ;;
esac
json::policy_add "$(ctx::policies)" "$name" "$tunnel_mode" \
"$default_rule" "$strict_rule" "$auto_apply" "$desc"
log::ok "Policy '${name}' added"
}
function cmd::policy::_rm() {
local name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--help) cmd::policy::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
policy::require_exists "$name" || return 1
json::policy_remove "$(ctx::policies)" "$name"
log::ok "Policy '${name}' removed"
}
function cmd::policy::_set() {
local name="" field="" value=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--field) field="$2"; shift 2 ;;
--value) value="$2"; shift 2 ;;
--help) cmd::policy::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
[[ -z "$field" ]] && { log::error "Missing required flag: --field"; return 1; }
[[ -z "$value" ]] && { log::error "Missing required flag: --value"; return 1; }
policy::require_exists "$name" || return 1
# Normalise field name (allow tunnel-mode as well as tunnel_mode)
field="${field//-/_}"
case "$field" in
tunnel_mode)
case "$value" in
split|full) ;;
*) log::error "Invalid value '${value}' for tunnel_mode. Use: split, full"; return 1 ;;
esac
;;
strict_rule|auto_apply)
case "$value" in
true|false) ;;
*) log::error "Invalid value '${value}' for ${field}. Use: true, false"; return 1 ;;
esac
;;
default_rule|desc) ;;
*)
log::error "Unknown field '${field}'. Valid: tunnel_mode, default_rule, strict_rule, auto_apply, desc"
return 1
;;
esac
json::policy_set_field "$(ctx::policies)" "$name" "$field" "$value"
log::ok "Policy '${name}': ${field} = ${value}"
}

View file

@ -60,6 +60,8 @@ function json::net_resolve() { python3 "$JSON_HELPER" net_resolve
function json::net_reverse_lookup() { python3 "$JSON_HELPER" net_reverse_lookup "$@" </dev/null; } function json::net_reverse_lookup() { python3 "$JSON_HELPER" net_reverse_lookup "$@" </dev/null; }
function json::block_is_empty() { python3 "$JSON_HELPER" block_is_empty "$@" </dev/null; } function json::block_is_empty() { python3 "$JSON_HELPER" block_is_empty "$@" </dev/null; }
function json::group_has_peer() { python3 "$JSON_HELPER" group_has_peer "$@" </dev/null; } function json::group_has_peer() { python3 "$JSON_HELPER" group_has_peer "$@" </dev/null; }
function json::get_nested() { python3 "$JSON_HELPER" get_nested "$@" </dev/null; }
function json::set_nested() { python3 "$JSON_HELPER" set_nested "$@" </dev/null; }
# Subnet wrappers # Subnet wrappers
function json::subnet_lookup() { python3 "$JSON_HELPER" subnet_lookup "$@" </dev/null; } function json::subnet_lookup() { python3 "$JSON_HELPER" subnet_lookup "$@" </dev/null; }
@ -87,7 +89,15 @@ function json::identity_peers() { python3 "$JSON_HELPER" identity_peers
function json::identity_migrate() { python3 "$JSON_HELPER" identity_migrate "$@" </dev/null; } function json::identity_migrate() { python3 "$JSON_HELPER" identity_migrate "$@" </dev/null; }
function json::identity_infer() { python3 "$JSON_HELPER" identity_infer "$@" </dev/null; } function json::identity_infer() { python3 "$JSON_HELPER" identity_infer "$@" </dev/null; }
function json::identity_exists() { python3 "$JSON_HELPER" identity_exists "$@" </dev/null; } function json::identity_exists() { python3 "$JSON_HELPER" identity_exists "$@" </dev/null; }
# Policy wrappers — append to json.sh
function json::policy_get() { python3 "$JSON_HELPER" policy_get "$@" </dev/null; }
function json::policy_list() { python3 "$JSON_HELPER" policy_list "$@" </dev/null; }
function json::policy_exists() { python3 "$JSON_HELPER" policy_exists "$@" </dev/null; }
function json::policy_add() { python3 "$JSON_HELPER" policy_add "$@" </dev/null; }
function json::policy_remove() { python3 "$JSON_HELPER" policy_remove "$@" </dev/null; }
function json::policy_set_field() { python3 "$JSON_HELPER" policy_set_field "$@" </dev/null; }
function json::subnet_policy() { python3 "$JSON_HELPER" subnet_policy "$@" </dev/null; }
function json::peer_transfer() { function json::peer_transfer() {
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \ ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \

View file

@ -2047,6 +2047,251 @@ def subnet_list_names(file):
for name in data.keys(): for name in data.keys():
print(name) print(name)
# ============================================
# Policy System
# ============================================
_POLICY_DEFAULTS = {
"default": {
"tunnel_mode": "split",
"default_rule": None,
"strict_rule": False,
"auto_apply": True,
"desc": "Default policy"
},
"guest": {
"tunnel_mode": "split",
"default_rule": "guest",
"strict_rule": True,
"auto_apply": True,
"desc": "Guest access policy"
},
"trusted": {
"tunnel_mode": "split",
"default_rule": None,
"strict_rule": False,
"auto_apply": True,
"desc": "Trusted device policy"
},
"server": {
"tunnel_mode": "split",
"default_rule": None,
"strict_rule": False,
"auto_apply": True,
"desc": "Server policy"
},
"iot": {
"tunnel_mode": "split",
"default_rule": None,
"strict_rule": False,
"auto_apply": True,
"desc": "IoT device policy"
}
}
def _policy_read(file):
"""Read policies.json, fall back to hardcoded defaults if missing."""
try:
if not os.path.exists(file):
return dict(_POLICY_DEFAULTS)
with open(file) as f:
content = f.read().strip()
if not content:
return dict(_POLICY_DEFAULTS)
data = json.loads(content)
# Merge with defaults so hardcoded policies always exist
merged = dict(_POLICY_DEFAULTS)
merged.update(data)
return merged
except Exception:
return dict(_POLICY_DEFAULTS)
def _policy_write(file, data):
"""Write policies.json."""
os.makedirs(os.path.dirname(file), exist_ok=True)
with open(file, 'w') as f:
json.dump(data, f, indent=2)
def _policy_get_entry(data, name):
"""Get a policy entry, falling back to 'default' policy values for missing fields."""
entry = data.get(name, {})
default = data.get('default', _POLICY_DEFAULTS.get('default', {}))
# Merge: entry fields override default
resolved = dict(default)
resolved.update(entry)
return resolved
def policy_get(file, name, field=''):
"""
Get a policy entry or a specific field from it.
policy_get(file, "guest") -> prints all fields as key|value lines
policy_get(file, "guest", "tunnel_mode") -> prints "split"
policy_get(file, "guest", "strict_rule") -> prints "true" or "false"
"""
data = _policy_read(file)
entry = _policy_get_entry(data, name)
if field:
val = entry.get(field)
if val is None:
print('')
elif isinstance(val, bool):
print('true' if val else 'false')
else:
print(val)
else:
for k, v in entry.items():
if isinstance(v, bool):
print(f"{k}|{'true' if v else 'false'}")
elif v is None:
print(f"{k}|")
else:
print(f"{k}|{v}")
def policy_list(file):
"""
List all policies.
Output per line: name|tunnel_mode|default_rule|strict_rule|auto_apply|desc
"""
data = _policy_read(file)
for name, raw_entry in data.items():
entry = _policy_get_entry(data, name)
tunnel_mode = entry.get('tunnel_mode', 'split')
default_rule = entry.get('default_rule') or ''
strict_rule = 'true' if entry.get('strict_rule', False) else 'false'
auto_apply = 'true' if entry.get('auto_apply', True) else 'false'
desc = entry.get('desc', '')
print(f"{name}|{tunnel_mode}|{default_rule}|{strict_rule}|{auto_apply}|{desc}")
def policy_exists(file, name):
"""Exit 0 if policy exists, 1 otherwise."""
data = _policy_read(file)
sys.exit(0 if name in data else 1)
def policy_add(file, name, tunnel_mode, default_rule, strict_rule, auto_apply, desc):
"""Add or update a policy entry."""
data = _policy_read(file)
data[name] = {
'tunnel_mode': tunnel_mode or 'split',
'default_rule': default_rule if default_rule else None,
'strict_rule': strict_rule == 'true',
'auto_apply': auto_apply != 'false',
'desc': desc or ''
}
_policy_write(file, data)
def policy_remove(file, name):
"""Remove a policy. Refuses to remove hardcoded defaults."""
if name in _POLICY_DEFAULTS:
print(f"Error: Cannot remove built-in policy '{name}'", file=sys.stderr)
sys.exit(1)
data = _policy_read(file)
if name not in data:
print(f"Error: Policy '{name}' not found", file=sys.stderr)
sys.exit(1)
del data[name]
_policy_write(file, data)
def policy_set_field(file, name, field, value):
"""Set a single field on an existing policy."""
data = _policy_read(file)
if name not in data:
print(f"Error: Policy '{name}' not found", file=sys.stderr)
sys.exit(1)
entry = data[name]
if field in ('strict_rule', 'auto_apply'):
entry[field] = value == 'true'
elif field == 'default_rule':
entry[field] = value if value else None
else:
entry[field] = value
data[name] = entry
_policy_write(file, data)
def subnet_policy(subnets_file, subnet_name, type_key=''):
"""
Get the policy name for a subnet entry.
Falls back to 'default' if no policy set.
"""
data = _subnet_read(subnets_file)
if subnet_name not in data:
print('default')
return
entry = data[subnet_name]
if _subnet_is_group(entry):
key = type_key if type_key else 'none'
child = entry.get(key, {})
print(child.get('policy', 'default'))
else:
print(entry.get('policy', 'default'))
def json_get_nested(file, *keys):
"""
Get a nested field from a JSON file.
json_get_nested(file, "rule_flags", "strict_rule")
Output: the value as a string, or empty string if not found.
"""
try:
with open(file) as f:
data = json.load(f)
val = data
for key in keys:
if not isinstance(val, dict):
print('')
return
val = val.get(key)
if val is None:
print('')
return
if isinstance(val, bool):
print('true' if val else 'false')
elif val is None:
print('')
else:
print(val)
except Exception:
print('')
def json_set_nested(file, *args):
"""
Set a nested field in a JSON file.
Args: file, key1, key2, ..., value
json_set_nested(file, "rule_flags", "strict_rule", "true")
Creates intermediate dicts as needed.
"""
if len(args) < 2:
return
keys = args[:-1]
value = args[-1]
try:
if os.path.exists(file):
with open(file) as f:
data = json.load(f)
else:
data = {}
# Navigate/create nested structure
target = data
for key in keys[:-1]:
if key not in target or not isinstance(target[key], dict):
target[key] = {}
target = target[key]
# Coerce value types
final_key = keys[-1]
if value == 'true':
target[final_key] = True
elif value == 'false':
target[final_key] = False
else:
target[final_key] = value
with open(file, 'w') as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
commands = { commands = {
'get': lambda args: get(args[0], args[1]), 'get': lambda args: get(args[0], args[1]),
@ -2069,7 +2314,7 @@ commands = {
'audit_fw_counts': lambda args: audit_fw_counts(args[0]), 'audit_fw_counts': lambda args: audit_fw_counts(args[0]),
'peer_group_map': lambda args: peer_group_map(args[0]), 'peer_group_map': lambda args: peer_group_map(args[0]),
'peer_groups': lambda args: peer_groups(args[0], args[1]), 'peer_groups': lambda args: peer_groups(args[0], args[1]),
'peer_data': lambda args: peer_data(args[0], args[1], args[2]), 'peer_data': lambda args: peer_data(args[0], args[1], args[2]),
'iso_to_ts': lambda args: iso_to_ts(args[0]), 'iso_to_ts': lambda args: iso_to_ts(args[0]),
'rule_list_data': lambda args: rule_list_data(args[0], args[1]), 'rule_list_data': lambda args: rule_list_data(args[0], args[1]),
'group_list_data': lambda args: group_list_data(args[0], args[1]), 'group_list_data': lambda args: group_list_data(args[0], args[1]),
@ -2170,6 +2415,17 @@ commands = {
'identity_exists': lambda args: identity_exists(args[0]), 'identity_exists': lambda args: identity_exists(args[0]),
'subnet_default_rule': lambda args: subnet_default_rule(args[0], args[1], args[2] if len(args) > 2 else ''), 'subnet_default_rule': lambda args: subnet_default_rule(args[0], args[1], args[2] if len(args) > 2 else ''),
'subnet_list_names': lambda args: subnet_list_names(args[0]), 'subnet_list_names': lambda args: subnet_list_names(args[0]),
# Policy commands:
'policy_get': lambda args: policy_get(args[0], args[1], args[2] if len(args) > 2 else ''),
'policy_list': lambda args: policy_list(args[0]),
'policy_exists': lambda args: policy_exists(args[0], args[1]),
'policy_add': lambda args: policy_add(args[0], args[1], args[2], args[3], args[4], args[5], args[6] if len(args) > 6 else ''),
'policy_remove': lambda args: policy_remove(args[0], args[1]),
'policy_set_field': lambda args: policy_set_field(args[0], args[1], args[2], args[3]),
'subnet_policy': lambda args: subnet_policy(args[0], args[1], args[2] if len(args) > 2 else ''),
'get_nested': lambda args: json_get_nested(args[0], *args[1:]),
'set_nested': lambda args: json_set_nested(args[0], *args[1:]),
} }
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -253,4 +253,124 @@ function identity::rename_peer() {
rm -f "$id_file" rm -f "$id_file"
fi fi
fi fi
}
# identity::rule <identity_name>
# Returns the rule assigned to an identity, or empty string if none.
function identity::rule() {
local identity_name="${1:-}"
local id_file
id_file=$(ctx::identity::path "${identity_name}.identity")
[[ ! -f "$id_file" ]] && return 0
json::get "$id_file" "rule" 2>/dev/null || true
}
# identity::set_rule <identity_name> <rule_name>
# Sets the rule on an identity file.
function identity::set_rule() {
local identity_name="${1:-}" rule_name="${2:-}"
local id_file
id_file=$(ctx::identity::path "${identity_name}.identity")
if [[ ! -f "$id_file" ]]; then
log::error "Identity '${identity_name}' not found"
return 1
fi
json::set "$id_file" "rule" "$rule_name"
}
# identity::clear_rule <identity_name>
# Removes the rule from an identity file.
function identity::clear_rule() {
local identity_name="${1:-}"
local id_file
id_file=$(ctx::identity::path "${identity_name}.identity")
[[ ! -f "$id_file" ]] && return 0
json::delete "$id_file" "rule" 2>/dev/null || true
}
# identity::policy <identity_name>
# Returns the policy name assigned to an identity, or "default".
function identity::policy() {
local identity_name="${1:-}"
local id_file
id_file=$(ctx::identity::path "${identity_name}.identity")
[[ ! -f "$id_file" ]] && echo "default" && return 0
local result
result=$(json::get "$id_file" "policy" 2>/dev/null) || true
echo "${result:-default}"
}
# identity::set_policy <identity_name> <policy_name>
# Sets the policy on an identity file.
function identity::set_policy() {
local identity_name="${1:-}" policy_name="${2:-}"
local id_file
id_file=$(ctx::identity::path "${identity_name}.identity")
if [[ ! -f "$id_file" ]]; then
log::error "Identity '${identity_name}' not found"
return 1
fi
json::set "$id_file" "policy" "$policy_name"
}
# identity::rule_flags <identity_name> <flag>
# Returns a specific rule_flag from the identity file.
# Falls back to the policy's value if not explicitly set on the identity.
function identity::rule_flags() {
local identity_name="${1:-}" flag="${2:-}"
local id_file
id_file=$(ctx::identity::path "${identity_name}.identity")
local result
result=$(json::get_nested "$id_file" "rule_flags" "$flag" 2>/dev/null) || true
if [[ -n "$result" ]]; then
echo "$result"
return 0
fi
# Fall back to policy value
local policy_name
policy_name=$(identity::policy "$identity_name")
policy::get "$policy_name" "$flag"
}
# identity::set_rule_flag <identity_name> <flag> <value>
# Sets a rule_flag directly on the identity file.
function identity::set_rule_flag() {
local identity_name="${1:-}" flag="${2:-}" value="${3:-}"
local id_file
id_file=$(ctx::identity::path "${identity_name}.identity")
if [[ ! -f "$id_file" ]]; then
log::error "Identity '${identity_name}' not found"
return 1
fi
json::set_nested "$id_file" "rule_flags" "$flag" "$value"
}
# identity::reapply_rules <identity_name>
# Reapply the identity rule to all peers in this identity.
# Respects auto_apply flag — if false, does nothing.
function identity::reapply_rules() {
local identity_name="${1:-}"
# Check auto_apply
local auto
auto=$(identity::rule_flags "$identity_name" "auto_apply")
[[ "$auto" == "false" ]] && return 0
local identity_rule
identity_rule=$(identity::rule "$identity_name")
[[ -z "$identity_rule" ]] && return 0
local peers
peers=$(identity::peers "$identity_name")
[[ -z "$peers" ]] && return 0
while IFS= read -r peer_name; do
[[ -z "$peer_name" ]] && continue
local client_ip
client_ip=$(peers::get_ip "$peer_name") || continue
rule::full_restore_peer "$peer_name" "$client_ip"
done <<< "$peers"
} }

200
modules/policy.module.sh Normal file
View file

@ -0,0 +1,200 @@
#!/usr/bin/env bash
# policy.module.sh — policy system
# Policies define behavioral flags for subnets, identities, and future contexts.
# Chain: Subnet → Policy → Identity → Peer
# ======================================================
# Hardcoded Fallbacks
# Mirror of policies.json built-in policies.
# Used when policies.json lookup fails.
# ======================================================
declare -gA _POLICY_TUNNEL_MODE=(
[default]="split"
[guest]="split"
[trusted]="split"
[server]="split"
[iot]="split"
)
declare -gA _POLICY_DEFAULT_RULE=(
[default]=""
[guest]="guest"
[trusted]=""
[server]=""
[iot]=""
)
declare -gA _POLICY_STRICT_RULE=(
[default]="false"
[guest]="true"
[trusted]="false"
[server]="false"
[iot]="false"
)
declare -gA _POLICY_AUTO_APPLY=(
[default]="true"
[guest]="true"
[trusted]="true"
[server]="true"
[iot]="true"
)
function policy::_hardcoded_field() {
local name="${1:-}" field="${2:-}"
case "$field" in
tunnel_mode) echo "${_POLICY_TUNNEL_MODE[$name]:-split}" ;;
default_rule) echo "${_POLICY_DEFAULT_RULE[$name]:-}" ;;
strict_rule) echo "${_POLICY_STRICT_RULE[$name]:-false}" ;;
auto_apply) echo "${_POLICY_AUTO_APPLY[$name]:-true}" ;;
*) echo "" ;;
esac
}
# ======================================================
# Core Accessors
# ======================================================
function ctx::policies() { echo "${_CTX_DATA}/policies.json"; }
function policy::exists() {
local name="${1:-}"
json::policy_exists "$(ctx::policies)" "$name" 2>/dev/null
}
function policy::require_exists() {
local name="${1:-}"
if ! policy::exists "$name"; then
log::error "Policy '${name}' not found. Use 'wgctl policy list' to see available policies."
return 1
fi
}
function policy::get() {
local name="${1:-}" field="${2:-}"
local result
result=$(json::policy_get "$(ctx::policies)" "$name" "$field" 2>/dev/null) || true
if [[ -n "$result" ]]; then
echo "$result"
return 0
fi
# Fallback to hardcoded
[[ -n "$field" ]] && policy::_hardcoded_field "$name" "$field"
}
function policy::tunnel_mode() {
local name="${1:-default}"
policy::get "$name" "tunnel_mode"
}
function policy::default_rule() {
local name="${1:-default}"
policy::get "$name" "default_rule"
}
function policy::strict_rule() {
local name="${1:-default}"
local val
val=$(policy::get "$name" "strict_rule")
[[ "$val" == "true" ]]
}
function policy::auto_apply() {
local name="${1:-default}"
local val
val=$(policy::get "$name" "auto_apply")
[[ "$val" != "false" ]]
}
function policy::list_data() {
json::policy_list "$(ctx::policies)" 2>/dev/null || true
}
# ======================================================
# Subnet Policy Resolution
# ======================================================
# policy::for_subnet <subnet_name> [type_key]
# Returns the policy name for a subnet entry.
# Falls back to "default" if no policy set on the subnet.
function policy::for_subnet() {
local subnet_name="${1:-}" type_key="${2:-}"
local result
result=$(json::subnet_policy "$(ctx::subnets)" "$subnet_name" "$type_key" 2>/dev/null) || true
echo "${result:-default}"
}
# policy::resolve_for_add <subnet_name> [type_key]
# Returns the fully resolved policy for use during wgctl add.
# Output: policy_name
function policy::resolve_for_add() {
local subnet_name="${1:-}" type_key="${2:-}"
if [[ -z "$subnet_name" ]]; then
echo "default"
return 0
fi
policy::for_subnet "$subnet_name" "$type_key"
}
# ======================================================
# Identity Policy Resolution
# ======================================================
# policy::for_identity <identity_name>
# Returns the policy name stored in an identity file.
# Falls back to "default".
function policy::for_identity() {
local identity_name="${1:-}"
local id_file
id_file=$(ctx::identity::path "${identity_name}.identity")
local result
result=$(json::get "$id_file" "policy" 2>/dev/null) || true
echo "${result:-default}"
}
# policy::effective <subnet_name> <type_key> <identity_name>
# Returns the effective policy name for a peer being added.
# Priority: identity policy > subnet policy > default
function policy::effective() {
local subnet_name="${1:-}" type_key="${2:-}" identity_name="${3:-}"
# Identity policy takes precedence if explicitly set
if [[ -n "$identity_name" ]]; then
local id_policy
id_policy=$(policy::for_identity "$identity_name")
if [[ "$id_policy" != "default" ]]; then
echo "$id_policy"
return 0
fi
fi
# Subnet policy next
if [[ -n "$subnet_name" ]]; then
local subnet_policy
subnet_policy=$(policy::for_subnet "$subnet_name" "$type_key")
echo "$subnet_policy"
return 0
fi
echo "default"
}
# ======================================================
# Warning Helpers (called from add.command.sh)
# ======================================================
function policy::warn_strict_rule() {
local identity_name="${1:-}" policy_name="${2:-}" identity_rule="${3:-}"
log::warn "Identity '${identity_name}' has policy '${policy_name}' with strict rule enabled — peer rule will not be applied, '${identity_rule}' is the only active rule"
}
function policy::warn_additive_rule() {
local identity_name="${1:-}" identity_rule="${1:-}" peer_rule="${2:-}"
log::info "Identity '${identity_name}' has strict rule disabled — '${identity_rule}' and '${peer_rule}' will both apply"
}
function policy::warn_no_rule() {
local peer_name="${1:-}"
log::warn "'${peer_name}' has no rule assigned — peer has unrestricted access"
}

View file

@ -234,6 +234,26 @@ function rule::unapply() {
# Bulk Operations # Bulk Operations
# ============================================ # ============================================
function rule::_apply_identity_rule() {
local peer_name="${1:-}" client_ip="${2:-}"
local identity_name
identity_name=$(identity::get_name "$peer_name")
[[ -z "$identity_name" ]] && return 0
local identity_rule strict
identity_rule=$(identity::rule "$identity_name")
[[ -z "$identity_rule" ]] && return 0
strict=$(identity::rule_flags "$identity_name" "strict_rule")
if [[ "$strict" == "true" ]]; then
fw::flush_peer "$client_ip"
rule::apply "$identity_rule" "$client_ip" "$peer_name"
else
rule::apply "$identity_rule" "$client_ip" "$peer_name"
fi
}
# rule::full_restore_peer <peer_name> <client_ip> # rule::full_restore_peer <peer_name> <client_ip>
# Flush and fully restore all fw rules for a peer — rule rules + block rules. # Flush and fully restore all fw rules for a peer — rule rules + block rules.
# Use this instead of calling rule::apply + block::restore_rules_for separately # Use this instead of calling rule::apply + block::restore_rules_for separately
@ -244,10 +264,11 @@ function rule::full_restore_peer() {
fw::flush_peer "$client_ip" fw::flush_peer "$client_ip"
local rule_name local peer_rule
rule_name=$(peers::get_meta "$peer_name" "rule") peer_rule=$(peers::get_meta "$peer_name" "rule")
[[ -n "$rule_name" ]] && rule::apply "$rule_name" "$client_ip" "$peer_name" [[ -n "$peer_rule" ]] && rule::apply "$peer_rule" "$client_ip" "$peer_name"
rule::_apply_identity_rule "$peer_name" "$client_ip"
block::restore_rules_for "$peer_name" "$client_ip" block::restore_rules_for "$peer_name" "$client_ip"
} }

View file

@ -199,6 +199,11 @@ function subnet::_hardcoded_tunnel_mode() {
# Core Resolution # Core Resolution
# ====================================================== # ======================================================
function subnet::policy() {
local subnet_name="${1:-}" type_key="${2:-}"
policy::for_subnet "$subnet_name" "$type_key"
}
# subnet::lookup <subnet_name> [type_key] # subnet::lookup <subnet_name> [type_key]
# Returns the CIDR for a given subnet name and optional type. # Returns the CIDR for a given subnet name and optional type.
# Falls back to hardcoded map on failure. # Falls back to hardcoded map on failure.
@ -258,10 +263,9 @@ function subnet::type_for_add() {
# Returns "split" or "full" for the given subnet. # Returns "split" or "full" for the given subnet.
function subnet::tunnel_mode() { function subnet::tunnel_mode() {
local subnet_name="${1:-}" type_key="${2:-}" local subnet_name="${1:-}" type_key="${2:-}"
local result local policy_name
result=$(json::subnet_tunnel_mode "$(ctx::subnets)" "$subnet_name" "$type_key" 2>/dev/null) || true policy_name=$(policy::for_subnet "$subnet_name" "$type_key")
[[ -n "$result" ]] && { echo "$result"; return 0; } policy::tunnel_mode "$policy_name"
subnet::_hardcoded_tunnel_mode
} }
function subnet::type_from_ip() { function subnet::type_from_ip() {
@ -302,7 +306,9 @@ function subnet::name_from_ip() {
function subnet::default_rule() { function subnet::default_rule() {
local subnet_name="${1:-}" type_key="${2:-}" local subnet_name="${1:-}" type_key="${2:-}"
[[ -z "$subnet_name" ]] && return 0 [[ -z "$subnet_name" ]] && return 0
json::subnet_default_rule "$(ctx::subnets)" "$subnet_name" "$type_key" 2>/dev/null || true local policy_name
policy_name=$(policy::for_subnet "$subnet_name" "$type_key")
policy::default_rule "$policy_name"
} }
# subnet::list_names # subnet::list_names
@ -353,4 +359,4 @@ function subnet::list_data() {
function subnet::show_data() { function subnet::show_data() {
local name="${1:-}" local name="${1:-}"
json::subnet_show "$(ctx::subnets)" "$name" json::subnet_show "$(ctx::subnets)" "$name"
} }