#!/usr/bin/env bash
# Published at https://www.ghostshield.ai/research/vpn-protocol-benchmark.sh with the results it produced
# on 26 October 2026: https://www.ghostshield.ai/learn/wireguard-vs-openvpn-benchmark-2026
# Run as root on an Ubuntu 24.04 machine with iperf3, wireguard-tools and openvpn installed. Everything
# it creates (a network namespace, a veth pair, keys and throwaway certificates) is removed at the end.
# WireGuard vs OpenVPN on one host, through network namespaces (the WireGuard project's own test
# harness works the same way). Measures throughput, ping and CPU per protocol and cipher.
# Everything it creates is torn down at the end. Results: /tmp/bench/results.jsonl + raw iperf JSON.
set -u
B=/tmp/bench; rm -rf "$B"; mkdir -p "$B"; cd "$B"
DUR=${DUR:-15}
log() { echo "$(date -u +%H:%M:%S) $*"; }
cleanup() {
  pkill -f "openvpn --config $B" 2>/dev/null; pkill -f "iperf3 -s" 2>/dev/null
  ip link del wg0 2>/dev/null; ip netns del peer 2>/dev/null; ip link del veth0 2>/dev/null
}
trap cleanup EXIT
cleanup

# --- the wire: a veth pair, one end in the "peer" namespace --------------------------------
ip netns add peer
ip link add veth0 type veth peer name veth1
ip link set veth1 netns peer
ip addr add 10.200.0.1/24 dev veth0; ip link set veth0 up
ip netns exec peer ip addr add 10.200.0.2/24 dev veth1; ip netns exec peer ip link set veth1 up; ip netns exec peer ip link set lo up

cpu_sample() { awk '/^cpu /{print $2+$3+$4+$6+$7+$8, $2+$3+$4+$5+$6+$7+$8}' /proc/stat; }
run_test() { # name, iperf target ip, ping ip, streams
  local name=$1 ip=$2 pip=$3 P=$4
  local before after busy total
  ip netns exec peer iperf3 -s -D -1 -B "$ip" >/dev/null 2>&1; sleep 0.5
  before=$(cpu_sample)
  iperf3 -c "$ip" -t "$DUR" -P "$P" -J > "$B/$name.P$P.json" 2>&1
  after=$(cpu_sample)
  busy=$(( $(echo $after | cut -d' ' -f1) - $(echo $before | cut -d' ' -f1) )); total=$(( $(echo $after | cut -d' ' -f2) - $(echo $before | cut -d' ' -f2) ))
  local mbps rtt
  mbps=$(python3 -c "import json;d=json.load(open('$B/$name.P$P.json'));print(round(d['end']['sum_received']['bits_per_second']/1e6,1))" 2>/dev/null || echo null)
  rtt=$(ping -c 30 -i 0.2 -q "$pip" | awk -F'/' '/rtt|round-trip/{print $5}')
  local cpu; cpu=$(python3 -c "print(round(100*$busy/max($total,1),1))")
  echo "{\"test\":\"$name\",\"streams\":$P,\"mbps\":$mbps,\"ping_ms\":${rtt:-null},\"cpu_pct\":$cpu,\"seconds\":$DUR}" | tee -a "$B/results.jsonl"
  pkill -f "iperf3 -s" 2>/dev/null; sleep 0.3
}

# --- baseline: the wire itself, no tunnel ---------------------------------------------------
run_test baseline 10.200.0.2 10.200.0.2 1
run_test baseline 10.200.0.2 10.200.0.2 2

# --- WireGuard (kernel module, ChaCha20-Poly1305) ------------------------------------------
wg genkey | tee h.key | wg pubkey > h.pub; wg genkey | tee p.key | wg pubkey > p.pub
ip link add wg0 type wireguard; ip addr add 10.201.0.1/24 dev wg0
wg set wg0 private-key h.key listen-port 51820 peer "$(cat p.pub)" allowed-ips 10.201.0.2/32 endpoint 10.200.0.2:51821
ip link set wg0 up
ip netns exec peer ip link add wg1 type wireguard; ip netns exec peer ip addr add 10.201.0.2/24 dev wg1
ip netns exec peer wg set wg1 private-key p.key listen-port 51821 peer "$(cat h.pub)" allowed-ips 10.201.0.1/32 endpoint 10.200.0.1:51820
ip netns exec peer ip link set wg1 up
ping -c 2 -W 2 10.201.0.2 >/dev/null && log "wireguard up" || log "WIREGUARD FAILED"
run_test wireguard 10.201.0.2 10.201.0.2 1
run_test wireguard 10.201.0.2 10.201.0.2 2
ip link del wg0; ip netns exec peer ip link del wg1

# --- OpenVPN 2.6 (user space, TLS, no DCO on this kernel) -----------------------------------
openssl ecparam -name prime256v1 -genkey -noout -out ca.key 2>/dev/null
openssl req -x509 -new -key ca.key -days 2 -subj "/CN=bench-ca" -out ca.crt 2>/dev/null
for n in server client; do
  openssl ecparam -name prime256v1 -genkey -noout -out $n.key 2>/dev/null
  openssl req -new -key $n.key -subj "/CN=$n" -out $n.csr 2>/dev/null
  openssl x509 -req -in $n.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 2 -out $n.crt 2>/dev/null
done
ovpn() { # name, cipher, proto(udp|tcp)
  local name=$1 cipher=$2 proto=$3
  local sproto cproto; if [ "$proto" = tcp ]; then sproto=tcp-server; cproto=tcp-client; else sproto=udp; cproto=udp; fi
  cat > "$B/s-$name.conf" <<EOF
dev tun
proto $sproto
lport 1194
local 10.200.0.2
ifconfig 10.202.0.1 10.202.0.2
tls-server
ca $B/ca.crt
cert $B/server.crt
key $B/server.key
dh none
data-ciphers $cipher
cipher $cipher
verb 1
EOF
  cat > "$B/c-$name.conf" <<EOF
dev tun
proto $cproto
remote 10.200.0.2 1194
nobind
ifconfig 10.202.0.2 10.202.0.1
tls-client
ca $B/ca.crt
cert $B/client.crt
key $B/client.key
data-ciphers $cipher
cipher $cipher
verb 1
EOF
  ip netns exec peer openvpn --config "$B/s-$name.conf" --daemon --log "$B/s-$name.log"
  sleep 1
  openvpn --config "$B/c-$name.conf" --daemon --log "$B/c-$name.log"
  for i in $(seq 1 20); do ping -c 1 -W 1 10.202.0.1 >/dev/null 2>&1 && break; sleep 0.5; done
  if ping -c 1 -W 1 10.202.0.1 >/dev/null 2>&1; then
    log "openvpn $name up"
    run_test "openvpn-$name" 10.202.0.1 10.202.0.1 1
    run_test "openvpn-$name" 10.202.0.1 10.202.0.1 2
  else
    log "OPENVPN $name FAILED"; tail -3 "$B/c-$name.log"
  fi
  pkill -f "openvpn --config $B" 2>/dev/null; sleep 1
}
ovpn aes256gcm-udp AES-256-GCM udp
ovpn chacha20-udp CHACHA20-POLY1305 udp
ovpn aes256gcm-tcp AES-256-GCM tcp

log "done"; cat "$B/results.jsonl"
