├── CHANGELOG.md ├── LICENSE ├── README.md ├── defaults └── main.yml ├── doc └── multiple-networks.md ├── handlers └── main.yml ├── meta └── main.yml ├── tasks ├── configure-service.yml ├── install.yml ├── main.yml └── write-configuration.yml └── templates ├── host ├── tinc-down ├── tinc-up └── tinc.conf /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## v1.1.0 5 | 6 | * Added support for custom commands and configuration options (#3 by h33x0) 7 | 8 | ## v1.0.4 9 | 10 | * Fixed deprecated ansible_ssh_host (#4 by @mtearle) 11 | 12 | ## v1.0.3 13 | 14 | * Switched to iproute2 (#2, thx @devfaz) 15 | 16 | ## v1.0.2 17 | 18 | * Bugfix: Updated project description 19 | 20 | ## v1.0.1 21 | 22 | * Bugfix: Check `tincvpn_connect_to` (string) for safechars 23 | 24 | ## v1.0.0 25 | 26 | * Added support for configuring multiple networks in parallel 27 | * Added support for `Port` setting 28 | 29 | ## v0.3.0 30 | 31 | * Added support for adding routes 32 | * Use Subnet definition only in router mode 33 | 34 | ## v0.2.0 35 | 36 | * Added Ansible Galaxy meta information 37 | * Fixed some Ansible Galaxy warnings 38 | * Renamed namespace from `mlohr` to `matthiaslohr` 39 | 40 | ## v0.1.0 41 | 42 | * First shippable playbook 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Matthias Lohr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible Role for tinc VPN 2 | 3 | This is an Ansible role for setting up one or many tinc VPN networks (https://www.tinc-vpn.org/). 4 | 5 | ## Usage 6 | 7 | Add role to your `requirements.yml`: 8 | ```yaml 9 | - src: https://github.com/MatthiasLohr/ansible-role-tincvpn 10 | name: matthiaslohr.tincvpn 11 | ``` 12 | 13 | It's also possible to specify the version to be installed by using the `version` parameters. 14 | Please read the [Ansible Galaxy Documentation](https://docs.ansible.com/ansible/latest/reference_appendices/galaxy.html#installing-multiple-roles-from-a-file) for details. 15 | 16 | Set `tincvpn_defaul_ip` for your hosts in inventory file: 17 | ```ini 18 | [all] 19 | node1 tincvpn_default_ip=192.168.255.1 20 | node2 tincvpn_default_ip=192.168.255.2 21 | node3 tincvpn_default_ip=192.168.255.3 22 | ``` 23 | 24 | Simple playbook example: 25 | ```yaml 26 | - hosts: all 27 | roles: 28 | - matthiaslohr.tincvpn 29 | ``` 30 | 31 | For examples how to configure multiple tinc networks in parallel, take a look at the [documentation](doc/multiple-networks.md). 32 | 33 | 34 | ## Host Variables 35 | 36 | | Variable Name | Default Value | Description | 37 | |---------------|---------------------|-------------------------------------------------------------------| 38 | | `tincvpn_{{ tincvpn_network }}_ip` | `none` | tinc IP address of this node (should be part of `tincvpn_subnet`) | 39 | 40 | 41 | ## Role Variables 42 | 43 | | Variable Name | Default Value | Description | 44 | |-------------------------------|-------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------| 45 | | `tincvpn_network` | `"default"` | Name of the tinc network (e.g. tinc configuration folder name). | 46 | | `tincvpn_interface` | `"tincvpn-{{ tincvpn_network }}"` | Name for the network interface used by tinc. | 47 | | `tincvpn_subnet` | `"192.168.255.0/24"` | Subnet used by tinc. | 48 | | `tincvpn_mode` | `"switch"` | Tinc `Mode` setting. | 49 | | `tincvpn_port` | `655` | Tinc listening port. | 50 | | `tincvpn_extra_hosts` | `[]` | Additional tinc hosts available (not covered by playbook, read [Additional Hosts](#additional-hosts)). | 51 | | `tincvpn_key_bits` | `2048` | Length of RSA private key. | 52 | | `tincvpn_connect_to` | `[]` | Nodes to connect to by default. You can give a single nodename as string or multiple nodes as list of strings. | 53 | | `tincvpn_routes` | `[]` | Add routes using the tinc VPN network interface. | 54 | | `tincvpn_local_directory` | `"{{ inventory_dir }}/tincvpn-hosts/{{ tincvpn_network }}"` | Where to save host public keys locally. | 55 | | `tincvpn_custom_config` | `{}` | Dictionary with tinc vpn custom config parameters. ex.: `PingInterval: "60"` | 56 | | `tincvpn_custom_up_scripts` | `[]` | Custom commands set for tinc-up script template. | 57 | | `tincvpn_custom_down_scripts` | `[]` | Custom commands set for tinc-down script template. | 58 | 59 | 60 | ## Configuration Tweaks 61 | 62 | ### Additional Hosts 63 | 64 | In case you want to connect to a node that is not included in the Ansible inventory (e.g. a central router you want to connect to), it is possible to configure additional hosts via playbook variables: 65 | ```yaml 66 | tincvpn_extra_hosts: 67 | - name: externalnode1 68 | address: externalnode1.example.com 69 | public_key: | 70 | -----BEGIN RSA PUBLIC KEY----- 71 | ... 72 | -----END RSA PUBLIC KEY----- 73 | 74 | - name: externalnode2 75 | address: externalnode2.example.com 76 | public_key: | 77 | -----BEGIN RSA PUBLIC KEY----- 78 | ... 79 | -----END RSA PUBLIC KEY----- 80 | ``` 81 | 82 | 83 | ### Custom Routes 84 | 85 | ```yaml 86 | tincvpn_routes: 87 | - network: "192.168.254.0/24" 88 | gateway: "192.168.255.1" 89 | ``` 90 | -------------------------------------------------------------------------------- /defaults/main.yml: -------------------------------------------------------------------------------- 1 | # name of the tinc network (also name of configuration directory) 2 | tincvpn_network: default 3 | 4 | # name of the network interface 5 | tincvpn_interface: "tincvpn-{{ tincvpn_network }}" 6 | 7 | # subnet (CIDR) 8 | tincvpn_subnet: 192.168.255.0/24 9 | 10 | # tinc VPN mode 11 | tincvpn_mode: switch 12 | 13 | # tinc listening port 14 | tincvpn_port: 655 15 | 16 | # list of (additional) hosts to connect to 17 | tincvpn_extra_hosts: [] 18 | 19 | # private key length for host keys 20 | tincvpn_key_bits: 2048 21 | 22 | # where to save host public keys locally 23 | tincvpn_local_directory: "{{ inventory_dir }}/tincvpn-hosts/{{ tincvpn_network }}" 24 | 25 | # To which host we should connect to per default 26 | tincvpn_connect_to: 27 | 28 | # Routes using the tincvpn interface 29 | tincvpn_routes: [] 30 | 31 | tincvpn_safechars_regex: '[^0-9a-zA-Z]+' 32 | tincvpn_safechars_replacement: '_' 33 | 34 | # custom configuration for updown scripts and main service 35 | tincvpn_custom_config: {} 36 | tincvpn_custom_up_scripts: [] 37 | tincvpn_custom_down_scripts: [] 38 | -------------------------------------------------------------------------------- /doc/multiple-networks.md: -------------------------------------------------------------------------------- 1 | # Multiple tinc VPN networks 2 | 3 | It's possible to set up multiple tinc networks in parallel by using the role multiple times: 4 | 5 | ```yaml 6 | - hosts: group1 7 | roles: 8 | - role: matthiaslohr.tincvpn 9 | vars: 10 | tincvpn_network: "net1" 11 | tincvpn_subnet: "192.168.255.0/24" 12 | 13 | - hosts: group2 14 | roles: 15 | - role: matthiaslohr.tincvpn 16 | vars: 17 | tincvpn_network: "net2" 18 | tincvpn_subnet: "192.168.42.0/24" 19 | tincvpn_port: 10655 20 | ``` 21 | 22 | You can specify node's VPN ip addresses in the inventory for each network: 23 | ```ini 24 | [all] 25 | node1 tincvpn_net1_ip=192.168.255.1 tincvpn_net2_ip=192.168.42.1 26 | node2 tincvpn_net1_ip=192.168.255.2 27 | node3 incvpn_net2_ip=192.168.42.3 28 | 29 | [group1] 30 | node1 31 | node2 32 | 33 | [group2] 34 | node1 35 | node3 36 | ``` 37 | -------------------------------------------------------------------------------- /handlers/main.yml: -------------------------------------------------------------------------------- 1 | - name: restart tinc VPN 2 | service: 3 | name: tinc 4 | state: restarted 5 | 6 | - name: reload tinc VPN 7 | service: 8 | name: tinc 9 | state: reloaded -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | galaxy_info: 2 | role_name: tincvpn 3 | author: Matthias Lohr 4 | description: Ansible role for setting up one or many tinc VPN networks (https://www.tinc-vpn.org/). 5 | company: https://mlohr.com/ 6 | license: MIT 7 | 8 | min_ansible_version: 2.2 9 | 10 | platforms: 11 | - name: Debian 12 | versions: 13 | - all 14 | - name: Ubuntu 15 | versions: 16 | - all 17 | 18 | galaxy_tags: 19 | - vpn 20 | - tinc 21 | 22 | dependencies: [] 23 | -------------------------------------------------------------------------------- /tasks/configure-service.yml: -------------------------------------------------------------------------------- 1 | - name: ensure network is in nets.boot 2 | lineinfile: 3 | path: "/etc/tinc/nets.boot" 4 | line: "{{ tincvpn_network }}" 5 | 6 | - name: Configure systemd service 7 | systemd: 8 | name: "tinc@{{ tincvpn_network }}" 9 | enabled: True 10 | when: ansible_service_mgr == 'systemd' -------------------------------------------------------------------------------- /tasks/install.yml: -------------------------------------------------------------------------------- 1 | - name: ensure tinc is installed 2 | package: 3 | name: tinc 4 | state: present 5 | register: package_result 6 | retries: 3 7 | until: package_result is success 8 | delay: 10 -------------------------------------------------------------------------------- /tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: set custom facts for tinc VPN 2 | set_fact: 3 | tincvpn_nodename: "{{ inventory_hostname | regex_replace(tincvpn_safechars_regex, tincvpn_safechars_replacement) }}" 4 | 5 | - import_tasks: "install.yml" 6 | - import_tasks: "write-configuration.yml" 7 | - import_tasks: "configure-service.yml" -------------------------------------------------------------------------------- /tasks/write-configuration.yml: -------------------------------------------------------------------------------- 1 | - name: ensure network configuration directory exists 2 | file: 3 | path: "/etc/tinc/{{ tincvpn_network }}/hosts" 4 | state: directory 5 | 6 | - name: check if RSA private key exists 7 | stat: 8 | path: "/etc/tinc/{{ tincvpn_network }}/rsa_key.priv" 9 | register: private_key_stat 10 | 11 | - name: generate RSA private key 12 | command: "openssl genrsa -out /etc/tinc/{{ tincvpn_network }}/rsa_key.priv {{ tincvpn_key_bits }}" 13 | when: not private_key_stat.stat.exists 14 | register: private_key_generate 15 | changed_when: private_key_generate.rc != 0 16 | failed_when: private_key_generate.rc != 0 17 | notify: 18 | - restart tinc VPN 19 | 20 | - name: generate RSA public key 21 | command: "openssl rsa -in /etc/tinc/{{ tincvpn_network }}/rsa_key.priv -pubout" 22 | register: public_key_generate 23 | changed_when: False 24 | failed_when: public_key_generate.rc != 0 25 | 26 | - name: write network profile configuration 27 | template: 28 | src: "templates/{{ item.file }}" 29 | dest: "/etc/tinc/{{ tincvpn_network }}/{{ item.file }}" 30 | mode: "{{ item.mode }}" 31 | with_items: 32 | - file: "tinc.conf" 33 | mode: "0644" 34 | - file: "tinc-up" 35 | mode: "744" 36 | - file: "tinc-down" 37 | mode: "744" 38 | notify: 39 | - restart tinc VPN 40 | 41 | - name: write personal host file 42 | template: 43 | src: "templates/host" 44 | dest: "/etc/tinc/{{ tincvpn_network }}/hosts/{{ tincvpn_nodename }}" 45 | mode: "0644" 46 | vars: 47 | tincvpn_host_address: "{{ ansible_host }}" 48 | tincvpn_host_public_key: "{{ public_key_generate.stdout }}" 49 | 50 | - name: download host file to coordinator 51 | fetch: 52 | src: "/etc/tinc/{{ tincvpn_network }}/hosts/{{ tincvpn_nodename }}" 53 | dest: "{{ tincvpn_local_directory }}/{{ tincvpn_nodename }}" 54 | flat: True 55 | 56 | - name: upload host files to all hosts 57 | copy: 58 | src: "{{ tincvpn_local_directory }}/" 59 | dest: "/etc/tinc/{{ tincvpn_network }}/hosts/" 60 | notify: 61 | - reload tinc VPN 62 | 63 | - name: write additional hosts 64 | template: 65 | src: "templates/host" 66 | dest: "/etc/tinc/{{ tincvpn_network }}/hosts/{{ item.name }}" 67 | mode: "0644" 68 | vars: 69 | tincvpn_host_address: "{{ item.address }}" 70 | tincvpn_host_public_key: "{{ item.public_key }}" 71 | with_items: "{{ tincvpn_extra_hosts }}" 72 | notify: 73 | - reload tinc VPN 74 | -------------------------------------------------------------------------------- /templates/host: -------------------------------------------------------------------------------- 1 | Address = {{ tincvpn_host_address }} 2 | Port = {{ tincvpn_port }} 3 | {% if tincvpn_mode == 'router' %} 4 | Subnet = {{ tincvpn_subnet }} 5 | {% endif %} 6 | {{ tincvpn_host_public_key }} -------------------------------------------------------------------------------- /templates/tinc-down: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | {% for route in tincvpn_routes %} 3 | ip route del {{ route.network }} via {{ route.gateway }} dev $INTERFACE 4 | {% endfor %} 5 | 6 | {% if tincvpn_custom_down_scripts is not none and tincvpn_custom_down_scripts|length %} 7 | {% if tincvpn_custom_down_scripts is iterable and tincvpn_custom_down_scripts is not string %} 8 | {% for command in tincvpn_custom_down_scripts %} 9 | {{ command }} 10 | {% endfor %} 11 | {% endif %} 12 | {% endif %} 13 | 14 | -------------------------------------------------------------------------------- /templates/tinc-up: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | {% if 'tincvpn_'+tincvpn_network+'_ip' in hostvars[inventory_hostname] %} 4 | ip address add dev $INTERFACE {{ hostvars[inventory_hostname]['tincvpn_'+tincvpn_network+'_ip'] }}/{{ tincvpn_subnet|ipaddr('netmask') }} 5 | ip link set dev $INTERFACE up 6 | {% endif %} 7 | 8 | {% for route in tincvpn_routes %} 9 | ip route add {{ route.network }} via {{ route.gateway }} dev $INTERFACE 10 | {% endfor %} 11 | 12 | {% if tincvpn_custom_up_scripts is not none and tincvpn_custom_up_scripts|length %} 13 | {% if tincvpn_custom_up_scripts is iterable and tincvpn_custom_up_scripts is not string %} 14 | {% for command in tincvpn_custom_up_scripts %} 15 | {{ command }} 16 | {% endfor %} 17 | {% endif %} 18 | {% endif %} 19 | -------------------------------------------------------------------------------- /templates/tinc.conf: -------------------------------------------------------------------------------- 1 | Name = {{ tincvpn_nodename }} 2 | Interface = {{ tincvpn_interface }} 3 | Mode = {{ tincvpn_mode }} 4 | Port = {{ tincvpn_port }} 5 | {% if tincvpn_connect_to is not none and tincvpn_connect_to|length %} 6 | {% if tincvpn_connect_to is iterable and tincvpn_connect_to is not string %} 7 | {% for destination in tincvpn_connect_to %} 8 | {% if destination != tincvpn_nodename %} 9 | ConnectTo = {{ destination | regex_replace(tincvpn_safechars_regex, tincvpn_safechars_replacement) }} 10 | {% endif %} 11 | {% endfor %} 12 | {% elif tincvpn_connect_to is string %} 13 | {% if tincvpn_connect_to | regex_replace(tincvpn_safechars_regex, tincvpn_safechars_replacement) != tincvpn_nodename %} 14 | ConnectTo = {{ tincvpn_connect_to | regex_replace(tincvpn_safechars_regex, tincvpn_safechars_replacement) }} 15 | {% endif %} 16 | {% endif %} 17 | {% endif %} 18 | 19 | {% if tincvpn_custom_config is not none and tincvpn_custom_config|length %} 20 | {% if tincvpn_custom_config is iterable and tincvpn_custom_config is not string %} 21 | {% for key, value in tincvpn_custom_config.items() %} 22 | {{ key }} = {{ value }} 23 | {% endfor %} 24 | {% endif %} 25 | {% endif %} 26 | --------------------------------------------------------------------------------