diff options
-rwxr-xr-x | dependencies.sh | 5 | ||||
-rw-r--r-- | dmvpn-hub.awall | 43 | ||||
-rwxr-xr-x | dmvpn-pfx-decode | 94 | ||||
-rw-r--r-- | dmvpn.swanctl | 26 | ||||
-rw-r--r-- | nhrp-events.initd | 13 | ||||
-rwxr-xr-x | setup-dmvpn | 248 |
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 |