add darwin support

This commit is contained in:
Clément Désiles
2026-09-15 17:30:20 +02:00
parent 923ffa4acd
commit f89a919022
5 changed files with 113 additions and 14 deletions
+18
View File
@@ -0,0 +1,18 @@
# WireGuard
Installs WireGuard and manages `wg-quick` tunnels from `wireguard_tunnels`
(see `defaults/main.yml` for the full schema).
- **Linux** — server (`server_mode: true`) or client. Server mode enables
masquerade + UFW rules; tunnels are managed by `wg-quick@<iface>.service`.
- **macOS (Darwin)** — client only, via Homebrew `wireguard-tools` +
`wireguard-go`. No systemd/UFW/sysctl: the config is rendered to
`{{ wireguard_config_base_path }}` (`/etc/wireguard`) and brought up with
`wg-quick`.
## Verify
```sh
sudo wg show # Linux
sudo /opt/homebrew/bin/wg show # macOS (or add /opt/homebrew/bin to sudo secure_path)
```
+19 -1
View File
@@ -1,6 +1,20 @@
--- ---
wireguard_primary_interface: "{{ network_interfaces.0.name }}" wireguard_primary_interface: "{{ network_interfaces.0.name }}"
wireguard_config_base_path: /etc/wireguard wireguard_config_base_path: /etc/wireguard
# Group owning the config dir/files. macOS has no `root` group (gid 0 is `wheel`).
wireguard_config_group: >-
{{ (ansible_facts['os_family'] == 'Darwin') | ternary('wheel', 'root') }}
# macOS: Homebrew prefix (arm64: /opt/homebrew, Intel: /usr/local). sudo's
# secure_path omits it, so wg/wg-quick must be called by absolute path.
wireguard_darwin_brew_prefix: /opt/homebrew
wireguard_wg_bin: >-
{{ (ansible_facts['os_family'] == 'Darwin') | ternary(wireguard_darwin_brew_prefix + '/bin/wg', 'wg') }}
wireguard_wg_quick_bin: >-
{{ (ansible_facts['os_family'] == 'Darwin') | ternary(wireguard_darwin_brew_prefix + '/bin/wg-quick', 'wg-quick') }}
# wg-quick's shebang is `#!/usr/bin/env bash` and requires bash 4+, but sudo's
# secure_path resolves `bash` to macOS's /bin/bash 3.2. Run it through
# Homebrew's bash explicitly (wireguard-tools depends on it).
wireguard_darwin_bash: "{{ wireguard_darwin_brew_prefix }}/bin/bash"
# wireguard_tunnels: # wireguard_tunnels:
# - interface: wg0 # required: maps to wgN interface name and config filename # - interface: wg0 # required: maps to wgN interface name and config filename
# address: 10.0.0.1/24 # required: CIDR address for [Interface] Address # address: 10.0.0.1/24 # required: CIDR address for [Interface] Address
@@ -8,6 +22,9 @@ wireguard_config_base_path: /etc/wireguard
# dns: 10.0.0.1 # optional: DNS= line; omit to suppress # dns: 10.0.0.1 # optional: DNS= line; omit to suppress
# server_mode: true # optional (default: false): enables NAT masquerade + UFW rule # server_mode: true # optional (default: false): enables NAT masquerade + UFW rule
# primary_interface: eth0 # optional: overrides wireguard_primary_interface for this tunnel # primary_interface: eth0 # optional: overrides wireguard_primary_interface for this tunnel
# private_key: ... # optional: use a fixed (e.g. vaulted) private key instead
# # of generating one on the host. Recommended for clients
# # whose public key is already a server's peer.
# peers: # optional: list of [Peer] entries # peers: # optional: list of [Peer] entries
# - name: peer_name # required: comment label # - name: peer_name # required: comment label
# public_key: ... # required: peer's public key # public_key: ... # required: peer's public key
@@ -18,7 +35,8 @@ wireguard_tunnels: []
# OS-dependent package name # OS-dependent package name
wireguard_package_name: >- wireguard_package_name: >-
{{ (ansible_facts['os_family'] == 'Archlinux') | ternary('wireguard-tools', 'wireguard') }} {{ (ansible_facts['os_family'] == 'Archlinux' or ansible_facts['os_family'] == 'Darwin')
| ternary('wireguard-tools', 'wireguard') }}
# Prometheus exporter (optional, gated by wireguard_exporter_enabled). # Prometheus exporter (optional, gated by wireguard_exporter_enabled).
# Exposes `wg show all dump` as Prometheus metrics (bytes per peer, last # Exposes `wg show all dump` as Prometheus metrics (bytes per peer, last
+15 -1
View File
@@ -2,17 +2,29 @@
- name: Reload systemd - name: Reload systemd
ansible.builtin.systemd: ansible.builtin.systemd:
daemon_reload: true daemon_reload: true
become: true
when: ansible_facts['os_family'] != 'Darwin'
- name: Apply sysctl - name: Apply sysctl
ansible.builtin.command: sysctl --system ansible.builtin.command: sysctl --system
become: true
changed_when: true changed_when: true
when: ansible_facts['os_family'] != 'Darwin'
# Linux applies the config live with wg syncconf (no downtime). macOS has no
# syncconf-friendly flow here, so bounce the interface through wg-quick.
- name: Reload wireguard tunnel - name: Reload wireguard tunnel
ansible.builtin.shell: | ansible.builtin.shell: |
set -o pipefail set -o pipefail
wg syncconf "{{ item.interface }}" <(wg-quick strip "{{ item.interface }}") if [ "{{ ansible_facts['os_family'] }}" = "Darwin" ]; then
{{ wireguard_darwin_bash }} {{ wireguard_wg_quick_bin }} down "{{ item.interface }}" 2>/dev/null || true
{{ wireguard_darwin_bash }} {{ wireguard_wg_quick_bin }} up "{{ item.interface }}"
else
{{ wireguard_wg_bin }} syncconf "{{ item.interface }}" <(wg-quick strip "{{ item.interface }}")
fi
args: args:
executable: /bin/bash executable: /bin/bash
become: true
loop: "{{ wireguard_tunnels }}" loop: "{{ wireguard_tunnels }}"
changed_when: true changed_when: true
@@ -20,3 +32,5 @@
ansible.builtin.systemd: ansible.builtin.systemd:
name: "{{ wireguard_exporter_service }}" name: "{{ wireguard_exporter_service }}"
state: restarted state: restarted
become: true
when: ansible_facts['os_family'] != 'Darwin'
+14 -2
View File
@@ -9,10 +9,20 @@
See roles/wireguard/defaults/main.yml for configuration instructions. See roles/wireguard/defaults/main.yml for configuration instructions.
success_msg: "wireguard_tunnels validation passed" success_msg: "wireguard_tunnels validation passed"
- name: Install wireguard - name: Install wireguard (Linux)
ansible.builtin.package: ansible.builtin.package:
name: "{{ wireguard_package_name }}" name: "{{ wireguard_package_name }}"
state: present state: present
when: ansible_facts['os_family'] != 'Darwin'
# Homebrew refuses to run as root, so never become for this task even when the
# role is applied with become (needed later for /etc/wireguard and wg-quick).
- name: Install wireguard (macOS)
community.general.homebrew:
name: "{{ wireguard_package_name }}"
state: present
become: false
when: ansible_facts['os_family'] == 'Darwin'
- name: Install nftables (Arch server_mode PostUp dependency) - name: Install nftables (Arch server_mode PostUp dependency)
ansible.builtin.package: ansible.builtin.package:
@@ -38,13 +48,15 @@
name: systemd-resolved name: systemd-resolved
enabled: true enabled: true
state: started state: started
when: ansible_facts['os_family'] != 'Darwin'
- name: Ensure wireguard configuration is only owned by root - name: Ensure wireguard configuration is only owned by root
ansible.builtin.file: ansible.builtin.file:
path: "{{ wireguard_config_base_path }}" path: "{{ wireguard_config_base_path }}"
owner: root owner: root
group: root group: "{{ wireguard_config_group }}"
mode: "0700" mode: "0700"
state: directory
recurse: true recurse: true
- name: Configure tunnel - name: Configure tunnel
+47 -10
View File
@@ -15,22 +15,36 @@
ansible.builtin.stat: ansible.builtin.stat:
path: "{{ wireguard_config_base_path }}/{{ _tunnel.interface }}.privatekey" path: "{{ wireguard_config_base_path }}/{{ _tunnel.interface }}.privatekey"
register: _tunnel_pkey_file register: _tunnel_pkey_file
when: _tunnel.private_key is not defined
- name: "Generate wireguard keys for {{ _tunnel.interface }} if not present" - name: "Generate wireguard keys if not present for {{ _tunnel.interface }}"
ansible.builtin.shell: > ansible.builtin.shell: |
wg genkey | set -o pipefail
{{ wireguard_wg_bin }} genkey |
tee {{ wireguard_config_base_path }}/{{ _tunnel.interface }}.privatekey | tee {{ wireguard_config_base_path }}/{{ _tunnel.interface }}.privatekey |
wg pubkey > {{ wireguard_config_base_path }}/{{ _tunnel.interface }}.publickey {{ wireguard_wg_bin }} pubkey > {{ wireguard_config_base_path }}/{{ _tunnel.interface }}.publickey
when: not _tunnel_pkey_file.stat.exists args:
executable: /bin/bash
changed_when: true
when:
- _tunnel.private_key is not defined
- not _tunnel_pkey_file.stat.exists
- name: "Retrieve wireguard private key for {{ _tunnel.interface }}" - name: "Retrieve wireguard private key for {{ _tunnel.interface }}"
ansible.builtin.slurp: ansible.builtin.slurp:
src: "{{ wireguard_config_base_path }}/{{ _tunnel.interface }}.privatekey" src: "{{ wireguard_config_base_path }}/{{ _tunnel.interface }}.privatekey"
register: _tunnel_private_key_b64 register: _tunnel_private_key_b64
when: _tunnel.private_key is not defined
- name: "Set wireguard private key fact for {{ _tunnel.interface }}" - name: "Set wireguard private key fact from inventory for {{ _tunnel.interface }}"
ansible.builtin.set_fact:
_tunnel_private_key: "{{ _tunnel.private_key }}"
when: _tunnel.private_key is defined
- name: "Set wireguard private key fact from host for {{ _tunnel.interface }}"
ansible.builtin.set_fact: ansible.builtin.set_fact:
_tunnel_private_key: "{{ _tunnel_private_key_b64['content'] | b64decode }}" _tunnel_private_key: "{{ _tunnel_private_key_b64['content'] | b64decode }}"
when: _tunnel.private_key is not defined
- name: "Resolve effective DNS for {{ _tunnel.interface }}" - name: "Resolve effective DNS for {{ _tunnel.interface }}"
ansible.builtin.set_fact: ansible.builtin.set_fact:
@@ -41,7 +55,7 @@
src: wireguard.conf.j2 src: wireguard.conf.j2
dest: "{{ wireguard_config_base_path }}/{{ _tunnel.interface }}.conf" dest: "{{ wireguard_config_base_path }}/{{ _tunnel.interface }}.conf"
owner: root owner: root
group: root group: "{{ wireguard_config_group }}"
mode: "0600" mode: "0600"
notify: Reload wireguard tunnel notify: Reload wireguard tunnel
@@ -50,6 +64,7 @@
path: "/etc/systemd/system/wg-quick@{{ _tunnel.interface }}.service.d" path: "/etc/systemd/system/wg-quick@{{ _tunnel.interface }}.service.d"
state: directory state: directory
mode: "0755" mode: "0755"
when: ansible_facts['os_family'] != 'Darwin'
- name: "Deploy systemd override for network dependency for {{ _tunnel.interface }}" - name: "Deploy systemd override for network dependency for {{ _tunnel.interface }}"
ansible.builtin.template: ansible.builtin.template:
@@ -57,6 +72,7 @@
dest: "/etc/systemd/system/wg-quick@{{ _tunnel.interface }}.service.d/network-dependency.conf" dest: "/etc/systemd/system/wg-quick@{{ _tunnel.interface }}.service.d/network-dependency.conf"
mode: "0644" mode: "0644"
notify: Reload systemd notify: Reload systemd
when: ansible_facts['os_family'] != 'Darwin'
- name: "Enable IP forwarding for {{ _tunnel.interface }}" - name: "Enable IP forwarding for {{ _tunnel.interface }}"
ansible.builtin.copy: ansible.builtin.copy:
@@ -64,10 +80,12 @@
content: | content: |
net.ipv4.ip_forward = 1 net.ipv4.ip_forward = 1
owner: root owner: root
group: root group: "{{ wireguard_config_group }}"
mode: "0644" mode: "0644"
notify: Apply sysctl notify: Apply sysctl
when: _tunnel.server_mode | default(false) when:
- _tunnel.server_mode | default(false)
- ansible_facts['os_family'] != 'Darwin'
- name: "Configure the firewall for {{ _tunnel.interface }}" - name: "Configure the firewall for {{ _tunnel.interface }}"
community.general.ufw: community.general.ufw:
@@ -83,6 +101,7 @@
when: when:
- _tunnel.server_mode | default(false) - _tunnel.server_mode | default(false)
- _tunnel.port is defined - _tunnel.port is defined
- ansible_facts['os_family'] != 'Darwin'
- name: "Allow forwarding for {{ _tunnel.interface }}" - name: "Allow forwarding for {{ _tunnel.interface }}"
community.general.ufw: community.general.ufw:
@@ -100,7 +119,9 @@
out: "{{ _tunnel.primary_interface | default(wireguard_primary_interface) }}" out: "{{ _tunnel.primary_interface | default(wireguard_primary_interface) }}"
- in: "{{ _tunnel.primary_interface | default(wireguard_primary_interface) }}" - in: "{{ _tunnel.primary_interface | default(wireguard_primary_interface) }}"
out: "{{ _tunnel.interface }}" out: "{{ _tunnel.interface }}"
when: _tunnel.server_mode | default(false) when:
- _tunnel.server_mode | default(false)
- ansible_facts['os_family'] != 'Darwin'
- name: "Start and enable wg-quick@{{ _tunnel.interface }}" - name: "Start and enable wg-quick@{{ _tunnel.interface }}"
ansible.builtin.service: ansible.builtin.service:
@@ -108,3 +129,19 @@
state: started state: started
enabled: true enabled: true
daemon_reload: true daemon_reload: true
when: ansible_facts['os_family'] != 'Darwin'
# macOS: no systemd; wg-quick drives wireguard-go directly. Idempotent via the
# list of active interfaces rather than a `creates` path.
- name: "List active wireguard interfaces (macOS)"
ansible.builtin.command: "{{ wireguard_wg_bin }} show interfaces"
register: _wg_active_interfaces
changed_when: false
when: ansible_facts['os_family'] == 'Darwin'
- name: "Bring up tunnel (macOS) for {{ _tunnel.interface }}"
ansible.builtin.command: "{{ wireguard_darwin_bash }} {{ wireguard_wg_quick_bin }} up {{ _tunnel.interface }}"
changed_when: true
when:
- ansible_facts['os_family'] == 'Darwin'
- _tunnel.interface not in _wg_active_interfaces.stdout.split()