diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa68aac --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# os files +.DS_Store + +# working directories +local/ + +# terraform files +.terraform/ +.terraform.* +*.tfstate +*.tfstate.backup +*.tfvars + +# ansible files +*.retry diff --git a/01-ssh.tf b/01-ssh.tf new file mode 100644 index 0000000..95797d3 --- /dev/null +++ b/01-ssh.tf @@ -0,0 +1,13 @@ +resource "local_file" "ssh_config" { + content = templatefile("templates/ssh.cfg.tpl", { + server_name = var.vpn_server.name, + server_ip = var.vpn_server.local_ip, + ssh_username = var.vpn_server.username, + ssh_key_file = var.vpn_server.ssh_key_file + }) + + filename = "local/ssh.cfg" + file_permission = "0640" +} + + diff --git a/02-ansible.tf b/02-ansible.tf new file mode 100644 index 0000000..dc084e7 --- /dev/null +++ b/02-ansible.tf @@ -0,0 +1,66 @@ +## render the run script + +resource "local_file" "run_playbook" { + content = templatefile("templates/ansible/run-ansible.sh.tpl", { + inventory_file = "inventory.ini" + }) + filename = "local/ansible/run-ansible.sh" + file_permission = "0755" +} + + +## render the playbook + +resource "local_file" "playbook" { + content = templatefile("templates/ansible/playbook.yml.tpl", { + gateway_role = local.gateway_role, + iptables_role = local.iptables_role, + pimd_role = local.pimd_role, + wireguard_role = local.wireguard_role + }) + filename = "local/ansible/playbook.yml" +} + + +## render host variables + +resource "local_file" "hostvars" { + + content = templatefile("templates/ansible/hostvars.yml.tpl", { + server_name = var.vpn_server.name, + server_ifname = var.vpn_server.interface, + private_ip = var.vpn_server.local_ip, + cidr_block = var.local_network.cidr_block + gateway = var.local_network.gateway + + vpn_endpoint_address = var.vpn_network.endpoint, + vpn_endpoint_port = var.vpn_network.listen_port + + vpn_cidr_block = var.vpn_network.cidr_block + vpn_netlen = split("/", var.vpn_network.cidr_block)[1] + vpn_ip = local.vpn_server_vpn_ip + vpn_private_key = wireguard_asymmetric_key.vpn_server.private_key, + + clients = [for client in var.vpn_clients : + { + name = client + vpn_ip = local.vpn_client_vpn_ips[index(var.vpn_clients, client)] + public_key = wireguard_asymmetric_key.vpn_clients[client].public_key + } + ] + }) + + filename = "local/ansible/host_vars/${var.vpn_server.name}.yml" + file_permission = "0640" +} + + +## render the inventory file + +resource "local_file" "inventory" { + content = templatefile("templates/ansible/inventory.ini.tpl", { + server = var.vpn_server.name + }) + filename = "local/ansible/inventory.ini" + file_permission = "0640" +} diff --git a/03-gateway.tf b/03-gateway.tf new file mode 100644 index 0000000..1308553 --- /dev/null +++ b/03-gateway.tf @@ -0,0 +1,27 @@ + +locals { + gateway_role = "gateway" + iptables_role = "iptables" + pimd_role = "pimd" +} + +resource "template_dir" "gateway" { + source_dir = "templates/ansible-roles/${local.gateway_role}" + destination_dir = "local/ansible/roles/${local.gateway_role}" + + vars = {} +} + +resource "template_dir" "iptables" { + source_dir = "templates/ansible-roles/${local.iptables_role}" + destination_dir = "local/ansible/roles/${local.iptables_role}" + + vars = {} +} + +resource "template_dir" "pimd" { + source_dir = "templates/ansible-roles/${local.pimd_role}" + destination_dir = "local/ansible/roles/${local.pimd_role}" + + vars = {} +} diff --git a/04-wireguard-server.tf b/04-wireguard-server.tf new file mode 100644 index 0000000..c168c19 --- /dev/null +++ b/04-wireguard-server.tf @@ -0,0 +1,19 @@ + +locals { + wireguard_role = "wireguard" +} + +resource "wireguard_asymmetric_key" "vpn_server" { +} + +resource "wireguard_asymmetric_key" "vpn_clients" { + for_each = toset(var.vpn_clients) +} + + +resource "template_dir" "wireguard" { + source_dir = "templates/ansible-roles/${local.wireguard_role}" + destination_dir = "local/ansible/roles/${local.wireguard_role}" + + vars = {} +} diff --git a/05-wireguard-clients.tf b/05-wireguard-clients.tf new file mode 100644 index 0000000..dddc3b0 --- /dev/null +++ b/05-wireguard-clients.tf @@ -0,0 +1,18 @@ + +resource "local_file" "client_configs" { + for_each = toset(var.vpn_clients) + + content = templatefile("templates/wireguard-client.conf.tpl", { + vpn_ip = local.vpn_client_vpn_ips[index(var.vpn_clients, each.key)] + vpn_netlen = split("/", var.vpn_network.cidr_block)[1] + vpn_private_key = wireguard_asymmetric_key.vpn_clients[each.key].private_key, + + vpn_endpoint = var.vpn_network.endpoint + vpn_endpoint_port = var.vpn_network.listen_port + vpn_endpoint_public_key = wireguard_asymmetric_key.vpn_server.public_key + }) + + filename = "local/clients/${each.key}.conf" + file_permission = "0640" +} + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d8580e9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Paul Ryan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bbd43fa --- /dev/null +++ b/README.md @@ -0,0 +1,268 @@ +# Sonos over VPN + +This project describes how to remotely access and control your Sonos speakers using the app over a VPN +connection into your home network. The final build looks like this: + +![Network Diagram](docs/sonos-vpn.png) + +These instructions assume that you run a very simple network with just your ISP provided router on your local +network. If you're doing something more advanced, the details below will be different. + +## Challenges + +There are three challenges to overcome to get this to work: + +* the app only works on wifi networks +* the multicast service discovery protocol doesn't cross network boundaries +* return traffic from the speakers to the app use the app's VPN IP address + +The build and operational instructions below address all these issues. + +Once the app has discovered the speakers and the speakers know about the app, the traffic seems to be +unicast traffic and it all works smoothly... it's the discovery and setting up the initial connection +that has special challenges. + +### Challenge #1: Wifi Network + +The app checks if your device is on a wifi network, and if it isn't, then it refuses to operate. + +The way I use it is, as shown in the diagram above, to setup a hotspot on my mobile phone and then connect +my tablet to it and start the VPN on my tablet. When that's all up, I can start the Sonos app and control +the speakers. + +### Challenge #2: Multicast Service Discovery + +Multicast packets aren't normally forwarded by routers (in this case, the RaspberryPi VPN server) which +means that by default, the discovery packets arriving at your VPN server will be dropped and never make it +into your home network to find your sonos speakers. + +This is solved with these two steps: + +* incrementing the multicast packet TTL so it can be forwarded into the home network +* installing a multicast router on the VPN server to do the forwarding + +An 'iptables' rule (in the 'mangle' table) is installed to increment the packet TTL. If you were to install +the rule manually, the command would look something like this: + + iptables -t mangle -A POSTROUTING -o eth0 -j TTL --ttl-inc 1 + +The ansible scripts install the multicast router 'pimd' on the vpn server to forward the packets. +It works with the default installation and configuration. + +These two steps are handled automatically by the ansible setup. + +### Challenge #3: Return Traffic to App Host's VPN IP Address + +It appears that the multicast service discovery protocol being used sends the IP address and port of +the device running the app as part of the packet payload. This means the speakers always want to talk +back to the app on it's actual VPN IP address. + +There are two consequences of this: + +* can't have NAT anywhere between the app and the speakers +* the speakers need to be able to route traffic back to the VPN network + +There are a number of ways to make this work and they are things you will need to do manually. The approach +documented below uses static routes on your ISP provided router. + +## Prerequisites + +### Router Capabilities + +There are two essential services this setup requires from your router: + +* port forwarding from the internet side of your ISP provided router to an internal machine +* the ability to configure static routes + +How you set this up will vary between routers, but here's how it is on mine. + +Port forwarding is pretty standard: from the web management console, browse to: + + 'Security' -> 'Apps and Gaming' -> 'Single Port Forwarding' -> 'Add new Single Port Forwarding' + +For static routes, from the web management console, browse to: + + 'Connectivity' -> 'Advanced Routing' -> 'Add Static Route' + +If you don't have equivalent features, then this isn't going to work for you. + +### Hardware + +I built my setup on a RaspberryPi 3 Model B+ with the Bullseye OS, 32bit lite version installed. +It will work on lots of other systems, but you will most likely need to adapt some things. + +Build the OS with the RaspberryPi Imager as described [here](https://www.raspberrypi.com/software/). + +Set it up with ssh access enabled using public-key authentication. + +Once it's built update the system with these commands: + + sudo apt update + sudo apt dist-upgrade + sudo reboot + +Once you can ssh into the server without being prompted for a password, you're ready to go. + +### Software + +The project uses [terraform](https://www.terraform.io/) and [ansible](https://www.ansible.com/) to +build the configs and configure the raspberry pi server. + +I used these versions: + +* terraform (v1.3.2) +* ansible (core 2.13.5) + +## Preparation + +In the below sections, the following network addresses are used: + +* home network: 192.168.1.0/24 +* raspberry pi server: 192.168.1.21 +* vpn network: 192.168.15.0/24 + +If your networks are different, you will need to use your correct settings in the below steps. + +### Create DHCP Reservation for RaspberryPi Server + +This step ensures that the IP address of your RaspberryPi won't change - it needs to be a well known +address so that VPN traffic can be forwarded to it. + +The first step is to find the the MAC and IP addresses of your server with this command: + + ifconfig eth0 + +The two lines of interest look like this: + + inet 192.168.1.21 netmask 255.255.255.0 broadcast 192.168.1.255 + ether b8:27:eb:81:53:de txqueuelen 1000 (Ethernet) + +Log into your ISP provided router and create the reservation. For my router, the process was to navigate +through the menus to: 'Connectivity' -> 'Local Network' and select 'DHCP Reservations'. +Enter the name, MAC address and IP address from above. For the example above, these values are: + + MAC address: b8:27:eb:81:53:de + IP address: 192.168.1.21 + +Reboot your RaspberryPi and verify that the IP address is correctly set. + +### Port Forwarding + +In the router web management console, browse to: 'Security' -> 'Apps and Gaming' -> 'Single Port Forwarding' +and select 'Add new Single Port Forwarding'. Enter the following in the dialog: + + Name: SonosVPN + Protocol: UDP + WAN Port: 51820 + LAN POrt: 51820 + Destination IP: 192.168.1.21 + +If you have changed the port in the configuration, you'll need to change those ports here as well. + +### Static Routes + +In the router web management console, browse to: 'Connectivity' -> 'Advanced Routing' and select 'Add Static Route'. +Add the following (assuming you're using default VPN network settings): + + Route Name: SonosVPN + Destination IP: 192.168.15.0 + Subnet Mask: 255.255.255.0 + Gateway: 192.168.1.21 + +If you have configured a different 'cidr_block' for the 'vpn_network' you will need to use the new values +in the 'DestinationIP' and 'Subnet Mask' fields. + +### VPN Endpoint + +To use your VPN, you need a way to access the endpoint from outside your home/work network - this can +be either a DNS name or an IP address. + +For the DNS name, you can use a dynamic DNS provider and a lot of ISP provided routers have built in support +for dynamic DNS. Now's a good time to set that up if you plan on using it. + +This can also just be an IP address. However, this will probably change from time to time so you'll need +to update it in your client config occasionally. A simple way to get your network's external, public IP address is +with this: + + dig @resolver1.opendns.com +short myip.opendns.com + +You will need this IP address to complete the steps below. + +## Build + +There are two steps to building this system: + +* use terraform to create the ansible and wireguard configs +* use ansible to configure the server + +### Terraform + +Terraform is where the configurations of your local network, the vpn network and the wireguard clients +are defined. + +Copy the file 'terraform.tfvars.examples' to 'terraform.tfvars' and modify it for your configuration. There +are four configuration blocks that need to be set, describing: + +* the home network +* the vpn network +* the vpn server +* the vpn clients + +You can add as many clients as you like to the client list and terraform will create a configuration +for each. + +To run terraform: + + terraform init + terraform apply + +Once it finishes, the output is written to the directory 'local'. The client configurations can be found +in the 'clients' subdirectory. + +### Ansible + +Configuring the VPN server is fully automatic and quite straightforward. Simply run this command from +the root directory of the repository and it will set it up: + + ./local/ansible/run-ansible.sh + +If you want to see what it's doing first, start with the file 'playbook.yml' and read through the roles; +there isn't a lot to it. + + +## Using the System + +### Wireguard Client + +In the 'local/clients' directory, there are configurations for each client. They look like this: + + [Interface] + Address = 192.168.15.7/24 + PrivateKey = + DNS = 1.1.1.1 + + [Peer] + PublicKey = + AllowedIPs = 0.0.0.0/0 + Endpoint = AAA.BBB.CCC.DDD:51820 + +To install this configuration on a iPad assuming you're running on a mac, follow these steps: + +* copy the client configuration to your icloud drive +* on your ipad, download the wireguard app from the app store +* click the '+' button and choose 'create from file or archive' +* browse to your icloud drive and select the configuration + +That will install the vpn profile and you should be ready to go. + +### Connection and Operating + +To use the system: + +* enable the personal hotspot on your mobile phone +* connect your tablet's wifi to the hotspot +* start the vpn on your tablet +* start your sonos app on your tablet and wait for it to discover your speakers + +You might need to fully kill the app and start it again so it isn't trying to use any cached settings. + diff --git a/docs/sonos-vpn.png b/docs/sonos-vpn.png new file mode 100644 index 0000000..306ca82 Binary files /dev/null and b/docs/sonos-vpn.png differ diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..3b17007 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,25 @@ + +/*output "local_network" { + value = var.local_network +} + +output "vpn_network" { + value = var.vpn_network +} + +output "vpn_server" { + value = var.vpn_server +} + +output "vpn_clients" { + value = var.vpn_clients +} + +output "vpn_server_vpn_ip" { + value = local.vpn_server_vpn_ip +} + +output "vpn_client_vpn_ips" { + value = local.vpn_client_vpn_ips +}*/ + diff --git a/templates/ansible-roles/gateway/tasks/main.yml b/templates/ansible-roles/gateway/tasks/main.yml new file mode 100644 index 0000000..dc6f96a --- /dev/null +++ b/templates/ansible-roles/gateway/tasks/main.yml @@ -0,0 +1,29 @@ +## configure to forward packets + +- name: enable packet forwarding + sysctl: + name: net.ipv4.ip_forward + value: 1 + +- name: enable secure redirects + sysctl: + name: net.ipv4.conf.all.secure_redirects + value: 1 + +- name: disable accept redirects + sysctl: + name: net.ipv4.conf.all.accept_redirects + value: 0 + +- name: disable source routing + sysctl: + name: net.ipv4.conf.all.accept_source_route + value: 0 + +- name: disable send redirects + sysctl: + name: net.ipv4.conf.all.send_redirects + value: 0 + + + diff --git a/templates/ansible-roles/iptables/handlers/main.yml b/templates/ansible-roles/iptables/handlers/main.yml new file mode 100644 index 0000000..d166e72 --- /dev/null +++ b/templates/ansible-roles/iptables/handlers/main.yml @@ -0,0 +1,6 @@ +- name: restore iptables v4 + shell: /sbin/iptables-restore < /etc/iptables/rules.v4 + +- name: restore iptables v6 + shell: /sbin/ip6tables-restore < /etc/iptables/rules.v6 + diff --git a/templates/ansible-roles/iptables/tasks/main.yml b/templates/ansible-roles/iptables/tasks/main.yml new file mode 100644 index 0000000..a90fb97 --- /dev/null +++ b/templates/ansible-roles/iptables/tasks/main.yml @@ -0,0 +1,25 @@ +- name: install iptables-persistent + apt: + name: iptables-persistent + state: present + +- name: install iptable v4 rules + template: + src: rules.v4 + dest: /etc/iptables/rules.v4 + owner: root + group: root + mode: 0644 + notify: + - restore iptables v4 + +- name: install iptable v6 rules + template: + src: rules.v6 + dest: /etc/iptables/rules.v6 + owner: root + group: root + mode: 0644 + notify: + - restore iptables v6 + diff --git a/templates/ansible-roles/iptables/templates/rules.v4 b/templates/ansible-roles/iptables/templates/rules.v4 new file mode 100644 index 0000000..080f9cd --- /dev/null +++ b/templates/ansible-roles/iptables/templates/rules.v4 @@ -0,0 +1,22 @@ +*filter +:INPUT ACCEPT [0:0] +:FORWARD ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] +COMMIT + +*nat +:PREROUTING ACCEPT [0:0] +:INPUT ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] +:POSTROUTING ACCEPT [0:0] +-A POSTROUTING -o eth0 -d {{ gateway }}/32 -j MASQUERADE +-A POSTROUTING -o eth0 ! -d {{ cidr_block }} -j MASQUERADE +COMMIT + +*mangle +:PREROUTING ACCEPT [0:0] +:INPUT ACCEPT [0:0] +:OUTPUT ACCEPT [0:0] +:POSTROUTING ACCEPT [0:0] +-A POSTROUTING -o {{ server_ifname }} -j TTL --ttl-inc 1 +COMMIT diff --git a/templates/ansible-roles/iptables/templates/rules.v6 b/templates/ansible-roles/iptables/templates/rules.v6 new file mode 100644 index 0000000..b4a831e --- /dev/null +++ b/templates/ansible-roles/iptables/templates/rules.v6 @@ -0,0 +1,6 @@ +*filter +:INPUT ACCEPT [0:0] +:FORWARD DROP [0:0] +:OUTPUT ACCEPT [0:0] +COMMIT + diff --git a/templates/ansible-roles/pimd/tasks/main.yml b/templates/ansible-roles/pimd/tasks/main.yml new file mode 100644 index 0000000..50f02cc --- /dev/null +++ b/templates/ansible-roles/pimd/tasks/main.yml @@ -0,0 +1,13 @@ +## install packages + +- name: install pimd + apt: + name: pimd + state: present + +# wireguard will start/stop this +- name: stop pimd + systemd: + name: pimd + state: stopped + enabled: false diff --git a/templates/ansible-roles/wireguard/handlers/main.yml b/templates/ansible-roles/wireguard/handlers/main.yml new file mode 100644 index 0000000..7bd672f --- /dev/null +++ b/templates/ansible-roles/wireguard/handlers/main.yml @@ -0,0 +1,6 @@ + +- name: restart wireguard + systemd: + name: wg-quick@wg0 + state: restarted + diff --git a/templates/ansible-roles/wireguard/tasks/main.yml b/templates/ansible-roles/wireguard/tasks/main.yml new file mode 100644 index 0000000..a4d5cdb --- /dev/null +++ b/templates/ansible-roles/wireguard/tasks/main.yml @@ -0,0 +1,49 @@ +- name: install wireguard-tools + apt: + name: wireguard-tools + state: present + +- name: install wireguard config file + template: + src: wireguard.conf + dest: /etc/wireguard/wg0.conf + owner: root + group: root + mode: 0440 + notify: + - restart wireguard + +- name: create wireguard script directory + file: + path: /etc/wireguard/scripts + state: directory + owner: root + group: root + mode: 0750 + +- name: install wireguard post-up script + template: + src: post-up.sh + dest: /etc/wireguard/scripts/post-up.sh + owner: root + group: root + mode: 0750 + notify: + - restart wireguard + +- name: install wireguard pre-down script + template: + src: pre-down.sh + dest: /etc/wireguard/scripts/pre-down.sh + owner: root + group: root + mode: 0750 + notify: + - restart wireguard + +- name: start wireguard + systemd: + name: wg-quick@wg0 + state: restarted + enabled: yes + daemon_reload: yes diff --git a/templates/ansible-roles/wireguard/templates/post-up.sh b/templates/ansible-roles/wireguard/templates/post-up.sh new file mode 100755 index 0000000..1e7fefd --- /dev/null +++ b/templates/ansible-roles/wireguard/templates/post-up.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +systemctl start pimd + diff --git a/templates/ansible-roles/wireguard/templates/pre-down.sh b/templates/ansible-roles/wireguard/templates/pre-down.sh new file mode 100755 index 0000000..a6008bb --- /dev/null +++ b/templates/ansible-roles/wireguard/templates/pre-down.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +systemctl stop pimd + + diff --git a/templates/ansible-roles/wireguard/templates/wireguard.conf b/templates/ansible-roles/wireguard/templates/wireguard.conf new file mode 100644 index 0000000..39a0e75 --- /dev/null +++ b/templates/ansible-roles/wireguard/templates/wireguard.conf @@ -0,0 +1,14 @@ +[Interface] +Address = {{ vpn_ip }}/{{ vpn_netlen }} +ListenPort = {{ vpn_endpoint_port }} +PrivateKey = {{ vpn_private_key }} +PostUp = /etc/wireguard/scripts/post-up.sh +PreDown = /etc/wireguard/scripts/pre-down.sh + +{% for client in clients %} +[Peer] +# Peer: {{ client.name }} +PublicKey = {{ client.public_key }} +AllowedIPs = {{ client.vpn_ip }} +{% endfor %} + diff --git a/templates/ansible/hostvars.yml.tpl b/templates/ansible/hostvars.yml.tpl new file mode 100644 index 0000000..0c4879e --- /dev/null +++ b/templates/ansible/hostvars.yml.tpl @@ -0,0 +1,21 @@ +--- +server_name: ${server_name} +server_ifname: ${server_ifname} +private_ip: ${private_ip} +cidr_block: ${cidr_block} +gateway: ${gateway} + +vpn_endpoint_address: ${vpn_endpoint_address} +vpn_endpoint_port: ${vpn_endpoint_port} + +vpn_cidr_block: ${vpn_cidr_block} +vpn_ip: ${vpn_ip} +vpn_netlen: ${vpn_netlen} +vpn_private_key: ${vpn_private_key} + +clients: +%{ for client in clients ~} +- name: ${client.name} + vpn_ip: ${client.vpn_ip} + public_key: ${client.public_key} +%{ endfor ~} diff --git a/templates/ansible/inventory.ini.tpl b/templates/ansible/inventory.ini.tpl new file mode 100644 index 0000000..19d6363 --- /dev/null +++ b/templates/ansible/inventory.ini.tpl @@ -0,0 +1,2 @@ +[servers] +${server} diff --git a/templates/ansible/playbook.yml.tpl b/templates/ansible/playbook.yml.tpl new file mode 100644 index 0000000..5af2974 --- /dev/null +++ b/templates/ansible/playbook.yml.tpl @@ -0,0 +1,22 @@ +--- +- hosts: servers + become: yes + gather_facts: no + + vars: + ansible_python_interpreter: "/usr/bin/env python3" + + tasks: + - import_role: + name: ${gateway_role} + + - import_role: + name: ${iptables_role} + + - import_role: + name: ${pimd_role} + + - import_role: + name: ${wireguard_role} + + diff --git a/templates/ansible/run-ansible.sh.tpl b/templates/ansible/run-ansible.sh.tpl new file mode 100755 index 0000000..1fa6a4e --- /dev/null +++ b/templates/ansible/run-ansible.sh.tpl @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$( cd "$( dirname "$${BASH_SOURCE[0]}" )" && pwd )" +RUN_DIR="$( dirname $( dirname $${SCRIPT_DIR} ) )" +cd $${RUN_DIR} + +export ANSIBLE_HOST_KEY_CHECKING=0 +export ANSIBLE_SSH_ARGS="-F local/ssh.cfg -C -o ControlMaster=auto -o ControlPersist=60s" + +ansible-playbook \ + --inventory=$${SCRIPT_DIR}/${inventory_file} \ + $${SCRIPT_DIR}/playbook.yml + diff --git a/templates/ssh.cfg.tpl b/templates/ssh.cfg.tpl new file mode 100644 index 0000000..d49522f --- /dev/null +++ b/templates/ssh.cfg.tpl @@ -0,0 +1,5 @@ +Host ${server_name} + Hostname ${server_ip} + User ${ssh_username} + IdentityFile ${ssh_key_file} + IdentitiesOnly yes diff --git a/templates/wireguard-client.conf.tpl b/templates/wireguard-client.conf.tpl new file mode 100644 index 0000000..e2eb95d --- /dev/null +++ b/templates/wireguard-client.conf.tpl @@ -0,0 +1,9 @@ +[Interface] +Address = ${vpn_ip }/${vpn_netlen} +PrivateKey = ${vpn_private_key} +DNS = 1.1.1.1 + +[Peer] +PublicKey = ${vpn_endpoint_public_key} +AllowedIPs = 0.0.0.0/0 +Endpoint = ${vpn_endpoint}:${vpn_endpoint_port} diff --git a/terraform.tf b/terraform.tf new file mode 100644 index 0000000..aba108f --- /dev/null +++ b/terraform.tf @@ -0,0 +1,13 @@ +terraform { + required_version = ">= 1.3.2" + required_providers { + wireguard = { + source = "ojford/wireguard" + version = "0.2.1+1" + } + } +} + + +provider "wireguard" {} + diff --git a/terraform.tfvars.example b/terraform.tfvars.example new file mode 100644 index 0000000..85ec3c4 --- /dev/null +++ b/terraform.tfvars.example @@ -0,0 +1,25 @@ + +local_network = { + cidr_block = "192.168.1.0/24" + gateway = "192.168.1.1" +} + +vpn_network = { + endpoint = "sonos.mydomain.com" + listen_port = 51820 + cidr_block = "192.168.15.0/24" +} + +vpn_server = { + name = "sonos" + interface = "eth0" + local_ip = "192.168.1.21" + username = "pi" + ssh_key_file = "~/.ssh/rpi" +} + +vpn_clients = [ + "client1", + "client2" +] + diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..f5f285e --- /dev/null +++ b/variables.tf @@ -0,0 +1,52 @@ + +variable "local_network" { + type = object({ + cidr_block = string + gateway = string + }) + default = { + cidr_block = "192.168.14.0/24" + gateway = "192.168.14.1" + } +} + +variable "vpn_server" { + type = object({ + name = string + interface = string + local_ip = string + username = string + ssh_key_file = string + }) + default = { + name = "sonos" + interface = "eth0" + local_ip = "192.168.14.4" + username = "pi" + ssh_key_file = "~/.ssh/rpi" + } +} + +variable "vpn_network" { + type = object({ + endpoint = string + listen_port = number + cidr_block = string + }) + default = { + endpoint = "sonos.mydomain.com" # DNS name or IP address + listen_port = 51820 + cidr_block = "192.168.15.0/24" + } +} + +variable "vpn_clients" { + type = list(string) + default = ["laptop", "ipad"] +} + +locals { + vpn_server_vpn_ip = cidrhost(var.vpn_network.cidr_block, 1) + vpn_client_vpn_ips = [for client in var.vpn_clients : cidrhost(var.vpn_network.cidr_block, index(var.vpn_clients, client)+5)] +} +