macosperlmacos-sierrappppptp

How to programmatically create a PPTP VPN connection on macOS Sierra/High Sierra?


Apple removed high-level PPTP support in macOS Sierra from its network configuration system. However, the PPP internals are all still there, including /usr/sbin/pppd and /etc/ppp/.

How can I programmatically initiate a PPTP VPN connection on macOS Sierra / High Sierra using what's left?


Solution

  • Answer:

    This method creates a PPTP connection that doesn't send all traffic and doesn't override other DNS providers, meaning it works with multiple simultaneous VPN connections each having different DNS search domains, and closes it in an orderly fashion.

    Not sending all traffic requires you to know the VPN subnet beforehand. If you don't, you must send all traffic (see below), since vanilla PPP/LCP has no means to tell the client its subnet (although theoretically the ip-up and ip-down scripts could guess it from the received IP address).

    Save this perl as /usr/local/bin/pptp:

    #!/usr/bin/env perl
    if (@ARGV) {
        my $name = $ARGV[0];
        if (length $name && -e "/etc/ppp/peers/$name") {
            my $pid;
            $SIG{"INT"} = "IGNORE";
            die "fork: $!" unless defined ($pid = fork);
            if ($pid) { # parent
                $SIG{"INT"} = sub {
                    kill HUP => $pid;
                };
                wait;
                exit;
            } else { #child
                $SIG{"INT"} = "DEFAULT";
                exec "pppd", "call", $name;
                exit;
            }
        } else {
            print "Error: PPTP name: $name\n";
        }
    } else {
        opendir my $d, "/etc/ppp/peers" or die "Cannot read /etc/ppp/peers";
        while (readdir $d) {
            print "$_\n" if !($_ eq "." || $_ eq "..");
        }
        closedir $d;
    }
    

    Run it as sudo pptp AcmeOffice, where AcmeOffice is the PPP connection name, and close it with a single Control-C/SIGINT.

    In /etc/ppp/peers, create the PPP connection file, in this example /etc/ppp/peers/AcmeOffice:

    plugin /System/Library/SystemConfiguration/PPPController.bundle/Contents/PlugIns/PPPDialogs.ppp
    plugin PPTP.ppp
    noauth
    # debug 
    redialcount 1
    redialtimer 5
    idle 1800
    #mru 1320
    mtu 1320
    receive-all
    novj 0:0
    ipcp-accept-local
    ipcp-accept-remote
    refuse-pap
    refuse-chap
    #refuse-chap-md5
    refuse-eap
    hide-password
    #noaskpassword
    #mppe-stateless 
    mppe-128 
    mppe-stateful 
    require-mppe 
    passive 
    looplocal 
    nodetach
    # defaultroute
    #replacedefaultroute
    # ms-dns 8.8.8.8
    # usepeerdns
    noipdefault
    # logfile /tmp/ppp.AcmeOffice.log 
    ipparam AcmeOffice
    remoteaddress office.acme.com
    user misteracme
    password acme1234
    

    The last 4 options are connection-specific. Note the password is stored cleartext. chown root:wheel and chmod 600 is recommended. nodetach, ipcp-accept-local, ipcp-accept-remote, noipdefault are critical.

    Since we're not becoming/replacing the default route, you must manually change your routing table. Add an AcmeOffice entry to the /etc/ppp/ip-up script:

    #!/bin/sh
    #params: interface-name tty-device speed local-IP-address remote-IP-address ipparam
    
    PATH=$PATH:/sbin:/usr/sbin
    
    case "$6" in
        AcmeOffice)
            route -n add -net 192.168.1.0/24 -interface "$1"
            ;;
        AcmeLab)
            route -n add -net 192.168.2.0/24 -interface "$1"
            ;;
        AcmeOffshore)
            route -n add -net 192.168.3.0/24 -interface "$1"
            ;;
        VPNBook)
            ;;
        *)
            ;;
    esac
    

    and your /etc/ppp/ip-down script:

    #!/bin/sh
    #params: interface-name tty-device speed local-IP-address remote-IP-address ipparam
    
    PATH=$PATH:/sbin:/usr/sbin
    
    case "$6" in
        AcmeOffice)
            route -n delete -net 192.168.1.0/24 -interface "$1"
            ;;
        AcmeLab)
            route -n delete -net 192.168.2.0/24 -interface "$1"
            ;;
        AcmeOffshore)
            route -n delete -net 192.168.3.0/24 -interface "$1"
            ;;
        VPNBook)
            ;;
        *)
            ;;
    esac
    

    If the VPN has a DNS search domain (i.e. somehost.office.acme.com), create a file in /etc/resolver/ named after the DNS suffix, like /etc/resolver/office.acme.com, with contents like:

    nameserver 192.168.1.1
    domain office.acme.com
    

    Note that this requires knowing the destination domain & nameserver beforehand. Theoretically ip-up & ip-down could create & delete this file on demand.

    To send all traffic (& if you don't know the destination subnet), uncomment #defaultroute in the PPP connection file and leave the ip-up & ip-down entries blank (e.g. the VPNBook example). To override your DNS with the VPN's, uncomment usepeerdns.