====== VPN over SSH ======
* **Server**: GNU/Linux (Ubuntu 12.04) -- ''172.16.0.1''
* **Client**: MacOS/BSD (El Capitan, v10.11.5) -- ''172.16.0.2''
Create and configure a tunnelled connection between client and server, via ''tun0'' interfaces:
- Go install http://tuntaposx.sourceforge.net/download.xhtml on the Mac, it is needed by ''ssh''
- SSH into the server and edit ''/etc/ssh/sshd_config'' to include
PermitRootLogin yes
PermitTunnel yes
- Restart SSHd on the server (''service ssh reload'' does not appear to be sufficient) sudo service ssh stop; sudo service ssh start
- Log out of server (need to reconnect to make use of config changes)
- **As root** on the client machine, SSH into the root account on the server with tun devices enabled via ''-w'' optionssh root@server.hostname.com -w 0:0
(''0:0'' specifies that both local and remote ends will create ''tun0'' interfaces)
- Within the resulting root shell on the **server**, configure the new ''tun0'' network interface:
ifconfig tun0 inet 172.16.0.1 dstaddr 172.16.0.2
ifconfig tun0
ping 172.16.0.1 # Check tun0 has an IP address
ping 172.16.0.2 # Should fail, as we've not yet configured the client's tun0
- In a root shell on the **client**, configure the new ''tun0'' network interface:
ifconfig tun0 inet 172.16.0.2 172.16.0.1
ifconfig tun0
ping 172.16.0.2 # Check tun0 has an IP address
ping 172.16.0.1 # Check we can communicate with remote end (server) via tun0
- Back in the root shell on the **server**, repeat ''ping 172.16.0.2'' and this time it should respond
- The tunnel is now configured. It will remain so until the SSH session is closed.
Configure IPv4 (ICMP+TCP+UDP) forwarding and Network Address Translation (NAT):
- In the root shell on the **server** (only needs to be done one per boot):
# Prepare networking stack for use by forced commands in
# /root/.ssh/authorized_keys that creates a point-to-point network (via tun0)
# between 172.16.0.1 (this host) and 172.16.0.2 (remote end).
# We then want to enabling forwarding of IPv4 traffic, i.e. we want to act as a
# router. We enable this in the kernel, and then ensure traffic originating
# from the remote side of the point-to-point link is accepted, and any
# responses are likewise accepted
echo 1 > /proc/sys/net/ipv4/ip_forward
/sbin/iptables -F
/sbin/iptables -A FORWARD ! --source 172.16.0.2 --destination 172.16.0.2 \
-m state --state RELATED,ESTABLISHED -j ACCEPT
/sbin/iptables -A FORWARD --source 172.16.0.2 ! --destination 172.16.0.2 -j ACCEPT
# Any traffic originating from the remote side should go through Network
# Address Translation (NAT), so responses from (e.g.) DNS servers are sent to
# this host, so *we* can forward it to the remote end. This is the MASQUERADE
# rule.
/sbin/iptables -t nat -F
/sbin/iptables -t nat -A POSTROUTING ! --destination 172.16.0.2 -j MASQUERADE
# Monitor packets
watch -n0.5 -d ifconfig tun0
- As root on the **client**:
route add 10.0.0.0/8 -interface tun0
(you can undo this by repeating the command with ''delete'' in place of ''add'')
- IPv4 forwarding via ''tun0'' is in effect.
Note: The changes made to the server persist after the SSH session has ended.
To get name resolution working, you need to configure the client to use a DNS server at the remote end, e.g.
- Discover the DNS nameservers used by the **server**: cat /etc/resolv.conf
- Add these to the **client** system: networksetup -setdnsservers Wi-Fi 10.1.2.24 10.1.2.23 192.168.1.253 8.8.8.8 8.8.4.4
This step must be manually undone (e.g. after closing the SSH session) by running networksetup -setdnsservers Wi-Fi Empty
You may also want to add your remote system's DNS search domains, e.g.: networksetup -setsearchdomains Wi-Fi site.example.com example.com local
Again, this must be manually undone after you close the VPN connection: sudo networksetup -setsearchdomains Wi-Fi Empty
Most useful guides:
* http://ubuntuforums.org/showthread.php?t=926435&page=3&p=7886699#post7886699
* http://kovyrin.net/2006/03/17/how-to-create-ip-ip-tunnel-between-freebsd-and-linux/
* https://help.ubuntu.com/community/SSH_VPN
* https://blog.brixandersen.dk/2006/10/23/vpn-using-openssh-and-tun4-under-freebsd/
* http://osxdaily.com/2015/06/02/change-dns-command-line-mac-os-x/
* NAT: http://www.revsys.com/writings/quicktips/nat.html
* SSH, including forced-commands via ''.ssh/authorized_key'': https://www.digitalocean.com/community/tutorials/ssh-essentials-working-with-ssh-servers-clients-and-keys
More thorough networking (Ethernet layer, instead of link layer): http://sgros.blogspot.co.uk/2011/11/ssh-vpns-bridged-connection-to-lan.html
===== Automating via SSH configuration files =====
All commands here are run as **root** on the client system
- **As root** on your client system, generate a new SSH keypair to use for VPN. ssh-keygen -f ~/.ssh/id_rsa_vpn -N ''
- Install new public key into remote system, and prefix with a ForeCommand which is run whenever this key is used to authenticate:( \
printf 'tunnel="0",command="fuser -k /dev/net/tun; ifconfig tun0 inet 172.16.0.1 dstaddr 172.16.0.2" ' ; \
cat ~/.ssh/id_rsa_test.pub \
) | ssh root@www.robmeerman.co.uk tee -a .ssh/authorized_keys
- Configure client via ''~/.ssh/config''. Add the following to the end of ''.ssh/config'' (create it if it does not exist) and replace ''$SERVER'' with your server's hostname: Host vpn
Hostname $SERVER
User root
# Remote's .ssh/authorised_keys entry for this identity is prefixed with:
# tunnel="0",command="fuser -k /dev/net/tun; ifconfig tun0 inet 172.16.0.1 dstaddr 172.16.0.2" ssh-rsa
IdentityFile ~root/.ssh/id_rsa_vpn
Tunnel yes
TunnelDevice 0:0
PermitLocalCommand yes
LocalCommand ~root/.ssh/vpn.sh %h %T
# Disable connection sharing, otherwise closing VPN may not actually reset
# network settings because vpn.sh (cf. LocalCommand) continues to wait
# for the `ssh` process to exit (which it may not if another session is
# active)
ControlPath none
# Disable use of ssh-agent, as it seems to prevent our preferred identity
# (cf. IdentityFile) being applied, which in turn means we don't trigger the
# ForceCommand of the remote's authorized_keys file
IdentityAgent none
- Create a new script on your client machine at ''~root/.ssh/vpn.sh'' which configures your Mac to route traffic headed to your server via the current gateway, and then change the default gateway (that applies to all __other__ traffic) to go via the new SSH ''tun'' device at 172.16.0.1, then wait for the ''ssh'' process to exit before returning settings to normal: #!/bin/bash
# .ssh/config: LocalCommand vpn.sh %h %T
REMOTE_HOST=$1
TUNNEL_DEVICE=$2
ifconfig $TUNNEL_DEVICE inet 172.16.0.2 172.16.0.1
ROUTE=$(route get $REMOTE_HOST)
GATEWAY=$(sed -ne 's/^ *gateway: //p' <<<"$ROUTE")
INTERFACE=$(sed -ne 's/^ *interface: //p' <<<"$ROUTE")
route add $REMOTE_HOST $GATEWAY
route add 10/8 $GATEWAY
route change default 172.16.0.1
WAIT_PID=$PPID
(
while kill -0 $WAIT_PID >/dev/null 2>&1; do sleep 0.5; done
# The route gets deleted when the SSH tunnel closes gracefully and tun0 disappears
route change default $GATEWAY
route add default $GATEWAY
route delete 10/8 $GATEWAY
route delete $REMOTE_HOST $GATEWAY
) &
This script is unlikely to work on other OS
- Make the new script executable: chmod a+x ~root/.ssh/vpn.sh
- Test it by running ssh vpn
Sample session showing the output from the commands above:
# ssh-keygen -f ~/.ssh/id_rsa_vpn -N ''
Generating public/private rsa key pair.
Your identification has been saved in /var/root/.ssh/id_rsa_vpn.
Your public key has been saved in /var/root/.ssh/id_rsa_vpn.pub.
The key fingerprint is:
SHA256:4c8jh23lnMr7ZEmiDCCenKEEo6ROBDIku3XCmKLqqcw root@roberts-mbp
The key's randomart image is:
+---[RSA 2048]----+
|X+ |
|OB |
|Bo* o . |
|** B . . . |
|+.= . S . o |
|. o * * o |
|. = B B |
|+ . = = |
|oE +o. |
+----[SHA256]-----+
# ( \
# printf 'tunnel="0",command="fuser -k /dev/net/tun; ifconfig tun0 inet 172.16.0.1 dstaddr 172.16.0.2" ' ; \
# cat ~/.ssh/id_rsa_test.pub \
# ) | ssh root@www.robmeerman.co.uk tee -a .ssh/authorized_keys
tunnel="0",command="ifconfig tun0 inet 172.16.0.1 dstaddr 172.16.0.2" ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+pPee+HqiExk28lwKGcjoAMnkWRVKoQsn8b+90ST3HteZq1oCKtig49YOtlXDZGma0vR/y9Xbelk26xJfZO32BR3GCPou6XYSU67qwC8wK256H0LfTUlquUufklmKd3BaKamAtXU0JwhVxQCFH0hToG6dgc0FLelqs1r8u6cPni1wTxaId6epHrYCBrKvP+fwYz0S0K3e2opcqZUTwMyPYwu280UxQr2HYvzykdoJeiJtsKgneFRxhX7gnlKCYoia0fToKHel24GfUFfqipFrJbsm8LDYuVh5KVgx1J1Hx19Fu0LM3IIqoXQESob91TjTx1bq41iIMZ0n0td5gDVj root@roberts-mbp
# ssh vpn
add host www.robmeerman.co.uk: gateway 10.1.36.1
add net 10: gateway 10.1.36.1
change net default: gateway 172.16.0.1
# Nothing further appears to happen. VPN is up and running! Try `traceroute
# google.com` in another terminal to verify that the traffic is going via your
# server and not its default route.
# When all done, press ^C to kill the VPN and restore default settings. Your
# prompt will return first, and *then* the clean-up code will execute and
# print:
^C
route: writing to routing socket: not in table
change net default: gateway 10.1.36.1: not in table
add net default: gateway 10.1.36.1
delete net 10: gateway 10.1.36.1
delete host www.robmeerman.co.uk: gateway 10.1.36.1
===== systemd service =====
Configure a remote host, which lives behind a firewall, to maintain an SSH connection to my home network that provides a reverse tunnel back into `sshd` on the remote host.
''/etc/systemd/system/my-vpn.service''
[Unit]
Description=SSH-based VPN
After=network.target
[Service]
Restart=always
# Disable rate-limiting, which may result in "giving up"
StartLimitInterval=0
ExecStartPre=/bin/sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward"
ExecStartPre=/sbin/iptables -A FORWARD ! --source 172.16.0.2 --destination 172.16.0.2 \
-m state --state RELATED,ESTABLISHED -j ACCEPT
ExecStartPre=/sbin/iptables -A FORWARD --source 172.16.0.2 ! --destination 172.16.0.2 -j ACCEPT
ExecStartPre=/sbin/iptables -t nat -A POSTROUTING ! --destination 172.16.0.2 -j MASQUERADE
ExecStart=/usr/bin/ssh user@example.com -R10022:localhost:22 -N -oServerAliveInterval=15 -oExitOnForwardFailure=yes
[Install]
WantedBy=multi-user.target
===== KCP Tunnelling via Fast Reverse Proxy (frp) =====
[[https://github.com/fatedier/frp|Fast Reverse Proxy (frp)]] is a pair of standalone executables which can be used to expose services behind NAT. I like it because:
* It supports [[https://github.com/skywind3000/kcp/blob/master/README.en.md|KCP protocol]].
* KCP reduces latency on lossy links by implementing error **correction** instead of just the error **detection** of TCP. This is achieved by using more bandwidth to send [[https://en.wikipedia.org/wiki/Erasure_code|erasure codes]] which can be used to recover lost packets (instead of requesting they be retransmitted which add round-trip delays). Similar to how CD-ROMs cope with scratches.
* This is especially useful on cellular / mobile networks (3G, 4G, etc)
* It can NAT-bust, even when **both** parties are behind NAT.
* This does require a public, non-NAT, server both parties can initiate contact with
Download and unpack [[https://github.com/fatedier/frp|Fast Reverse Proxy (frp)]] which contains two executables: ''frpc'' and ''frps''.
I drive them with this bash script, which I call ''frp'':
#!/bin/bash
#
# FRP: Fast Reverse Proxy (cf. https://github.com/fatedier/frp)
#
# XTCP: Creates a direct connection between hosts which are behind NAT gateways
# by getting both to contact a public server they can both access, and then
# (ab)using a UDP connection directly to one another. Because UDP is stateless,
# both sides can send a packet to the other and thereby get their NAT gateway
# to set up a session (temporary port forward) for any return traffic. Voila,
# you now have holes in both NAT gateways and the clients (frpc) can talk to
# each other directly.
SERVER_PORT=29900
SERVER_ADDR=203.0.113.0:${SERVER_PORT}
TOKEN=yourtokenhere
SECRET_KEY=yoursecrethere
case $1 in
public-server)
CMD=(
./frps
--bind_udp_port=7001
--kcp_bind_port=${SERVER_PORT}
--token=${TOKEN}
);;
ssh-server)
CMD=(
./frpc xtcp
# Public server details and auth token
--server_addr=${SERVER_ADDR}
--protocol=kcp
--token=${TOKEN}
# Proxy entry to publish for other hosts (also behind NAT) to
# access, thus making us act as a server.
--role=server
--proxy_name=ssh_p2p
--sk=${SECRET_KEY}
# Service to connect incoming tunnelled connections to
--local_ip=127.0.0.1
--local_port=22
);;
ssh-client)
CMD=(
./frpc xtcp
# Public server details and auth token
--server_addr=${SERVER_ADDR}
--protocol=kcp
--token=${TOKEN}
# Proxy entry (published by another client) within the server we want to
# use, and server secret key
--server_name=ssh_p2p
--sk=${SECRET_KEY}
# As a visitor, we are trying to access something published by another
# client (also behind NAT) with --role=server. "bind" parameters dictate
# where to put the listening end of our P2P tunnel: apps on our network
# will connect to this to be tunnelled through FRP and it's NAT hole.
#
# Also note that KCP protocol runs over UDP, but very few applications
# support that directly, so the both ends of the KCP tunnel convert
# to/from TCP.
--role=visitor
--bind_addr=127.0.0.1
--bind_port=29922
);;
*)
echo "Please provide exactly one argument: public-server, ssh-server, or ssh-client"
exit 1
;;
esac
cd $(dirname $0)
exec "${CMD[@]}"
and if you like, here's a systemd file for it, ''frp.service'':
[Unit]
Description=Fast Reliable Proxy Server
After=network.target
[Service]
Type=simple
User=nobody
Restart=on-failure
RestartSec=5s
ExecStart=/opt/frp/frp public-server
[Install]
WantedBy=multi-user.target
To install it:
sudo ln -s $(readlink -f frp.service) /etc/systemd/system/
sudo systemctl daemon-reload # To notice the new symbolic link
sudo systemctl enable frp.service # So it starts automatically on boot
sudo systemctl start frp.service # .. one off start it now