Skip to content

Commit 146e2dc

Browse files
dguidoclaude
andauthored
Fix IPv6 address selection on BSD systems (#14786)
* fix: Fix IPv6 address selection on BSD systems (#1843) BSD systems return IPv6 addresses in the order they were added to the interface, not sorted by scope like Linux. This causes ansible_default_ipv6 to contain link-local addresses (fe80::) with interface suffixes (%em0) instead of global addresses, breaking certificate generation. This fix: - Adds a new task file to properly select global IPv6 addresses on BSD - Filters out link-local addresses and interface suffixes - Falls back to ansible_all_ipv6_addresses when needed - Ensures certificates are generated with valid global IPv6 addresses The workaround is implemented in Algo rather than waiting for the upstream Ansible issue (#16977) to be fixed, which has been open since 2016. Fixes #1843 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: Remove duplicate condition in BSD IPv6 facts Removed redundant 'global_ipv6_address is not defined' condition that was checked twice in the same when clause. * improve: simplify regex for IPv6 interface suffix removal Change regex from '(.*)%.*' to '%.*' for better readability and performance when stripping interface suffixes from IPv6 addresses. The simplified regex is equivalent but more concise and easier to understand. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: resolve yamllint trailing spaces in BSD IPv6 test Remove trailing spaces from test_bsd_ipv6.yml to ensure CI passes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: resolve yamllint issues across repository - Remove trailing spaces from server.yml, WireGuard test files, and keys.yml - Add missing newlines at end of test files - Ensure all YAML files pass yamllint validation for CI 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 358d503 commit 146e2dc

File tree

8 files changed

+127
-8
lines changed

8 files changed

+127
-8
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
# BSD systems return IPv6 addresses in the order they were added to the interface,
3+
# not sorted by scope like Linux does. This means ansible_default_ipv6 often contains
4+
# a link-local address (fe80::) instead of a global address, which breaks certificate
5+
# generation due to the %interface suffix.
6+
#
7+
# This task file creates a fact with the first global IPv6 address found.
8+
9+
- name: Initialize all_ipv6_addresses as empty list
10+
set_fact:
11+
all_ipv6_addresses: []
12+
13+
- name: Get all IPv6 addresses for the default interface
14+
set_fact:
15+
all_ipv6_addresses: "{{ ansible_facts[ansible_default_ipv6.interface]['ipv6'] | default([]) }}"
16+
when:
17+
- ansible_default_ipv6 is defined
18+
- ansible_default_ipv6.interface is defined
19+
- ansible_facts[ansible_default_ipv6.interface] is defined
20+
21+
- name: Find first global IPv6 address from interface-specific addresses
22+
set_fact:
23+
global_ipv6_address: "{{ item.address }}"
24+
global_ipv6_prefix: "{{ item.prefix }}"
25+
loop: "{{ all_ipv6_addresses }}"
26+
when:
27+
- all_ipv6_addresses | length > 0
28+
- item.address is defined
29+
- not item.address.startswith('fe80:') # Filter out link-local addresses
30+
- "'%' not in item.address" # Ensure no interface suffix
31+
- global_ipv6_address is not defined # Only set once
32+
loop_control:
33+
label: "{{ item.address | default('no address') }}"
34+
35+
- name: Find first global IPv6 address from ansible_all_ipv6_addresses
36+
set_fact:
37+
global_ipv6_address: "{{ item | regex_replace('%.*', '') }}"
38+
global_ipv6_prefix: "128" # Assume /128 for addresses from this list
39+
loop: "{{ ansible_all_ipv6_addresses | default([]) }}"
40+
when:
41+
- global_ipv6_address is not defined
42+
- ansible_all_ipv6_addresses is defined
43+
- not item.startswith('fe80:')
44+
45+
- name: Override ansible_default_ipv6 with global address on BSD
46+
set_fact:
47+
ansible_default_ipv6: "{{ ansible_default_ipv6 | combine({'address': global_ipv6_address, 'prefix': global_ipv6_prefix}) }}"
48+
when:
49+
- global_ipv6_address is defined
50+
- ansible_default_ipv6 is defined
51+
- ansible_default_ipv6.address.startswith('fe80:') or '%' in ansible_default_ipv6.address
52+
53+
- name: Debug IPv6 address selection
54+
debug:
55+
msg: "Selected IPv6 address: {{ ansible_default_ipv6.address | default('none') }}"
56+
when: algo_debug | default(false) | bool

roles/common/tasks/freebsd.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
- name: Gather additional facts
1717
import_tasks: facts.yml
1818

19+
- name: Fix IPv6 address selection on BSD
20+
import_tasks: bsd_ipv6_facts.yml
21+
when: ipv6_support | default(false) | bool
22+
1923
- name: Set OS specific facts
2024
set_fact:
2125
config_prefix: /usr/local/

roles/wireguard/tasks/keys.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# Access pattern: item.item.item where:
1212
# - First 'item' = current async_status result
13-
# - Second 'item' = original async job object
13+
# - Second 'item' = original async job object
1414
# - Third 'item' = actual username from original loop
1515
#
1616
# Reference: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_loops.html
@@ -92,7 +92,7 @@
9292
become: false
9393
# DATA STRUCTURE EXPLANATION:
9494
# item = current result from wg_genkey_results.results
95-
# item.item = original job object from wg_genkey.results
95+
# item.item = original job object from wg_genkey.results
9696
# item.item.item = actual username from original loop
9797
# item.stdout = the generated private key content
9898

server.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
- block:
1010
- name: Wait until the cloud-init completed
1111
wait_for:
12-
path: /var/lib/cloud/data/result.json
12+
path: /var/lib/cloud/data/result.json
1313
delay: 10 # Conservative 10 second initial delay
1414
timeout: 480 # Reduce from 600 to 480 seconds (8 minutes)
1515
sleep: 10 # Check every 10 seconds (less aggressive)

tests/test-wireguard-async.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
rc: 0
1616
failed: false
1717
finished: true
18-
- item: "10.10.10.1" # This comes from the original wg_genkey.results item
18+
- item: "10.10.10.1" # This comes from the original wg_genkey.results item
1919
stdout: "mock_private_key_2" # This is the command output
2020
changed: true
2121
rc: 0

tests/test-wireguard-fix.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
register: file_check
4848
loop:
4949
- "testuser1"
50-
- "testuser2"
50+
- "testuser2"
5151
- "127.0.0.1"
5252

5353
- name: Assert all files exist
@@ -63,4 +63,4 @@
6363
state: absent
6464

6565
- debug:
66-
msg: "✅ WireGuard async fix test PASSED - item.item.item is the correct pattern!"
66+
msg: "✅ WireGuard async fix test PASSED - item.item.item is the correct pattern!"

tests/test-wireguard-real-async.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
- name: Debug - Show wg_genkey structure
3737
debug:
3838
var: wg_genkey
39-
39+
4040
- name: Simulate the actual async pattern - Wait for completion
4141
async_status:
4242
jid: "{{ item.ansible_job_id }}"
@@ -62,4 +62,4 @@
6262
- name: Cleanup
6363
file:
6464
path: "{{ wireguard_pki_path }}"
65-
state: absent
65+
state: absent

tests/test_bsd_ipv6.yml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
# Test playbook for BSD IPv6 address selection
3+
# Run with: ansible-playbook tests/test_bsd_ipv6.yml -e algo_debug=true
4+
5+
- name: Test BSD IPv6 address selection logic
6+
hosts: localhost
7+
gather_facts: no
8+
vars:
9+
# Simulate BSD system facts with link-local as default
10+
ansible_default_ipv6:
11+
address: "fe80::1%em0"
12+
interface: "em0"
13+
prefix: "64"
14+
gateway: "fe80::1"
15+
16+
# Simulate interface facts with multiple IPv6 addresses
17+
ansible_facts:
18+
em0:
19+
ipv6:
20+
- address: "fe80::1%em0"
21+
prefix: "64"
22+
- address: "2001:db8::1"
23+
prefix: "64"
24+
- address: "2001:db8::2"
25+
prefix: "64"
26+
27+
# Simulate all_ipv6_addresses fact
28+
ansible_all_ipv6_addresses:
29+
- "fe80::1%em0"
30+
- "2001:db8::1"
31+
- "2001:db8::2"
32+
33+
ipv6_support: true
34+
algo_debug: true
35+
36+
tasks:
37+
- name: Show initial IPv6 facts
38+
debug:
39+
msg: "Initial ansible_default_ipv6: {{ ansible_default_ipv6 }}"
40+
41+
- name: Include BSD IPv6 fix tasks
42+
include_tasks: ../roles/common/tasks/bsd_ipv6_facts.yml
43+
44+
- name: Show fixed IPv6 facts
45+
debug:
46+
msg: |
47+
Fixed ansible_default_ipv6: {{ ansible_default_ipv6 }}
48+
Global IPv6 address: {{ global_ipv6_address | default('not found') }}
49+
Global IPv6 prefix: {{ global_ipv6_prefix | default('not found') }}
50+
51+
- name: Verify fix worked
52+
assert:
53+
that:
54+
- ansible_default_ipv6.address == "2001:db8::1"
55+
- global_ipv6_address == "2001:db8::1"
56+
- "'%' not in ansible_default_ipv6.address"
57+
- not ansible_default_ipv6.address.startswith('fe80:')
58+
fail_msg: "BSD IPv6 address selection failed"
59+
success_msg: "BSD IPv6 address selection successful"

0 commit comments

Comments
 (0)