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>
This commit is contained in:
Dan Guido 2025-08-03 17:15:27 -07:00 committed by GitHub
parent 358d50314e
commit 146e2dcf24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 127 additions and 8 deletions

View file

@ -0,0 +1,56 @@
---
# BSD systems return IPv6 addresses in the order they were added to the interface,
# not sorted by scope like Linux does. This means ansible_default_ipv6 often contains
# a link-local address (fe80::) instead of a global address, which breaks certificate
# generation due to the %interface suffix.
#
# This task file creates a fact with the first global IPv6 address found.
- name: Initialize all_ipv6_addresses as empty list
set_fact:
all_ipv6_addresses: []
- name: Get all IPv6 addresses for the default interface
set_fact:
all_ipv6_addresses: "{{ ansible_facts[ansible_default_ipv6.interface]['ipv6'] | default([]) }}"
when:
- ansible_default_ipv6 is defined
- ansible_default_ipv6.interface is defined
- ansible_facts[ansible_default_ipv6.interface] is defined
- name: Find first global IPv6 address from interface-specific addresses
set_fact:
global_ipv6_address: "{{ item.address }}"
global_ipv6_prefix: "{{ item.prefix }}"
loop: "{{ all_ipv6_addresses }}"
when:
- all_ipv6_addresses | length > 0
- item.address is defined
- not item.address.startswith('fe80:') # Filter out link-local addresses
- "'%' not in item.address" # Ensure no interface suffix
- global_ipv6_address is not defined # Only set once
loop_control:
label: "{{ item.address | default('no address') }}"
- name: Find first global IPv6 address from ansible_all_ipv6_addresses
set_fact:
global_ipv6_address: "{{ item | regex_replace('%.*', '') }}"
global_ipv6_prefix: "128" # Assume /128 for addresses from this list
loop: "{{ ansible_all_ipv6_addresses | default([]) }}"
when:
- global_ipv6_address is not defined
- ansible_all_ipv6_addresses is defined
- not item.startswith('fe80:')
- name: Override ansible_default_ipv6 with global address on BSD
set_fact:
ansible_default_ipv6: "{{ ansible_default_ipv6 | combine({'address': global_ipv6_address, 'prefix': global_ipv6_prefix}) }}"
when:
- global_ipv6_address is defined
- ansible_default_ipv6 is defined
- ansible_default_ipv6.address.startswith('fe80:') or '%' in ansible_default_ipv6.address
- name: Debug IPv6 address selection
debug:
msg: "Selected IPv6 address: {{ ansible_default_ipv6.address | default('none') }}"
when: algo_debug | default(false) | bool

View file

@ -16,6 +16,10 @@
- name: Gather additional facts
import_tasks: facts.yml
- name: Fix IPv6 address selection on BSD
import_tasks: bsd_ipv6_facts.yml
when: ipv6_support | default(false) | bool
- name: Set OS specific facts
set_fact:
config_prefix: /usr/local/

59
tests/test_bsd_ipv6.yml Normal file
View file

@ -0,0 +1,59 @@
---
# Test playbook for BSD IPv6 address selection
# Run with: ansible-playbook tests/test_bsd_ipv6.yml -e algo_debug=true
- name: Test BSD IPv6 address selection logic
hosts: localhost
gather_facts: no
vars:
# Simulate BSD system facts with link-local as default
ansible_default_ipv6:
address: "fe80::1%em0"
interface: "em0"
prefix: "64"
gateway: "fe80::1"
# Simulate interface facts with multiple IPv6 addresses
ansible_facts:
em0:
ipv6:
- address: "fe80::1%em0"
prefix: "64"
- address: "2001:db8::1"
prefix: "64"
- address: "2001:db8::2"
prefix: "64"
# Simulate all_ipv6_addresses fact
ansible_all_ipv6_addresses:
- "fe80::1%em0"
- "2001:db8::1"
- "2001:db8::2"
ipv6_support: true
algo_debug: true
tasks:
- name: Show initial IPv6 facts
debug:
msg: "Initial ansible_default_ipv6: {{ ansible_default_ipv6 }}"
- name: Include BSD IPv6 fix tasks
include_tasks: ../roles/common/tasks/bsd_ipv6_facts.yml
- name: Show fixed IPv6 facts
debug:
msg: |
Fixed ansible_default_ipv6: {{ ansible_default_ipv6 }}
Global IPv6 address: {{ global_ipv6_address | default('not found') }}
Global IPv6 prefix: {{ global_ipv6_prefix | default('not found') }}
- name: Verify fix worked
assert:
that:
- ansible_default_ipv6.address == "2001:db8::1"
- global_ipv6_address == "2001:db8::1"
- "'%' not in ansible_default_ipv6.address"
- not ansible_default_ipv6.address.startswith('fe80:')
fail_msg: "BSD IPv6 address selection failed"
success_msg: "BSD IPv6 address selection successful"