Table of Contents

About

This is a guide on how to make OS X clients dial-in using OpenVPN to an OpenVPN server and ask for IP and DNS addresses via ISC DHCPd. The advantage of using this setup is that you can connect two machines together over the Internet such that the client is on the same network as the server allowing services to work seamlessly over the tunnel.

Requirements

You will need:

Setting up The OpenVPN Server

To setup the bridge on the server on a Debian distribution, you would open up /etc/network/interfaces and add the following:

auto br0
iface br0 inet static
        bridge_ports eth0
        bridge_stp off
        bridge_maxwait 0
        bridge_fd 0
        address 192.168.1.1
        broadcast 192.168.1.255
        netmask 255.255.255.0

where the following have to be changed:

Next, OpenVPN has to be set-up. For this configuration we will be using a pre-shared static key. To generate a key issue:

openvpn --genkey --secret vpn.domain.tld.key

where vpn.domain.tld.key can be changed to match your VPN domain.

After that, create a new file in /etc/openvpn, for example a file at /etc/openvpn/vpn.domain.tld.conf with the following contents:

cd /etc/openvpn
dev tap0
port 1194
secret keys/vpn.domain.tld.key
keepalive 10 120
comp-lzo adaptive
cipher BF-CBC
script-security 2
up scripts/bridge-if-up.sh
down scripts/bridge-if-down.sh
verb 4

where vpn.domain.tld.key points to the key-file generated in the previous step. The two scripts bridge-if-up.sh and bridge-if-down.sh are meant to add the tap0 interface to br0 once the VPN is up respectively remove it once the VPN is down.

Bridge Scripts

For the bridge-if-up.sh we have:

bridge-if-up.sh
#!/bin/sh
 
brctl addif br0 $1
ifconfig $1 promisc up

For the bridge-if-up.sh we have:

bridge-if-down.sh
#!/bin/sh
 
brctl delif br0 $1

Setting up the OS X Client

To setup the OS X clients we will use homebrew to install OpenVPN by issuing:

brew install openvpn

and follow the on-screen instructions to make OpenVPN start on boot.

Since OS X does not have a tap device, we will have to install tuntap from homebrew as well:

brew install Caskroom/cask/tuntap

Next, we go to /usr/local/etc/openvpn and create a configuration file:

cd /usr/local/etc/openvpn
dev tap0
remote vpn.domain.tld 1194
secret keys/vpn.domain.tld.key
keepalive 10 120
comp-lzo adaptive
cipher BF-CBC
route-noexec
script-security 2
up-restart
ipchange scripts/openvpn-osx-tuntap.sh
up scripts/openvpn-osx-tuntap.sh
down scripts/openvpn-osx-tuntap.sh
verb 4

where:

OpenVPN OSX Up-Down Script

Finally, the script needed to make OS X use DHCP on the tap interface is the following:

14 February 2017

  • Initial commit.
#!/bin/sh
###########################################################################
##  Copyright (C) Wizardry and Steamworks 2017 - License: GNU GPLv3      ##
##  Please see: http://www.gnu.org/licenses/gpl.html for legal details,  ##
##  rights of fair usage, the disclaimer and warranty conditions.        ##
###########################################################################
# openvpn-osx-tuntap.sh                                                   #
# Up / Down script for OpenVPN running on OSX as a client.                #
#                                                                         #
# The OpenVPN server is expected to serve clients with IP addresses via   #
# a previously configured DHCP server.                                    #
#                                                                         #
# Note that this script does not replace the default route but instead    #
# uses the Mac OS capability of using scoped DNS resolution in order to   #
# make the remote network available whilst preserving the default route.  #
#                                                                         #
# To use this script on an OS X OpenVPN client, the following changes to  #
# your OpenVPN configuration are required:                                #
#                                                                         #
# up-restart                                                              #
# up openvpn-osx-tuntap.sh                                                #
# down openvpn-osx-tuntap.sh                                              #
# ipchange openvpn-osx-tuntap.sh                                          #
#                                                                         #
# where openvpn-osx-tuntap.sh is the filesystem path to this script.      #
#                                                                         #
# Based on:                                                               #
# - 2006-09-21, Ben Low - original version                                #
# - Nick Williams - for TunnelBrick                                       #
# - Jonathan K. Bullard - additions for Mountain Lion                     #
###########################################################################
 
###########################################################################
#                            CONFIGURATION                                #
###########################################################################
# A MAC address for the tunnel interface.
STATIC_TUNTAP_MAC_ADDRESS="42:75:e2:67:43:db"
# Whether to prepend domain names instead of replacing the exiting ones.
PREPEND_DOMAIN_NAME="false"
 
###########################################################################
#                             INTERNALS                                   #
###########################################################################
 
# Regular expression for matching IP addresses.
IPRX="(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
# Domain name regular expression.
DOMRX="(?:[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]|[A-Za-z0-9])"
 
###########################################################################
# Utility function to flush the DNS cache on various Mac OS releases.     #
###########################################################################
flushDNSCache()
{
    if [ "${OSVER}" = "10.4" ] ; then
        if [ -f /usr/sbin/lookupd ] ; then
            set +e # we will catch errors from lookupd
            /usr/sbin/lookupd -flushcache
            set -e # bash should again fail on errors
        fi
    else
        if [ -f /usr/bin/dscacheutil ] ; then
            set +e # we will catch errors from dscacheutil
            /usr/bin/dscacheutil -flushcache
            set -e # bash should again fail on errors
        fi
        if [ -f /usr/sbin/discoveryutil ] ; then
            set +e # we will catch errors from discoveryutil
            /usr/sbin/discoveryutil udnsflushcaches
            /usr/sbin/discoveryutil mdnsflushcache
            set -e # bash should again fail on errors
        fi
        set +e # "grep" will return error status (1) if no matches are found, so don't fail on individual errors
        hands_off_ps="$( ps -ax | grep HandsOffDaemon | grep -v grep.HandsOffDaemon )"
        set -e # We instruct bash that it CAN again fail on errors
        if [ -z "${hands_off_ps}" ] ; then
            if [ -f /usr/bin/killall ] ; then
                set +e # ignore errors if mDNSResponder isn't currently running
                /usr/bin/killall -HUP mDNSResponder
                set -e # bash should again fail on errors
            fi
        fi
    fi
}
 
###########################################################################
# Sets all dynamic DHCP options on the tuntap interface.                  #
###########################################################################
setDnsServersAndDomainName()
{
    readonly PSID="DHCP-$dev"
 
    # Set up the DYN_* variables to contain what is asked for (dynamically, by a 'push' directive, for example)
    declare -a vDNS=("${!1}")
    declare -a vSMB=("${!3}")
    declare -a vSD=("${!4}")
 
    if [ ${#vDNS[*]} -eq 0 ] ; then
        readonly DYN_DNS_SA=""
    else
        readonly DYN_DNS_SA="${!1}"
    fi
 
    if [ ${#vSMB[*]} -eq 0 ] ; then
        readonly DYN_SMB_WA=""
    else
        readonly DYN_SMB_WA="${!3}"
    fi
 
    if [ ${#vSD[*]} -eq 0 ] ; then
        readonly DYN_DNS_SD=""
    else
        readonly DYN_DNS_SD="${!4}"
    fi
 
    DYN_DNS_DN="$2"
 
    # set up the FIN_* variables with what we want to set things to
    # Three FIN_* variables are simple -- no aggregation is done for them
    if [ ! -z "${DYN_DNS_DN}" ] ; then
        readonly FIN_DNS_DN="${DYN_DNS_DN}"
    else
        readonly FIN_DNS_DN=""
    fi
 
    if [ ! -z "${DYN_SMB_NN}" ] ; then
        readonly FIN_SMB_NN="${DYN_SMB_NN}"
    else
        readonly FIN_SMB_NN=""
    fi
 
    if [ ! -z "${DYN_SMB_WG}" ] ; then
        readonly FIN_SMB_WG="${DYN_SMB_WG}"
    else
        readonly FIN_SMB_WG=""
    fi
 
    # DNS ServerAddresses (FIN_DNS_SA) are aggregated for 10.4 and 10.5
    if [ ${#vDNS[*]} -eq 0 ] ; then
        readonly FIN_DNS_SA=""
    else
        case "${OSVER}" in
            10.4 | 10.5 )
                # We need to remove duplicate DNS entries, so that our reference list matches MacOSX's
                SDNS="$( echo -n "${DYN_DNS_SA}" | tr ' ' '\n' )"
                i=0
                for n in "${vDNS[@]}" ; do
                    if echo -n "${SDNS}" | grep -q "${n}" ; then
                        unset vDNS[${i}]
                    fi
                    let i++
                done
                if [ ${#vDNS[*]} -gt 0 ] ; then
                    readonly FIN_DNS_SA="$( echo -n "${DYN_DNS_SA}" | sed s/"${vDNS[*]}"//g )"
                else
                    readonly FIN_DNS_SA="${DYN_DNS_SA}"
                fi
                ;;
            * )
                # Do nothing - in 10.6 and higher -- we don't aggregate our configurations, apparently
                readonly FIN_DNS_SA="${DYN_DNS_SA}"
                ;;
        esac
    fi
 
    # SMB WINSAddresses (FIN_SMB_WA) are aggregated for 10.4 and 10.5
    if [ ${#vSMB[*]} -eq 0 ] ; then
        readonly FIN_SMB_WA=""
    else
        case "${OSVER}" in
            10.4 | 10.5 )
                # We need to remove duplicate SMB entries, so that our reference list matches MacOSX's
                SSMB="$( echo -n "${DYN_SMB_WA}" | tr ' ' '\n' )"
                i=0
                for n in "${vSMB[@]}" ; do
                    if echo -n "${SSMB}" | grep -q "${n}" ; then
                        unset vSMB[${i}]
                    fi
                    let i++
                done
                if [ ${#vSMB[*]} -gt 0 ] ; then
                    readonly FIN_SMB_WA="$( echo -n "${DYN_SMB_WA}" | sed s/"${vSMB[*]}"//g )"
                else
                    readonly FIN_SMB_WA="${DYN_SMB_WA}"
                fi
                ;;
            * )
                # Do nothing - in 10.6 and higher -- we don't aggregate our configurations, apparently
                readonly FIN_SMB_WA="${DYN_SMB_WA}"
                ;;
        esac
    fi
 
    # DNS SearchDomains (FIN_DNS_SD) is treated specially
    #
    # OLD BEHAVIOR:
    #     if SearchDomains was not set manually, we set SearchDomains to the DomainName
    #     else
    #          In OS X 10.4-10.5, we add the DomainName to the end of any manual SearchDomains (unless it is already there)
    #          In OS X 10.6+, if SearchDomains was entered manually, we ignore the DomainName 
    #                         else we set SearchDomains to the DomainName
    #
    # NEW BEHAVIOR (done if ARG_PREPEND_DOMAIN_NAME is "true"):
    #
    #     if SearchDomains was entered manually, we do nothing
    #     else we  PREpend new SearchDomains (if any) to the existing SearchDomains (NOT replacing them)
    #          and PREpend DomainName to that
    #
    #              (done if ARG_PREPEND_DOMAIN_NAME is "false" and there are new SearchDomains from DOMAIN-SEARCH):
    #
    #     if SearchDomains was entered manually, we do nothing
    #     else we  PREpend any new SearchDomains to the existing SearchDomains (NOT replacing them)
    #
    #     This behavior is meant to behave like Linux with Network Manager and Windows
    if "${PREPEND_DOMAIN_NAME}" ; then
            if [ ! -z "${DYN_DNS_SD}" ] ; then
                readonly TMP_DNS_SD="${DYN_DNS_SD}"
                if [ ! -z "${FIN_DNS_DN}" -a  "${FIN_DNS_DN}" != "localdomain" ]; then
                    if ! echo -n "${TMP_DNS_SD}" | tr ' ' '\n' | grep -q "${FIN_DNS_DN}" ; then
                        readonly FIN_DNS_SD="$( echo -n "${FIN_DNS_DN}" | sed s/"${TMP_DNS_SD}"//g )"
                    else
                        readonly FIN_DNS_SD="${TMP_DNS_SD}"
                    fi
                else
                    readonly FIN_DNS_SD="${TMP_DNS_SD}"
                fi
            else
                readonly FIN_DNS_SD="${DYN_DNS_SD}"
            fi
    else
        if [ ! -z "${DYN_DNS_SD}" ] ; then
            readonly FIN_DNS_SD="${DYN_DNS_SD}"
        else
            if [ ! -z "${FIN_DNS_DN}" -a "${FIN_DNS_DN}" != "localdomain" ] ; then
                case "${OSVER}" in
                    10.4 | 10.5 )
                        readonly FIN_DNS_SD="${FIN_DNS_DN}"
                        ;;
                    * )
                        readonly FIN_DNS_SD="${FIN_DNS_DN}"
                        ;;
                esac
            else
                readonly FIN_DNS_SD=""
            fi
        fi
    fi
 
    # Set up SKP_* variables to inhibit scutil from making some changes
    # SKP_DNS_* and SKP_SMB_* are used to comment out individual items 
    # that are not being set
    if [ -z "${FIN_DNS_DN}" ] ; then
        SKP_DNS_DN="#"
    else
        SKP_DNS_DN=""
    fi
    if [ -z "${FIN_DNS_SA}" ] ; then
        SKP_DNS_SA="#"
    else
        SKP_DNS_SA=""
    fi
    if [ -z "${FIN_DNS_SD}" ] ; then
        SKP_DNS_SD="#"
    else
        SKP_DNS_SD=""
    fi
    if [ -z "${FIN_SMB_NN}" ] ; then
        SKP_SMB_NN="#"
    else
        SKP_SMB_NN=""
    fi
    if [ -z "${FIN_SMB_WG}" ] ; then
        SKP_SMB_WG="#"
    else
        SKP_SMB_WG=""
    fi
    if [ -z "${FIN_SMB_WA}" ] ; then
        SKP_SMB_WA="#"
    else
        SKP_SMB_WA=""
    fi
 
    # if any DNS items should be set, set all that have values
    if [ "${SKP_DNS_DN}${SKP_DNS_SA}${SKP_DNS_SD}" = "###" ] ; then
        readonly SKP_DNS="#"
    else
        readonly SKP_DNS=""
        if [ ! -z "${FIN_DNS_DN}" ] ; then
            SKP_DNS_DN=""
        fi
        if [ ! -z "${FIN_DNS_SA}" ] ; then
            SKP_DNS_SA=""
        fi
        if [ ! -z "${FIN_DNS_SD}" ] ; then
            SKP_DNS_SD=""
        fi
    fi
 
    # if any SMB items should be set, set all that have values
    if [ "${SKP_SMB_NN}${SKP_SMB_WG}${SKP_SMB_WA}" = "###" ] ; then
        readonly SKP_SMB="#"
    else
        readonly SKP_SMB=""
        if [ ! -z "${FIN_SMB_NN}" ] ; then
            SKP_SMB_NN=""
        fi
        if [ ! -z "${FIN_SMB_WG}" ] ; then
            SKP_SMB_WG=""
        fi
        if [ ! -z "${FIN_SMB_WA}" ] ; then
            SKP_SMB_WA=""
        fi
    fi
 
    readonly SKP_DNS_SA SKP_DNS_SD SKP_DNS_DN
    readonly SKP_SMB_NN SKP_SMB_WG SKP_SMB_WA
 
    # special-case fiddling:
    # 10.8+ : ServerAddresses and SearchDomains must be set via the Setup:
    #     key in addition to the State: key
    # 10.7  : if ServerAddresses or SearchDomains are manually set,
    #     ServerAddresses and SearchDomains must be similarly set with the
    #     Setup: key in addition to the State: key
    case "${OSVER}" in
        10.4 | 10.5 | 10.6 | 10.7 )
            readonly SKP_SETUP_DNS="#"
            ;;
        * )
            readonly SKP_SETUP_DNS=""
            ;;
    esac
 
    # Set all parameters.
    /usr/sbin/scutil >/dev/null 2>&1 <<-EOF
        open
 
        # Initialize the new DNS map via State:
        ${SKP_DNS}d.init
        ${SKP_DNS}${SKP_DNS_SA}d.add ServerAddresses * ${FIN_DNS_SA}
        ${SKP_DNS}${SKP_DNS_SD}d.add SearchDomains   * ${FIN_DNS_SD}
        ${SKP_DNS}${SKP_DNS_DN}d.add DomainName        ${FIN_DNS_DN}
        ${SKP_DNS}${SKP_DNS_DN}d.add SupplementalMatchDomains * ${FIN_DNS_DN}
        ${SKP_DNS}set State:/Network/Service/${PSID}/DNS
 
        # If necessary, initialize the new DNS map via Setup: also
        ${SKP_SETUP_DNS}${SKP_DNS}d.init
        ${SKP_SETUP_DNS}${SKP_DNS}${SKP_DNS_SA}d.add ServerAddresses * ${FIN_DNS_SA}
        ${SKP_SETUP_DNS}${SKP_DNS}${SKP_DNS_SD}d.add SearchDomains   * ${FIN_DNS_SD}
        ${SKP_SETUP_DNS}${SKP_DNS}${SKP_DNS_DN}d.add DomainName        ${FIN_DNS_DN}
        ${SKP_SETUP_DNS}${SKP_DNS}set Setup:/Network/Service/${PSID}/DNS
 
        # Initialize the SMB map
        ${SKP_SMB}d.init
        ${SKP_SMB}${SKP_SMB_NN}d.add NetBIOSName     ${FIN_SMB_NN}
        ${SKP_SMB}${SKP_SMB_WG}d.add Workgroup       ${FIN_SMB_WG}
        ${SKP_SMB}${SKP_SMB_WA}d.add WINSAddresses * ${FIN_SMB_WA}
        ${SKP_SMB}set State:/Network/Service/${PSID}/SMB
 
        quit
EOF
 
}
 
# If OpenVPN has not brought up the device, then terminate.
if [ -z "$dev" ]; then 
    echo "$0: \$dev not defined, exiting"; 
    exit 1; 
fi
 
# OpenVPN passes $script_type set to the script method.
case "$script_type" in
    ipchange)
        # Set the MAC address for the tuntap device for static DHCP bindings
        /sbin/ifconfig "$dev" ether $STATIC_TUNTAP_MAC_ADDRESS
        # Set the interface to NONE
        /usr/sbin/ipconfig set "$dev" NONE
        # Set the interface to DHCP
        /usr/sbin/ipconfig set "$dev" DHCP
    ;;
    up)
 
        # Set the MAC address for the tuntap device for static DHCP bindings
        /sbin/ifconfig "$dev" ether $STATIC_TUNTAP_MAC_ADDRESS
        # Set the interface to NONE
        /usr/sbin/ipconfig set "$dev" NONE
        # Set the interface to DHCP
        /usr/sbin/ipconfig set "$dev" DHCP
 
        {
            # Issue the waitall command - even if it does not wait.
            /usr/sbin/ipconfig waitall
 
            unset PACKET
 
            # Spin and check for packet from the tap device
            set +e
            n=0
            while [ -z "$PACKET" -a $n -lt 60 ] ; do
                PACKET="$( /usr/sbin/ipconfig getpacket "$dev" )"
                let n++
                sleep 1
            done
            set -e
 
            # Get packet to set options
            if [ -z "$PACKET" ]; then
                exit 1
            fi
 
            unset DOMAIN_NAME
            unset DOMAIN_NAME_SERVERS
            unset SEARCH_DOMAINS
            unset WINS_SERVERS
 
            set +e
            # Get domain name
            DOMAIN_NAME="$( echo -n "$PACKET" | grep "domain_name " | grep -Eo ": $DOMRX" | grep -Eo "$DOMRX" | tr -d [:space:] )"
 
            # Get nameservers
            DOMAIN_NAME_SERVERS_INDEX=1
            for DOMAIN_NAME_SERVER in $( echo -n "$PACKET" | grep "domain_name_server" | grep -Eo "\{($IPRX)(, $IPRX)*\}" | grep -Eo "($IPRX)" ); do
                DOMAIN_NAME_SERVERS[DOMAIN_NAME_SERVERS_INDEX-1]=$DOMAIN_NAME_SERVER
                let DOMAIN_NAME_SERVERS_INDEX++
            done
 
            # Get search domains
            SEARCH_DOMAINS_INDEX=1
            for SEARCH_DOMAIN in $( echo -n "$PACKET" | grep "search_domain" | grep -Eo "\{($DOMRX)(, $DOMRX)*\}" | grep -Eo "($DOMRX)" ); do
                SEARCH_DOMAINS[SEARCH_DOMAINS_INDEX-1]=$SEARCH_DOMAIN
                let SEARCH_DOMAINS_INDEX++
            done
 
            # Get WINS servers
            WINS_SERVERS_INDEX=1
            for WINS_SERVER in $( echo -n "$PACKET" | grep "nb_over_tcpip_name_server" | grep -Eo "\{($IPRX)(, $IPRX)*\}" | grep -Eo "($IPRX)" ); do
                WINS_SERVERS[WINS_SERVERS_INDEX-1]=$WINS_SERVER
                let WINS_SERVERS_INDEX++
            done
 
            if [ ${#DOMAIN_NAME_SERVERS[*]} -gt 0 -a "$DOMAIN_NAME" ]; then
                setDnsServersAndDomainName DOMAIN_NAME_SERVERS[@] "$DOMAIN_NAME" WINS_SERVERS[@] SEARCH_DOMAINS[@]
            elif [ ${#DOMAIN_NAME_SERVERS[*]} -gt 0 ]; then
                setDnsServersAndDomainName DOMAIN_NAME_SERVERS[@] "$DEFAULT_DOMAIN_NAME" WINS_SERVERS[@] SEARCH_DOMAINS[@]
            else
                exit 1
            fi
 
            set -e
 
            sleep 1
 
            flushDNSCache
 
            exit 0
        } &
    ;;
    down)
        sleep 1
 
        /usr/sbin/scutil >/dev/null 2>&1 <<-EOF
            open
            remove State:/Network/Service/DHCP-$dev/IPv4
            remove State:/Network/Service/DHCP-$dev/DNS
            close
EOF
 
        flushDNSCache
 
        exit 0
    ;;
    *) 
        echo "$0: invalid script_type" && exit 1 
    ;;
esac

Running a Permanently Connected Client

You will need to move the OpenVPN launch control file from /usr/local/Cellar/openvpn/*/homebrew.mxcl.openvpn.plist (provided you use homebrew) and into /Library/LaunchDaemons and edit it as follows:

homebrew.mxcl.openvpn.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd";>
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>homebrew.mxcl.openvpn</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/sbin/openvpn</string>
    <string>--config</string>
    <string>/usr/local/etc/openvpn/client.conf</string>
  </array>
  <key>KeepAlive</key>
  <true/>
  <key>OnDemand</key>
  <false/>
  <key>RunAtLoad</key>
  <true/>
  <key>TimeOut</key>
  <integer>90</integer>
  <key>WatchPaths</key>
  <array>
    <string>/usr/local/etc/openvpn</string>
  </array>
  <key>WorkingDirectory</key>
  <string>/usr/local/etc/openvpn</string>
</dict>
</plist>

aside from punching-in the correct path to the OpenVPN daemon, the launch control file contains the following option:

  <key>KeepAlive</key>
  <true/>

that will make OS X restart OpenVPN whenever it goes down (either on successful exit or otherwise).

To activate the control file, terminate any lingering OpenVPN processes and issue:

launchctl load -w /Library/LaunchDaemons/homebrew.mxcl.openvpn.plist

and OpenVPN should start and connect.