aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xdependencies.sh5
-rw-r--r--dmvpn-hub.awall43
-rwxr-xr-xdmvpn-pfx-decode94
-rw-r--r--dmvpn.swanctl26
-rw-r--r--nhrp-events.initd13
-rwxr-xr-xsetup-dmvpn248
6 files changed, 427 insertions, 2 deletions
diff --git a/dependencies.sh b/dependencies.sh
index 57a913d..2d2d6ce 100755
--- a/dependencies.sh
+++ b/dependencies.sh
@@ -8,5 +8,6 @@
# See LICENSE file for license details
sudo apk add -t .dmvpn-tools-deps \
- lua5.2 lua5.2-cqueues lua5.2-lyaml lua5.2-ossl lua5.2-posix \
- lua5.2-sql-sqlite3 lua5.2-stringy lua5.2-struct lua-asn1
+ augeas bind-tools lua5.2 lua5.2-cqueues lua5.2-lyaml lua5.2-ossl \
+ lua5.2-posix lua5.2-sql-sqlite3 lua5.2-stringy lua5.2-struct lua-asn1 \
+ quagga strongswan tunnel
diff --git a/dmvpn-hub.awall b/dmvpn-hub.awall
new file mode 100644
index 0000000..7bf13d6
--- /dev/null
+++ b/dmvpn-hub.awall
@@ -0,0 +1,43 @@
+{
+ "zone": {
+ "dmvpn-ipsec": { "addr": "0.0.0.0/0" },
+ "dmvpn-gre": { "addr": "0.0.0.0/0", "ipsec": true },
+ "dmvpn-bgp": {
+ "iface": "$dmvpn_gre_iface", "addr": "0.0.0.0/0"
+ },
+ "dmvpn": { "iface": "$dmvpn_gre_iface", "route-back": true }
+ },
+ "log": {
+ "dmvpn": {
+ "mode": "nflog",
+ "group": "$dmvpn_nflog_group",
+ "range": 128,
+ "limit": {
+ "interval": 15,
+ "src-mask": "$dmvpn_site_mask",
+ "dest-mask": "$dmvpn_site_mask"
+ }
+ }
+ },
+ "packet-log": [ { "in": "dmvpn", "out": "dmvpn", "log": "dmvpn" } ],
+ "filter": [
+ {
+ "in": "_fw",
+ "service": [
+ "dns",
+ "http",
+ "https",
+ "ldap",
+ "ldaps",
+ "ntp"
+ ]
+ },
+ { "in": "dmvpn-ipsec", "out": "_fw", "service": "ipsec" },
+ { "in": "_fw", "out": "dmvpn-ipsec", "service": "ipsec" },
+ { "in": "dmvpn-gre", "out": "_fw", "service": "gre" },
+ { "in": "_fw", "out": "dmvpn-gre", "service": "gre" },
+ { "in": "dmvpn-bgp", "out": "_fw", "service": "bgp" },
+ { "in": "_fw", "out": "dmvpn-bgp", "service": "bgp" },
+ { "in": "dmvpn", "out": "dmvpn" }
+ ]
+}
diff --git a/dmvpn-pfx-decode b/dmvpn-pfx-decode
new file mode 100755
index 0000000..2ec13a9
--- /dev/null
+++ b/dmvpn-pfx-decode
@@ -0,0 +1,94 @@
+#!/usr/bin/lua5.2
+
+--[[
+Copyright (c) 2017-2018 Kaarle Ritvanen
+See LICENSE file for license details
+]]--
+
+dmvpn = require('dmvpn')
+pkcs12 = require('openssl.pkcs12')
+
+name = arg[1]
+file = io.open(name)
+data = file:read('*a')
+file:close()
+
+name = name:match('/([^/]+)$') or name
+password = name:match('^%w*_%d+%.(%w+)%.pfx$')
+if password then
+ success, key, cert, chain = pcall(pkcs12.parse, data, password)
+end
+if not success then
+ io.stderr:write('Password: ')
+ os.execute('stty -echo')
+ password = io.read()
+ os.execute('stty echo')
+ io.stderr:write('\n')
+ key, cert, chain = pkcs12.parse(data, password)
+end
+
+function write_pem_file(dir, data)
+ local file = io.open('/etc/swanctl/'..dir..'/dmvpn.pem', 'w')
+ file:write(data)
+ file:close()
+end
+
+write_pem_file('private', key:toPEM('private'))
+write_pem_file('x509', tostring(cert))
+for i, ca_cert in pairs(chain) do
+ assert(i == 1)
+ write_pem_file('x509ca', tostring(ca_cert))
+end
+
+function print_var(name, value)
+ print(name.."='"..value:gsub("'", "'\\''").."'")
+end
+
+for tpe, value in pairs(cert:getSubjectAlt()) do
+ if tpe == 'IP' then
+ if value:match('^[%d%.]+$') then
+ print_var('GRE_IPV4_ADDRESS', value)
+ elseif value:match('^[%x:]+$') then
+ print_var('GRE_IPV6_ADDRESS', value)
+ else assert(false) end
+ end
+end
+
+EXTENSIONS = {
+ basicConstraints=true,
+ ['sbgp-ipAddrBlock']={
+ IPV4_PREFIXES=function(v) return v[1] end,
+ IPV6_PREFIXES=function(v) return v[2] end
+ },
+ ['sbgp-autonomousSysNum']='AS_NUMBER',
+ [dmvpn.OID_IS_HUB]={
+ VPNC_TYPE=function(v) return v and 'hub' or 'spoke' end
+ },
+ [dmvpn.OID_HUB_HOSTS]='HUBS'
+}
+
+
+for i=1,cert:getExtensionCount() do
+ ext = cert:getExtension(i)
+ k = ext:getName()
+ v = EXTENSIONS[k]
+ if v then
+ if type(v) == 'string' then
+ v = {[v]=function(v) return v end}
+ end
+ if type(v) == 'table' then
+ local data = dmvpn.decode_ext(k, ext)
+ for var, func in pairs(v) do
+ local d = func(data)
+ print_var(
+ var,
+ type(d) == 'table' and
+ table.concat(d, ' ') or
+ tostring(d)
+ )
+ end
+ end
+ elseif ext:getCritical() then
+ error('Unrecognized critical extension: '..k)
+ end
+end
diff --git a/dmvpn.swanctl b/dmvpn.swanctl
new file mode 100644
index 0000000..9d90819
--- /dev/null
+++ b/dmvpn.swanctl
@@ -0,0 +1,26 @@
+# Copyright (c) 2017-2018 Kaarle Ritvanen
+# See LICENSE file for license details
+
+connections {
+ dmvpn {
+ mobike = no
+ dpd_delay = 15s
+ unique = replace
+ reauth_time = 13h
+ local {
+ certs = dmvpn.pem
+ }
+ remote {
+ cacerts = dmvpn.pem
+ }
+ children {
+ dmvpn {
+ local_ts = dynamic[gre]
+ remote_ts = dynamic[gre]
+ inactivity = 90m
+ rekey_time = 100m
+ mode = transport
+ }
+ }
+ }
+}
diff --git a/nhrp-events.initd b/nhrp-events.initd
new file mode 100644
index 0000000..c42124f
--- /dev/null
+++ b/nhrp-events.initd
@@ -0,0 +1,13 @@
+#!/sbin/openrc-run
+
+# init.d file for nhrp-events
+# Copyright (c) 2017-2018 Kaarle Ritvanen
+
+name=nhrp-events
+command=/usr/sbin/$name
+pidfile=/var/run/$name.pid
+command_background=1
+
+depend() {
+ need bgpd
+}
diff --git a/setup-dmvpn b/setup-dmvpn
new file mode 100755
index 0000000..98ff505
--- /dev/null
+++ b/setup-dmvpn
@@ -0,0 +1,248 @@
+#!/bin/sh -e
+
+# Dynamic Multipoint VPN setup script for Alpine Linux
+# Copyright (c) 2017-2018 Kaarle Ritvanen
+# See LICENSE file for license details
+
+
+. /lib/libalpine.sh
+
+if [ -z "$1" ]; then
+ echo "Usage: $0 <pfx_file>" >&2
+ exit 1
+fi
+
+
+ATTRS=$(/usr/libexec/dmvpn-pfx-decode "$1")
+eval $ATTRS
+
+for attr in GRE_IPV4_ADDRESS HUBS VPNC_TYPE; do
+ eval "[ \"\$$attr\" ]" || die "attribute not defined: $attr"
+done
+
+
+ask "NHRP network ID" 1
+NHRP_ID=$resp
+
+NFLOG_GROUP=
+if [ $VPNC_TYPE = hub ]; then
+ ask "NFLOG group" 1
+ NFLOG_GROUP=$resp
+
+ ask "DMVPN site IPv4 prefix length" 16
+ SITE_PREFIX_LEN_IPV4=$resp
+
+ if [ "$GRE_IPV6_ADDRESS" ]; then
+ ask "DMVPN site IPv6 prefix length" 48
+ SITE_PREFIX_LEN_IPV6=$resp
+ fi
+fi
+
+
+PMTU_SYSCTL=net.ipv4.ip_forward_use_pmtu
+
+
+get_dev() {
+ sed -E "s/^$* (.+ )?dev ([^ ]+)( .+)?\$/\\2/;ta;d;:a"
+}
+
+get_local_dev() {
+ ip route list table local | get_dev local $1
+}
+
+enable_service() {
+ rc-update add $1
+ rc-service $1 start
+}
+
+enable_firewall() {
+ augtool -s <<EOF
+set /files/etc/conf.d/$1/IPFORWARD yes
+set /files/etc/conf.d/$1/SAVE_ON_STOP no
+EOF
+ enable_service $1
+}
+
+get_config_cmds() {
+ local p=$1
+ shift
+ while [ $# -gt 0 ]; do
+ [ "$1" ] && echo $p $1
+ shift
+ done
+}
+
+get_nhrp_config() {
+ (
+ IFS=$'\n'
+ get_config_cmds "$1 nhrp" \
+ "network-id $NHRP_ID" \
+ shortcut \
+ "registration no-unique" \
+ "${NFLOG_GROUP:+redirect}" \
+ $(get_config_cmds "nhs dynamic nbma" $HUBS)
+ )
+}
+
+get_peer_config() {
+ local group=$1
+ local map=RTT-$2
+ shift 2
+ get_config_cmds "neighbor $group" \
+ peer-group \
+ passive \
+ "ebgp-multihop 1" \
+ disable-connected-check \
+ "timers 10 30" \
+ "next-hop-self all" \
+ "soft-reconfiguration inbound" \
+ "route-map $map in" \
+ "$@"
+}
+
+get_spoke_config() {
+ local group=spoke-$1
+ shift
+ get_peer_config $group SET "$@" \
+ "advertisement-interval 1" \
+ "prefix-list no-hosts out"
+}
+
+get_quagga_config() {
+ cat <<EOF
+ configure terminal
+ nhrp event socket /var/run/nhrp-events.sock
+ ${NFLOG_GROUP:+nhrp nflog-group $NFLOG_GROUP}
+ interface $GRE_IFACE
+ tunnel protection vici profile dmvpn
+EOF
+ get_nhrp_config ip
+ [ "$GRE_IPV6_ADDRESS" ] && get_nhrp_config ipv6
+ cat <<EOF
+ exit
+ router bgp $AS_NUMBER
+EOF
+ get_config_cmds network $IPV4_PREFIXES
+ if [ "$IPV6_PREFIXES" ]; then
+ echo address-family ipv6
+ get_config_cmds network $IPV6_PREFIXES
+ echo exit
+ fi
+ echo exit
+ if [ $VPNC_TYPE = hub ]; then
+ cat <<EOF
+ ip prefix-list no-hosts seq 5 permit 0.0.0.0/0 le 30
+ route-map RTT-SET permit 10
+ set metric rtt
+ exit
+ router bgp $AS_NUMBER
+EOF
+ get_peer_config hubs ADD \
+ "remote-as $AS_NUMBER" \
+ "timers connect 10"
+ get_spoke_config ebgp "attribute-unchanged med"
+ get_spoke_config ibgp \
+ "remote-as $AS_NUMBER" \
+ route-reflector-client
+ echo exit
+ fi
+ cat <<EOF
+ exit
+ write memory
+EOF
+}
+
+
+GRE_IFACE=$(get_local_dev $GRE_IPV4_ADDRESS)
+
+if [ -z "$GRE_IFACE" ]; then
+ ask_which interface "shall be used for GRE transport" \
+ "$(ls /sys/class/net)" \
+ $(for h in $HUBS; do
+ if expr ${h##*.} : '[a-zA-Z][a-zA-Z0-9]*$' \
+ > /dev/null; then
+
+ host $h | sed -E 's/^.+ has address //;ta;d;:a'
+ else
+ echo $h
+ fi
+ done | while read addr; do
+
+ if [ -z "$(get_local_dev $addr)" ]; then
+ ip route get $addr | get_dev $addr
+ break
+ fi
+ done)
+ TRANSPORT_IFACE=$resp
+
+ i=1
+ while [ -d /sys/class/net/gre$i ]; do
+ : $(( i++ ))
+ done
+
+ ask "GRE tunnel interface" gre$i
+ GRE_IFACE=$resp
+
+ echo "$PMTU_SYSCTL = 1" > /etc/sysctl.d/dmvpn.conf
+ sysctl -w $PMTU_SYSCTL=1
+
+ cat >> /etc/network/interfaces <<EOF
+
+auto $GRE_IFACE
+iface $GRE_IFACE inet static
+ address $GRE_IPV4_ADDRESS
+ netmask 255.255.255.255
+ tunnel-mode gre
+ tunnel-dev $TRANSPORT_IFACE
+ tunnel-ttl 64
+EOF
+
+ if [ "$GRE_IPV6_ADDRESS" ]; then
+ cat >> /etc/network/interfaces <<EOF
+iface $GRE_IFACE inet6 static
+ address $GRE_IPV6_ADDRESS
+ netmask 128
+EOF
+ fi
+
+ ifup $GRE_IFACE
+fi
+
+
+augtool -s <<EOF
+set /files/etc/conf.d/nhrpd/rc_need '"charon nhrp-events"'
+set /files/etc/strongswan.d/charon.conf/charon/x509/enforce_critical no
+EOF
+
+for serv in bgpd nhrpd zebra; do
+ file=/etc/quagga/$serv.conf
+ touch $file
+ chown quagga $file
+done
+
+enable_service nhrpd
+
+vtysh -c "$(get_quagga_config)"
+
+
+if [ "$NFLOG_GROUP" ]; then
+ apk add awall
+ cat > /etc/awall/dmvpn.json <<EOF
+{
+ "variable": {
+ "dmvpn_gre_iface": "$GRE_IFACE",
+ "dmvpn_nflog_group": $NFLOG_GROUP,
+ "dmvpn_site_mask": { "inet": $SITE_PREFIX_LEN_IPV4 }
+ }
+}
+EOF
+ [ "$SITE_PREFIX_LEN_IPV6" ] && augtool -s <<EOF
+set /files/etc/awall/dmvpn.json/dict/entry/dict/entry['dmvpn_site_mask']/dict/entry[2] inet6
+set /files/etc/awall/dmvpn.json/dict/entry/dict/entry['dmvpn_site_mask']/dict/entry[2]/number $SITE_PREFIX_LEN_IPV6
+EOF
+
+ awall enable dmvpn-hub
+ awall translate
+ enable_firewall iptables
+ [ "$SITE_PREFIX_LEN_IPV6" ] && enable_firewall ip6tables
+fi