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.
You will need:
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:
eth0
is the internal interface192.168.1.1
is the address to give to the internal interfaces once it is masked by the bridge192.168.1.255
is the broadcast address255.255.255.0
is the netmarkNext, 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.
For the bridge-if-up.sh
we have:
#!/bin/sh brctl addif br0 $1 ifconfig $1 promisc up
For the bridge-if-up.sh
we have:
#!/bin/sh brctl delif br0 $1
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:
vpn.domain.tld
is the IP address or hostname of the OpenVPN servervpn.domain.tld.key
is the same key-file that has been used on the OpenVPN serveropenvpn-osx-tuntap.sh
is the script from the code section.Finally, the script needed to make OS X use DHCP on the tap interface is the following:
14 February 2017
#!/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
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:
<?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.