implement policy system
This commit is contained in:
parent
de1a44a7e4
commit
92d829e184
9 changed files with 1120 additions and 40 deletions
|
|
@ -1,3 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# ============================================
|
||||
# Lifecycle
|
||||
# ============================================
|
||||
|
|
@ -5,6 +7,7 @@
|
|||
function cmd::add::on_load() {
|
||||
load_module subnet
|
||||
load_module identity
|
||||
load_module policy
|
||||
|
||||
flag::register --name
|
||||
flag::register --identity
|
||||
|
|
@ -40,8 +43,8 @@ Options:
|
|||
--type <type> Device type: desktop, laptop, phone, tablet, server, iot
|
||||
--subnet <subnet> Subnet to allocate from (default: type-native)
|
||||
--ip <ip> Override auto-assigned IP (optional)
|
||||
--tunnel <mode> Tunnel mode: split|full (default: from subnet)
|
||||
--rule <rule> Assign rule on creation (default: from subnet or user)
|
||||
--tunnel <mode> Tunnel mode: split|full (overrides policy)
|
||||
--rule <rule> Peer rule (default: from policy default_rule or none)
|
||||
--group <group> Add to group on creation (group must exist)
|
||||
--show-qr Show the WireGuard config as a QR code after creation
|
||||
|
||||
|
|
@ -50,11 +53,11 @@ Subnet shorthands (equivalent to --subnet <name>):
|
|||
|
||||
Examples:
|
||||
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 laptop
|
||||
wgctl add --identity nuno --type phone
|
||||
wgctl add --name zephyr --type desktop --guests
|
||||
wgctl add --identity zephyr --type desktop --guests
|
||||
wgctl add --name visitor --type phone --guests --show-qr
|
||||
wgctl add --name dev --type laptop --rule dev-01
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -165,16 +168,25 @@ function cmd::add::run() {
|
|||
resolved_cidr=$(subnet::resolve_for_add "$type" "$subnet_name") || return 1
|
||||
resolved_type=$(subnet::type_for_add "$type" "$subnet_name") || return 1
|
||||
|
||||
# Resolve tunnel mode
|
||||
[[ -z "$tunnel" ]] && tunnel=$(subnet::tunnel_mode "${subnet_name:-$type}" "$type")
|
||||
# Resolve effective policy
|
||||
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
|
||||
if [[ -z "$rule" ]]; then
|
||||
rule=$(subnet::default_rule "$subnet_name" "$resolved_type")
|
||||
[[ -z "$rule" ]] && rule="user"
|
||||
# Resolve tunnel mode — flag overrides policy
|
||||
if [[ -z "$tunnel" ]]; then
|
||||
tunnel=$(policy::tunnel_mode "$effective_policy")
|
||||
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
|
||||
allowed_ips=$(config::allowed_ips_for "$tunnel") || return 1
|
||||
|
|
@ -189,28 +201,37 @@ function cmd::add::run() {
|
|||
fi
|
||||
|
||||
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
|
||||
peers::create_client_config "$full_name" "$resolved_type" "$ip" "$allowed_ips" || return 1
|
||||
keys::generate_pair "$full_name" || 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" "rule" "$rule"
|
||||
if [[ -n "$subnet_name" ]]; then
|
||||
peers::set_meta "$full_name" "subnet" "$subnet_name"
|
||||
fi
|
||||
if [[ -n "$rule" ]]; then
|
||||
peers::set_meta "$full_name" "rule" "$rule"
|
||||
fi
|
||||
|
||||
cmd::add::_assign_group "$full_name" "$group"
|
||||
|
||||
local public_key
|
||||
public_key=$(keys::public "$full_name") || 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"
|
||||
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]"
|
||||
cmd::add::_show_result "$full_name" "$resolved_type" "$show_qr"
|
||||
|
|
@ -223,7 +244,7 @@ function cmd::add::run() {
|
|||
function cmd::add::_log_plan() {
|
||||
local full_name="${1:-}" type="${2:-}" resolved_type="${3:-}" \
|
||||
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 "Type: ${resolved_type}"
|
||||
|
|
@ -232,6 +253,7 @@ function cmd::add::_log_plan() {
|
|||
log::wg_add "Tunnel: ${tunnel} (${allowed_ips})"
|
||||
log::wg_add "Endpoint: $(config::endpoint)"
|
||||
log::wg_add "Rule: ${rule}"
|
||||
log::wg_add "Policy: ${policy}"
|
||||
}
|
||||
|
||||
function cmd::add::_assign_group() {
|
||||
|
|
@ -244,6 +266,34 @@ function cmd::add::_assign_group() {
|
|||
group::add_peer "$group" "$full_name"
|
||||
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() {
|
||||
local full_name="${1:-}" type="${2:-}" show_qr="${3:-false}"
|
||||
|
|
|
|||
|
|
@ -13,10 +13,23 @@
|
|||
# ============================================
|
||||
|
||||
function cmd::identity::on_load() {
|
||||
load_module identity
|
||||
load_module policy
|
||||
|
||||
flag::register --name
|
||||
flag::register --peer
|
||||
flag::register --dry-run
|
||||
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() {
|
||||
cat <<EOF
|
||||
Usage: wgctl identity <subcommand> [options]
|
||||
|
||||
|
||||
Manage peer identities.
|
||||
|
||||
|
||||
Subcommands:
|
||||
list List all identities
|
||||
show --name <name> Show identity details and device status
|
||||
|
|
@ -36,14 +49,24 @@ Subcommands:
|
|||
--peer <peer>
|
||||
remove --name <name> Remove identity and all associated peers
|
||||
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:
|
||||
wgctl identity list
|
||||
wgctl identity show --name nuno
|
||||
wgctl identity add --name nuno --peer roboclean
|
||||
wgctl identity remove --name zephyr
|
||||
wgctl identity migrate --dry-run
|
||||
wgctl identity migrate
|
||||
wgctl identity rule assign --name nuno --rule admin
|
||||
wgctl identity rule unassign --name nuno
|
||||
wgctl identity options --name guests-identity --policy guest
|
||||
wgctl identity options --name nuno --set-strict-rule
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -61,9 +84,11 @@ function cmd::identity::run() {
|
|||
add) cmd::identity::_add "$@" ;;
|
||||
remove) cmd::identity::_remove "$@" ;;
|
||||
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
|
||||
;;
|
||||
esac
|
||||
|
|
@ -281,4 +306,158 @@ function cmd::identity::_migrate() {
|
|||
done <<< "$output"
|
||||
|
||||
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
238
commands/policy.command.sh
Normal 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}"
|
||||
}
|
||||
12
core/json.sh
12
core/json.sh
|
|
@ -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::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::get_nested() { python3 "$JSON_HELPER" get_nested "$@" </dev/null; }
|
||||
function json::set_nested() { python3 "$JSON_HELPER" set_nested "$@" </dev/null; }
|
||||
|
||||
# Subnet wrappers
|
||||
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_infer() { python3 "$JSON_HELPER" identity_infer "$@" </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() {
|
||||
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \
|
||||
|
|
|
|||
|
|
@ -2047,6 +2047,251 @@ def subnet_list_names(file):
|
|||
for name in data.keys():
|
||||
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 = {
|
||||
'get': lambda args: get(args[0], args[1]),
|
||||
|
|
@ -2069,7 +2314,7 @@ commands = {
|
|||
'audit_fw_counts': lambda args: audit_fw_counts(args[0]),
|
||||
'peer_group_map': lambda args: peer_group_map(args[0]),
|
||||
'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]),
|
||||
'rule_list_data': lambda args: rule_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]),
|
||||
'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]),
|
||||
|
||||
# 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__':
|
||||
|
|
|
|||
|
|
@ -253,4 +253,124 @@ function identity::rename_peer() {
|
|||
rm -f "$id_file"
|
||||
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
200
modules/policy.module.sh
Normal 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"
|
||||
}
|
||||
|
|
@ -234,6 +234,26 @@ function rule::unapply() {
|
|||
# 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>
|
||||
# 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
|
||||
|
|
@ -244,10 +264,11 @@ function rule::full_restore_peer() {
|
|||
|
||||
fw::flush_peer "$client_ip"
|
||||
|
||||
local rule_name
|
||||
rule_name=$(peers::get_meta "$peer_name" "rule")
|
||||
[[ -n "$rule_name" ]] && rule::apply "$rule_name" "$client_ip" "$peer_name"
|
||||
local peer_rule
|
||||
peer_rule=$(peers::get_meta "$peer_name" "rule")
|
||||
[[ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -199,6 +199,11 @@ function subnet::_hardcoded_tunnel_mode() {
|
|||
# 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]
|
||||
# Returns the CIDR for a given subnet name and optional type.
|
||||
# Falls back to hardcoded map on failure.
|
||||
|
|
@ -258,10 +263,9 @@ function subnet::type_for_add() {
|
|||
# Returns "split" or "full" for the given subnet.
|
||||
function subnet::tunnel_mode() {
|
||||
local subnet_name="${1:-}" type_key="${2:-}"
|
||||
local result
|
||||
result=$(json::subnet_tunnel_mode "$(ctx::subnets)" "$subnet_name" "$type_key" 2>/dev/null) || true
|
||||
[[ -n "$result" ]] && { echo "$result"; return 0; }
|
||||
subnet::_hardcoded_tunnel_mode
|
||||
local policy_name
|
||||
policy_name=$(policy::for_subnet "$subnet_name" "$type_key")
|
||||
policy::tunnel_mode "$policy_name"
|
||||
}
|
||||
|
||||
function subnet::type_from_ip() {
|
||||
|
|
@ -302,7 +306,9 @@ function subnet::name_from_ip() {
|
|||
function subnet::default_rule() {
|
||||
local subnet_name="${1:-}" type_key="${2:-}"
|
||||
[[ -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
|
||||
|
|
@ -353,4 +359,4 @@ function subnet::list_data() {
|
|||
function subnet::show_data() {
|
||||
local name="${1:-}"
|
||||
json::subnet_show "$(ctx::subnets)" "$name"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue