最終更新 1 month ago

nuno revised this gist 1 month ago. Go to revision

1 file changed, 358 insertions

gistfile1.txt(file created)

@@ -0,0 +1,358 @@
1 + #!/usr/bin/env bash
2 +
3 + function cmd::inspect::on_load() {
4 + flag::register --name
5 + flag::register --type
6 + flag::register --config
7 + flag::register --qr
8 + }
9 +
10 + function cmd::inspect::help() {
11 + cat <<EOF
12 + Usage: wgctl inspect --name <name> [options]
13 + wgctl inspect <full-name>
14 +
15 + Show detailed information for a WireGuard client.
16 +
17 + Sections shown:
18 + Client — IP, type, rule, status, activity
19 + Groups — group memberships
20 + Rule — firewall rule with inheritance tree and service annotations
21 + Peer Blocks — peer-specific restrictions (beyond the assigned rule)
22 + Firewall — active iptables rules with ACCEPT/DROP counts
23 +
24 + Options:
25 + --name <name> Client name (e.g. phone-nuno)
26 + --type <type> Device type — combines with --name
27 + --config Also show raw WireGuard client config
28 + --qr Also show QR code
29 +
30 + Examples:
31 + wgctl inspect --name phone-nuno
32 + wgctl inspect --name nuno --type phone
33 + wgctl inspect --name phone-nuno --config
34 + wgctl inspect --name phone-nuno --qr
35 + wgctl inspect guest-zephyr
36 + EOF
37 + }
38 +
39 + INSPECT_WIDTH=48 # total visible width of section lines
40 + INSPECT_LABEL_WIDTH=20
41 +
42 + # ============================================
43 + # Private helpers
44 + # ============================================
45 +
46 +
47 + function cmd::inspect::_section() {
48 + local title="${1:-}" extra="${2:-0}"
49 + local width=$(( INSPECT_WIDTH + extra ))
50 + local title_len=${#title}
51 + # Account for "── " (3) + " " (1) before dashes
52 + local dash_count=$(( width - title_len - 4 ))
53 + [[ $dash_count -lt 2 ]] && dash_count=2
54 + local dashes
55 + dashes=$(printf '─%.0s' $(seq 1 $dash_count))
56 + printf "\n \033[0;37m── %s %s\033[0m\n" "$title" "$dashes"
57 + }
58 +
59 + function cmd::inspect::_peer_info() {
60 + local name="${1:-}"
61 +
62 + local ip type rule public_key allowed_ips
63 + ip=$(peers::get_ip "$name")
64 + type=$(peers::get_type "$name")
65 + rule=$(peers::get_meta "$name" "rule")
66 + public_key=$(keys::public "$name" 2>/dev/null || echo "")
67 + allowed_ips=$(grep "^AllowedIPs" "$(ctx::clients)/${name}.conf" \
68 + 2>/dev/null | cut -d'=' -f2- | xargs)
69 +
70 + # Status
71 + local handshake_ts is_blocked last_ts
72 + handshake_ts=$(monitor::get_handshake_ts "$public_key")
73 + peers::is_blocked "$name" && is_blocked="true" || is_blocked="false"
74 + last_ts=$(monitor::last_attempt "$name")
75 +
76 + local is_restricted="false"
77 + block::has_specific_rules "$name" 2>/dev/null && is_restricted="true"
78 +
79 + local status last_seen endpoint
80 + status=$(peers::format_status_verbose "$name" "$public_key" \
81 + "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
82 + last_seen=$(peers::format_last_seen "$name" "$public_key" \
83 + "$is_blocked" "$last_ts" "" "$handshake_ts")
84 + endpoint=$(monitor::get_cached_endpoint "$name")
85 +
86 + local activity_total
87 + activity_total=$(peers::format_activity_total "$public_key")
88 +
89 + local activity_current
90 + activity_current=$(peers::format_activity_current "$public_key")
91 +
92 + local rule_file=""
93 + local rule_extends=""
94 + if [[ -n "$rule" ]]; then
95 + rule_file="$(rule::path "$rule" 2>/dev/null)" || true
96 + if [[ -n "$rule_file" ]]; then
97 + local ext=()
98 + mapfile -t ext < <(json::get "$rule_file" "extends" 2>/dev/null || true)
99 + if [[ ${#ext[@]} -gt 0 && -n "${ext[0]:-}" ]]; then
100 + rule_extends=" (↳ ${ext[*]})"
101 + fi
102 + fi
103 + fi
104 +
105 + # Rule formatting
106 + local rule_display="${rule:-—}"
107 + if [[ -n "$rule_file" && ${#ext[@]} -gt 0 && -n "${ext[0]:-}" ]]; then
108 + local extends_str
109 + extends_str=$(printf '%s, ' "${ext[@]}" | sed 's/, $//')
110 + rule_display="${rule} ↳ (${extends_str})"
111 + fi
112 +
113 + cmd::inspect::_section "Client"
114 + printf "\n"
115 + ui::row "Name" "$name" "${INSPECT_LABEL_WIDTH}"
116 + ui::row "IP" "$ip" "${INSPECT_LABEL_WIDTH}"
117 + ui::row "Type" "$(peers::display_type "$type")" "${INSPECT_LABEL_WIDTH}"
118 + ui::row "Rule" "$rule_display" "${INSPECT_LABEL_WIDTH}"
119 + ui::row "Status" "$(echo -e "$status")" "${INSPECT_LABEL_WIDTH}"
120 + ui::row "Endpoint" "${endpoint:-—}" "${INSPECT_LABEL_WIDTH}"
121 + ui::row "Last seen" "$last_seen" "${INSPECT_LABEL_WIDTH}"
122 + ui::row "AllowedIPs" "$allowed_ips" "${INSPECT_LABEL_WIDTH}"
123 + ui::row "Public key" "${public_key:-—}" "${INSPECT_LABEL_WIDTH}"
124 + ui::row "Activity (total)" "$activity_total" "${INSPECT_LABEL_WIDTH}"
125 + ui::row "Activity (current)" "$activity_current" "${INSPECT_LABEL_WIDTH}"
126 +
127 + return 0
128 + }
129 +
130 + # function cmd::inspect::_rule_info() {
131 + # local name="${1:-}"
132 + # local rule
133 + # rule=$(peers::get_meta "$name" "rule")
134 + # [[ -z "$rule" ]] && return 0
135 + # rule::exists "$rule" || return 0
136 +
137 + # cmd::inspect::_section "Rule: ${rule}"
138 +
139 + # if ui::rule::tree "$rule"; then
140 + # # printf "\n"
141 + # : # no-op
142 + # else
143 + # # No inheritance — flat view
144 + # rule::render_flat "$rule"
145 + # fi
146 + # return 0
147 + # }
148 +
149 + function cmd::inspect::_rule_info() {
150 + local name="${1:-}"
151 + local rule
152 + rule=$(peers::get_meta "$name" "rule")
153 +
154 + # Get identity name and its rules
155 + local identity_name identity_rules strict
156 + identity_name=$(identity::get_name "$name")
157 + if [[ -n "$identity_name" ]]; then
158 + identity_rules=$(identity::rules "$identity_name")
159 + strict=$(identity::rule_flags "$identity_name" "strict_rule")
160 + fi
161 +
162 + # Build section header
163 + local header="Rules"
164 + [[ -n "$rule" ]] && header="${header}: ${rule}"
165 + [[ -n "$identity_name" && -n "$identity_rules" ]] && \
166 + header="${header} · identity:${identity_name}"
167 +
168 + # Skip section entirely if nothing to show
169 + [[ -z "$rule" && -z "$identity_rules" ]] && return 0
170 +
171 + cmd::inspect::_section "$header"
172 +
173 + # Identity block first (applied first, base access level)
174 + if [[ -n "$identity_name" && -n "$identity_rules" ]]; then
175 + ui::rule::identity_block "$identity_name" "$strict"
176 + fi
177 +
178 + # Peer rule tree (if set and not suppressed by strict)
179 + if [[ -n "$rule" && "$strict" != "true" ]]; then
180 + rule::exists "$rule" || return 0
181 + if rule::render_extends_tree "$rule"; then
182 + printf "\n"
183 + else
184 + rule::render_flat "$rule"
185 + fi
186 + elif [[ -n "$rule" && "$strict" == "true" ]]; then
187 + printf "\n \033[2mpeer rule '%s' suppressed by strict policy\033[0m\n" "$rule"
188 + fi
189 +
190 + return 0
191 + }
192 +
193 + function cmd::inspect::_blocks_info() {
194 + local name="${1:-}"
195 + block::has_file "$name" || return 0
196 +
197 + local blocked_direct
198 + blocked_direct=$(block::is_blocked_direct "$name")
199 +
200 + local blocked_groups
201 + blocked_groups=$(block::get_groups "$name")
202 +
203 + local rules_output
204 + rules_output=$(block::get_rules "$name")
205 +
206 + # Skip if truly empty
207 + if [[ "$blocked_direct" != "true" ]] && \
208 + ui::empty "$blocked_groups" && \
209 + ui::empty "$rules_output"; then
210 + block::cleanup "$name" # clean up stale empty file
211 + return 0
212 + fi
213 +
214 + # Count rules for header
215 + local rule_count=0
216 + while IFS= read -r line; do
217 + [[ -n "$line" ]] && (( rule_count++ )) || true
218 + done <<< "$rules_output"
219 +
220 + # Build header like firewall: Blocks (+N)
221 + local header_counts=""
222 + [[ "$rule_count" -gt 0 ]] && header_counts=" (${rule_count})"
223 + [[ "$blocked_direct" == "true" || -n "$blocked_groups" ]] && \
224 + header_counts="${header_counts} 🚫"
225 +
226 + cmd::inspect::_section "Blocks${header_counts}"
227 + printf "\n"
228 +
229 + [[ "$blocked_direct" == "true" ]] && \
230 + printf " \033[1;31m🚫\033[0m blocked directly\n"
231 + [[ -n "$blocked_groups" ]] && \
232 + printf " \033[1;31m🚫\033[0m blocked by groups: %s\n" "$blocked_groups"
233 +
234 + block::format_rules "$name"
235 + return 0
236 + }
237 +
238 + function cmd::inspect::_group_info() {
239 + local name="$1"
240 +
241 + local groups=()
242 + mapfile -t groups < <(json::peer_groups "$(ctx::groups)" "$name")
243 +
244 + ui::empty "${groups[*]}" && return 0
245 +
246 + local count=${#groups[@]}
247 + cmd::inspect::_section "Groups (${count})"
248 + printf "\n"
249 +
250 + for g in "${groups[@]}"; do
251 + [[ -z "$g" ]] && continue
252 + local peer_count
253 + local main_marker=""
254 + peer_count=$(json::count "$(group::path "$g")" "peers")
255 + [[ "$g" == "$(peers::get_main_group "$name")" ]] && \
256 + main_marker=" \033[0;33m★\033[0m"
257 + printf " \033[0;37m·\033[0m %-20s \033[0;37m%s peers\033[0m%b\n" \
258 + "$g" "$peer_count" "$main_marker"
259 + done
260 +
261 + return 0
262 + }
263 +
264 + function cmd::inspect::_firewall_info() {
265 + local name="${1:-}"
266 + local ip
267 + ip=$(peers::get_ip "$name")
268 +
269 + local total=0 accepts=0 drops=0
270 + local rules_output=()
271 + while IFS= read -r line; do
272 + [[ -z "$line" ]] && continue
273 + (( total++ )) || true
274 + [[ "$line" =~ ACCEPT ]] && (( accepts++ )) || true
275 + [[ "$line" =~ DROP ]] && (( drops++ )) || true
276 + rules_output+=("$line")
277 + done < <(fw::forward_rules_for_ip "$ip" | grep -v NFLOG)
278 +
279 + ui::empty "${rules_output[*]}" && return 0
280 +
281 + printf "\n \033[0;37m── Firewall (%s %s) \033[0m%s\n\n" \
282 + "$(color::green "+${accepts}")" \
283 + "$(color::red "-${drops}")" \
284 + "$(printf '\033[0;37m─%.0s' {1..28})"
285 +
286 + fw::list_peer_rules "$ip" false
287 +
288 + return 0
289 + }
290 +
291 + function cmd::inspect::_config() {
292 + local name="$1"
293 + cmd::inspect::_section "Config"
294 + printf "\n"
295 + cat "$(ctx::clients)/${name}.conf"
296 + printf "\n"
297 +
298 + return 0
299 + }
300 +
301 + # ============================================
302 + # Run
303 + # ============================================
304 +
305 + function cmd::inspect::run() {
306 + local name="" type="" show_config=false show_qr=false
307 +
308 + if [[ $# -gt 0 && "$1" != "--"* ]]; then
309 + name="$1"
310 + shift
311 + fi
312 +
313 + while [[ $# -gt 0 ]]; do
314 + case "$1" in
315 + --name) name="$2"; shift 2 ;;
316 + --type) type="$2"; shift 2 ;;
317 + --config) show_config=true; shift ;;
318 + --qr) show_qr=true; shift ;;
319 + --help) cmd::inspect::help; return ;;
320 + *)
321 + log::error "Unknown flag: $1"
322 + cmd::inspect::help
323 + return 1
324 + ;;
325 + esac
326 + done
327 +
328 + if [[ -z "$name" ]]; then
329 + log::error "Missing required flag: --name"
330 + cmd::inspect::help
331 + return 1
332 + fi
333 +
334 + name=$(peers::resolve_and_require "$name" "$type") || return 1
335 +
336 + load_command list
337 +
338 + log::section "Inspect: ${name}"
339 +
340 + cmd::inspect::_peer_info "$name"
341 + cmd::inspect::_group_info "$name"
342 + cmd::inspect::_rule_info "$name"
343 + cmd::inspect::_blocks_info "$name"
344 + cmd::inspect::_firewall_info "$name"
345 +
346 + if $show_config; then
347 + cmd::inspect::_config "$name"
348 + fi
349 +
350 + if $show_qr; then
351 + cmd::inspect::_section "QR Code"
352 + printf "\n"
353 + load_command qr
354 + cmd::qr::run --name "$name"
355 + fi
356 +
357 + printf "\n"
358 + }
Newer Older