Last active 1 month ago

add.command.sh Raw
1#!/usr/bin/env bash
2
3# ============================================
4# Lifecycle
5# ============================================
6
7function cmd::add::on_load() {
8 load_module subnet
9 load_module identity
10 load_module policy
11
12 flag::register --name
13 flag::register --identity
14 flag::register --type
15 flag::register --subnet
16 flag::register --rule
17 flag::register --group
18 flag::register --ip
19 flag::register --tunnel
20 flag::register --show-qr
21
22 # Dynamically register --<subnet_name> as shorthand flags
23 local subnet_name
24 while IFS= read -r subnet_name; do
25 [[ -n "$subnet_name" ]] && flag::register "--${subnet_name}"
26 done < <(subnet::list_names)
27}
28
29# ============================================
30# Help
31# ============================================
32
33function cmd::add::help() {
34 cat <<EOF
35Usage: wgctl add --name <name> --type <type> [options]
36 or: wgctl add --identity <identity> --type <type> [options]
37
38Add a new WireGuard client.
39
40Options:
41 --name <name> Client name (e.g. nuno) — combined with type: phone-nuno
42 --identity <name> Identity name — auto-names peer with next available index
43 --type <type> Device type: desktop, laptop, phone, tablet, server, iot
44 --subnet <subnet> Subnet to allocate from (default: type-native)
45 --ip <ip> Override auto-assigned IP (optional)
46 --tunnel <mode> Tunnel mode: split|full (overrides policy)
47 --rule <rule> Peer rule (default: from policy default_rule or none)
48 --group <group> Add to group on creation (group must exist)
49 --show-qr Show the WireGuard config as a QR code after creation
50
51Subnet shorthands (equivalent to --subnet <name>):
52 --guests, --servers, --iot, ... (see: wgctl subnet list)
53
54Examples:
55 wgctl add --name nuno --type phone
56 wgctl add --identity nuno --type phone
57 wgctl add --name zephyr --type desktop --guests
58 wgctl add --identity zephyr --type desktop --guests
59 wgctl add --name visitor --type phone --guests --show-qr
60 wgctl add --name dev --type laptop --rule dev-01
61EOF
62}
63
64# ============================================
65# Validation
66# ============================================
67
68function cmd::add::_validate() {
69 local name="$1" identity="$2" type="$3" ip="$4" tunnel="$5"
70
71 if [[ -z "$name" && -z "$identity" ]]; then
72 log::error "Missing required flag: --name or --identity"
73 return 1
74 fi
75
76 if [[ -z "$type" ]]; then
77 log::error "Missing required flag: --type"
78 return 1
79 fi
80
81 if ! json::subnet_exists "$(ctx::subnets)" "$type" 2>/dev/null; then
82 log::error "Unknown device type: '${type}'"
83 log::info "Use 'wgctl subnet list' to see valid types and subnets"
84 return 1
85 fi
86
87 if [[ -n "$tunnel" && "$tunnel" != "split" && "$tunnel" != "full" ]]; then
88 log::error "Invalid tunnel mode: '${tunnel}' (use 'split' or 'full')"
89 return 1
90 fi
91
92 if [[ -n "$ip" ]]; then
93 ip::require_valid "$ip"
94 fi
95}
96
97function cmd::add::_validate_not_exists() {
98 local full_name="$1"
99 if [[ -f "$(ctx::clients)/${full_name}.conf" ]]; then
100 log::error "Client already exists: ${full_name}"
101 return 1
102 fi
103}
104
105# ============================================
106# Display helpers
107# ============================================
108
109function cmd::add::is_mobile() {
110 local type="$1"
111 [[ "$type" == "phone" || "$type" == "tablet" ]]
112}
113
114# ============================================
115# Run
116# ============================================
117
118function cmd::add::run() {
119 local name="" identity="" type="" subnet_name="" rule="" \
120 group="" ip="" tunnel="" show_qr=false
121
122 while [[ $# -gt 0 ]]; do
123 case "$1" in
124 --name) name="$2"; shift 2 ;;
125 --identity) identity="$2"; shift 2 ;;
126 --type) type="$2"; shift 2 ;;
127 --subnet) subnet_name="$2"; shift 2 ;;
128 --rule) rule="$2"; shift 2 ;;
129 --group) group="$2"; shift 2 ;;
130 --ip) ip="$2"; shift 2 ;;
131 --tunnel) tunnel="$2"; shift 2 ;;
132 --show-qr) show_qr=true; shift ;;
133 --help) cmd::add::help; return ;;
134 --*)
135 local flag_name="${1#--}"
136 if subnet::exists "$flag_name" 2>/dev/null; then
137 subnet_name="$flag_name"
138 shift
139 else
140 log::error "Unknown flag: $1"
141 cmd::add::help
142 return 1
143 fi
144 ;;
145 *)
146 log::error "Unknown flag: $1"
147 cmd::add::help
148 return 1
149 ;;
150 esac
151 done
152
153 cmd::add::_validate "$name" "$identity" "$type" "$ip" "$tunnel" || return 1
154
155 # Resolve full peer name
156 local full_name
157 if [[ -n "$identity" ]]; then
158 full_name=$(identity::next_peer_name "$identity" "$type") || return 1
159 log::info "Auto-named: ${full_name}"
160 else
161 full_name="${type}-${name}"
162 fi
163
164 cmd::add::_validate_not_exists "$full_name" || return 1
165
166 # Resolve subnet CIDR and canonical type
167 local resolved_cidr resolved_type
168 resolved_cidr=$(subnet::resolve_for_add "$type" "$subnet_name") || return 1
169 resolved_type=$(subnet::type_for_add "$type" "$subnet_name") || return 1
170
171 # Resolve effective policy
172 local identity_name="${identity:-$(identity::get_name "$full_name")}"
173 local effective_policy
174 effective_policy=$(policy::effective "$subnet_name" "$resolved_type" "$identity_name")
175
176 # Resolve tunnel mode — flag overrides policy
177 if [[ -z "$tunnel" ]]; then
178 tunnel=$(policy::tunnel_mode "$effective_policy")
179 fi
180
181 # Resolve peer rule — explicit flag overrides policy default_rule
182 if [[ -z "$rule" ]]; then
183 rule=$(policy::default_rule "$effective_policy")
184 fi
185
186 # Validate rule if set
187 if [[ -n "$rule" ]]; then
188 rule::exists "$rule" || { log::error "Rule not found: ${rule}"; return 1; }
189 fi
190
191 local allowed_ips
192 allowed_ips=$(config::allowed_ips_for "$tunnel") || return 1
193
194 log::section "Adding client: ${full_name}"
195
196 # Allocate IP
197 if [[ -n "$ip" ]]; then
198 subnet::require_ip_valid_for "$resolved_cidr" "$ip" || return 1
199 else
200 ip=$(ip::next_for_subnet "$resolved_cidr") || return 1
201 fi
202
203 cmd::add::_log_plan "$full_name" "$type" "$resolved_type" \
204 "$subnet_name" "$resolved_cidr" "$ip" "$tunnel" \
205 "$allowed_ips" "${rule:---}" "$effective_policy"
206
207 keys::generate_pair "$full_name" || return 1
208 peers::create_client_config "$full_name" "$resolved_type" "$ip" "$allowed_ips" || return 1
209
210 # Write meta — type, subnet, rule (if set)
211 peers::set_meta "$full_name" "type" "$resolved_type"
212 if [[ -n "$subnet_name" ]]; then
213 peers::set_meta "$full_name" "subnet" "$subnet_name"
214 fi
215 if [[ -n "$rule" ]]; then
216 peers::set_meta "$full_name" "rule" "$rule"
217 fi
218
219 cmd::add::_assign_group "$full_name" "$group"
220
221 local public_key
222 public_key=$(keys::public "$full_name") || return 1
223 peers::add_to_server "$full_name" "$public_key" "$ip" || return 1
224
225 # Apply peer rule if set
226 if [[ -n "$rule" ]]; then
227 rule::apply "$rule" "$ip" "$full_name" || return 1
228 fi
229
230 # Auto-attach to identity and apply identity rule if set
231 identity::auto_attach "$full_name" "$resolved_type"
232 cmd::add::_apply_identity_rule "$full_name" "$ip" "$identity_name" "$effective_policy" "$rule"
233
234 peers::reload || return 1
235
236 log::wg_success "Client added: ${full_name} (${ip}) [${tunnel} tunnel]"
237 cmd::add::_show_result "$full_name" "$resolved_type" "$show_qr"
238}
239
240# ============================================
241# Internal helpers
242# ============================================
243
244function cmd::add::_log_plan() {
245 local full_name="${1:-}" type="${2:-}" resolved_type="${3:-}" \
246 subnet_name="${4:-}" resolved_cidr="${5:-}" ip="${6:-}" \
247 tunnel="${7:-}" allowed_ips="${8:-}" rule="${9:-}" policy="${10:-}"
248
249 log::wg_add "Name: ${full_name}"
250 log::wg_add "Type: ${resolved_type}"
251 [[ -n "$subnet_name" ]] && log::wg_add "Subnet: ${subnet_name} (${resolved_cidr})"
252 log::wg_add "IP: ${ip}"
253 log::wg_add "Tunnel: ${tunnel} (${allowed_ips})"
254 log::wg_add "Endpoint: $(config::endpoint)"
255 log::wg_add "Rule: ${rule}"
256 log::wg_add "Policy: ${policy}"
257}
258
259function cmd::add::_assign_group() {
260 local full_name="${1:-}" group="${2:-}"
261 [[ -z "$group" ]] && return 0
262 if ! group::exists "$group"; then
263 log::wg_warning "Group '${group}' not found — skipping group assignment"
264 return 0
265 fi
266 group::add_peer "$group" "$full_name"
267 log::wg "Added to group: ${group}"
268}
269
270function cmd::add::_apply_identity_rule() {
271 local full_name="${1:-}" ip="${2:-}" identity_name="${3:-}" \
272 effective_policy="${4:-}" peer_rule="${5:-}"
273
274 [[ -z "$identity_name" ]] && return 0
275
276 local rules
277 rules=$(identity::rules "$identity_name")
278
279 if [[ -z "$rules" ]]; then
280 # No identity rules — warn if no peer rule either
281 if [[ -z "$peer_rule" ]]; then
282 policy::warn_no_rule "$full_name"
283 fi
284 return 0
285 fi
286
287 # Apply all identity rules
288 rule::_apply_identity_rule "$full_name" "$ip"
289
290 # Warn based on strict_rule
291 local strict
292 strict=$(identity::rule_flags "$identity_name" "strict_rule")
293 if [[ "$strict" == "true" ]]; then
294 local rule_list
295 rule_list=$(echo "$rules" | tr '\n' ',' | sed 's/,$//')
296 policy::warn_strict_rule "$identity_name" "$effective_policy" "$rule_list"
297 elif [[ -n "$peer_rule" ]]; then
298 local rule_list
299 rule_list=$(echo "$rules" | tr '\n' ',' | sed 's/,$//')
300 policy::warn_additive_rule "$identity_name" "$rule_list" "$peer_rule"
301 fi
302}
303
304function cmd::add::_show_result() {
305 local full_name="${1:-}" type="${2:-}" show_qr="${3:-false}"
306 if $show_qr || cmd::add::is_mobile "$type"; then
307 log::section "Client QR"
308 keys::qr "$full_name"
309 else
310 log::section "Client Config"
311 cat "$(ctx::clients)/${full_name}.conf"
312 fi
313}
314