Whenever you find yourself on a sketchy public WiFi hotspot or a heavily-filtered network, using a VPN is a relatively sure way to tunnel your traffic to a safe location. It’s probably best not to trust a free provider, who will be selling your traffic information to advertisers, and why pay for a VPN when you can create one yourself with little to no bandwidth limitation and a trustworthy host? Setting up a VPN can be challenging, however, with a myriad of incompatible protocols and lots of outdated information. Most guides online are for either OpenVPN or PPTP, neither of which are supported on modern iOS (10+).

As usual, the OpenBSD folks are here to help with a sanely designed IKEv2 server (and client) in the form of OpenIKED, which conveniently comes in the base system. We also get to use the not-insanely-braindead PF firewall, which is always a plus.

After a long time searching the web for a pre-made solution to this problem (because I was feeling lazy), I found an article that had a perfect solution to the problem. In an attempt to get the word out about the world’s easiest way to make an IKEv2 VPN, I’ll reproduce it here. Calum MacRae on Medium created an Ansible playbook to automate this task, which I very slightly modified into the following:

---

- hosts: v.snazz.xyz
  gather_facts: false

  vars:
    iked_psk: put_your_password_here

  handlers:
    - name: Reload pf configuration
      shell: pfctl -f /etc/pf.conf

    - name: Reload iked service
      service:
        name: iked
        state: reloaded

    - name: Reload network configuration
      shell: sh /etc/netstart

  tasks:
    - block:
        - name: Ensure sysctl params are set
          sysctl:
            name: net.inet.
            value: 1
          with_items:
            - ip.forwarding
            - esp.enable
            - ah.enable
            - ipcomp.enable

        - name: Ensure /etc/badguys exists
          copy:
            dest: /etc/badguys
            owner: root
            group: wheel
            mode: 0640
            force: false
            content: ""

        - name: Ensure the enc0 interface is configured
          copy:
            dest: /etc/hostname.enc0
            owner: root
            group: wheel
            mode: 0640
            content: |
              inet 10.0.1.1 255.255.255.0 10.0.1.255
              up
          notify: Reload network configuration

        - name: Ensure pf is configured
          copy:
            dest: /etc/pf.conf
            owner: root
            group: wheel
            mode: 0644
            content: |
              intra = "vio0"
              vpn = "enc0"

              set reassemble yes
              set block-policy return
              set loginterface egress
              set skip on { lo, enc }

              match in all scrub (no-df random-id max-mss 1440)

              table <ossec_fwtable> persist
              table <badguys> persist file "/etc/badguys"

              block in quick  on egress from <badguys> label "bad"
              block out quick on egress to <badguys> label "bad"
              block in quick  on egress from <ossec_fwtable> label "bad"
              block out quick on egress to <ossec_fwtable> label "bad"
              block in quick from urpf-failed label uRPF
              block return log

              pass out all modulate state

              pass in on egress proto { ah, esp }
              pass in on egress proto udp to (egress) port { isakmp, ipsec-nat-t }
              pass out on egress from 10.0.1.0/24 to any nat-to (egress)
              pass out on $intra from 10.0.1.0/24 to $intra:network nat-to ($intra)

              pass in quick inet proto icmp icmp-type { echoreq, unreach }

              pass in on egress proto tcp from any to (egress) port 22 keep state (max-src-conn 40, max-src-conn-rate 10/30, overload <badguys> flush global)
              pass in on $intra proto { udp tcp } from any to ($intra) port 53
          notify: Reload pf configuration

        - name: Ensure iked is configured
          copy:
            dest: /etc/iked.conf
            owner: root
            group: wheel
            mode: 0600
            content: |
              ikev2 "inet" passive ipcomp esp \
              from 0.0.0.0/0 to 10.0.1.0/24 \
              from 10.0.0.0/24 to 10.0.1.0/24 \
              local egress peer any \
              psk "" \
              config protected-subnet 0.0.0.0/0 \
              config address 10.0.1.0/24 \
              config name-server 1.1.1.1 \
              tag "IKED" tap enc0
          notify: Reload iked service

        - name:  Ensure iked service is running/enabled
          service:
            name: iked
            state: started
            enabled: true

      tags: vpn

Save this as ipsec.yml and run Ansible on your workstation with ansible-playbook -i vpn.example.com, -u root -k -e ansible_network_os=openbsd ipsec.yml. You may need to install sshpass on the machine you’re running Ansible from.

The only changes I made were to use a password stored in the playbook (instead of Ansible Vault), for simplicity, and to replace the DNS server with Cloudflare’s DNS offering.

On the iOS device, head to General, VPN, Add VPN Configuration. Use IKEv2 as the type, enter whatever in the Description box, add the (internet-facing) hostname of your server in the Server and Remote ID boxes, keep Local ID blank, and turn User Authentication to None. Disable Use Certificate and type your password (from the top of the playbook) into the Secret box. When you’re done, connect to your VPN and enjoy.

All credit for the original solution goes to Calum MacRae.