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
|
# 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
|
||||||
|
|
||||||
|
# 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; }
|
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() {
|
||||||
|
|
@ -245,6 +267,34 @@ function cmd::add::_assign_group() {
|
||||||
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}"
|
||||||
if $show_qr || cmd::add::is_mobile "$type"; then
|
if $show_qr || cmd::add::is_mobile "$type"; then
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
@ -37,13 +50,23 @@ Subcommands:
|
||||||
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 "$@" ;;
|
||||||
|
rule) cmd::identity::_rule "$@" ;;
|
||||||
|
options) cmd::identity::_options "$@" ;;
|
||||||
--help) cmd::identity::help ;;
|
--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
|
||||||
|
|
@ -282,3 +307,157 @@ function cmd::identity::_migrate() {
|
||||||
|
|
||||||
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
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}"
|
||||||
|
}
|
||||||
10
core/json.sh
10
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::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; }
|
||||||
|
|
@ -88,6 +90,14 @@ function json::identity_migrate() { python3 "$JSON_HELPER" identity_migrat
|
||||||
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)" \
|
||||||
|
|
|
||||||
|
|
@ -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]),
|
||||||
|
|
@ -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__':
|
||||||
|
|
|
||||||
|
|
@ -254,3 +254,123 @@ function identity::rename_peer() {
|
||||||
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
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
|
# 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"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue