VPN over SSH with pppd

Oren Oichman
7 min readNov 12, 2023

Poor man’s VPN

In many cases we would want to bypass all the security restriction in opening ports in our IT world (and they are many of those) whether it is in our public cloud Network or internally in our IT organization.
one way to get pass those restriction is to create a alternative VPN connection with allow us to avoid all the ports request and use that one port in order to esstabilish the VPN session and use it as our source and destination IP communication.

SSH tunnel

SSH as a protocol allow us to create a tunnel and with ppp (pppd to be exact) we can use it to provide IP address to both ends that communication over that tunnel.
This type of VPN is not without a few difficulties. Basically, it doesn’t run unattended very well. If you’re looking for a production-quality VPN that you can set up and forget about, you will probably find PPP-SSH a little disappointing.

What is it good for ?

In general if you are using a tool that requires multiple ports communication between the server and client or between the server and a proxy server then using a VPN (any VPN) will help you a great deal of achieving what you want without many issues.

Where to begin ?

The following tutorial is based on a very good (and old ) tutorial that is located here. The base assumption is that you have SSH access from the client to the server and you have root access on both sides.

packages

To install the needed packages on both server and client just make sure that ppp is installed

# dnf install -y ppp

and that PermitTunnel is set to “yes” on the SSHD server configuration
with your favorite editor open the “/etc/ssh/sshd_config” configuration file and make sure the line is not comment :

PermitTunnel yes

If you had to change the configuration restart the sshd service :

# systemctl restart sshd

Firewalld and NAT

to make sure the VPN server acts as a route the following configuration need to be in place :

Enable the port-forwarding kernel module by running following commands:

# echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
# echo 'net.ipv4.conf.all.accept_redirects = 0' >> /etc/sysctl.conf
# echo 'net.ipv4.conf.all.send_redirects = 0' >> /etc/sysctl.conf
# sysctl -p

Next, configure routing using the Firewalld for PPP over SSH.

To see your zone you can run the following command :

# firewall-cmd --get-default-zone
# export FWZONE=$(firewall-cmd --get-default-zone)

This will give us the zone we need for our services configuration.

Add the ‘ssh’ service to the firewalld list service and add the ‘ppp0’ interface to the firewalld trusted zone.

# firewall-cmd --permanent --add-service=ssh
# firewall-cmd --permanent --zone=${FWZONE} --add-interface=ppp0

Enable ‘MASQUERADE’ and “FORWARD” on the ‘trusted’ zone firewalld.

# firewall-cmd --permanent --zone=${FWZONE} --add-masquerade
# firewall-cmd --permanent --zone=${FWZONE} --add-forward

Enable NAT for the PPP internal IP address to the external IP address e.g. ‘SERVERIP’.

# SERVERIP=$(ip route get 1.1.1.1| awk 'NR==1 {print $(NF-2)}')
# echo $SERVERIP
# firewall-cmd --permanent --direct --passthrough ipv4 -t nat \
-A POSTROUTING -s 172.29.10.0/24 -o $SERVERIP -j MASQUERADE

If you need to make changes the configuration are been kept at the file “direct.xml”

# cat > /etc/firewalld/direct.xml << EOF
<?xml version=”1.0" encoding=”utf-8"?>
<direct>
<passthrough ipv=”ipv4">-t nat -A POSTROUTING -s 172.29.10.0/24 -o $SERVERIP -j MASQUERADE
</passthrough>
</direct>
EOF

NOTE:

The “172.29.10.0/24 are taking from the server configuration and is the VPN network the Server will use for Client / Server communication.

And reload firewall.

# firewall-cmd --reload

VPN user

The incoming SSH VPN requests must be directed to a particular user on the server. For security and accountability, I recommend you use a dedicated user to field VPN requests. The following steps will set up a system user named “vpn” to do just that.

# useradd -g users vpn

We don’t want to set a password for the user because we will authenticate using SSH key exchange.

Now we will create a new ssh keys (public/private) on the client side and add the public key to the server side.

(ON the Client side)

# ssh-keygen -t rsa -N '' -f id_rsa

copy the public key to the server

# scp .ssh/id_rsa.pub root@< server> :/home/vpn/id_rsa.pub

Now we will copy it to the right location on the Server and make sure we create it with the right permissions:

(ON the Server side)

# su - vpn
# mkdir .ssh
# cat /home/vpn/id_rsa.pub > .ssh/authorized_keys

and set the permissions:

# chmod 600 .ssh/authorized_keys
# chmod 700 .ssh/

Set Up sudo

pppd needs to run as root. However, on the server, we’re running everything as the “vpn” user. How can the vpn user run pppd?

There are a number of ways of solving this problem. One is to use the suid bit, and arrange permissions by groups. However, this can get confusing and difficult to administer pretty fast, leading to unintentional security holes. Personally, I find the sudo utility to be a much better solution.

sudo gives ordinary users superuser powers, but only for a very limited set of commands. The system administrator gets to decide what commands are allowed and how much logging to perform. In this case, we want to allow the user “vpn” to run pppd with superuser privilege, but not be allowed to do anything else.

We need to edit sudo’s configuration file, /etc/sudoers and add the following lines :

Cmnd_Alias VPN=/usr/sbin/pppd
vpn ALL=NOPASSWD: VPN

To make life a little bit more simple I will use a bash script that will create the PPP tunnel. the script can run with argument or you can adjust the scriipt to your requirement.

(ON the Client side)

with your favorite editor , create under : /usr/local/sbin/ called “vpn-pppssh” and add the following content to it : (“/usr/local/sbin/vpn-pppssh”)

#!/bin/bash
# /usr/local/bin/vpn-pppssh
#
# This script initiates a ppp-ssh vpn connection.
# see the VPN PPP-SSH HOWTO on http://www.linuxdoc.org for more information.
#
# revision history:
# 1.6 11-Nov-1996 miquels@cistron.nl
# 1.7 20-Dec-1999 bart@jukie.net
# 2.0 16-May-2001 bronson@trestle.com
# 2.1 11-Nov-2023 two.oes@gmail.com

#
# You will need to change these variables...
#
usage() {
echo "Usage: $0 [ stop | start | config ]
start
-s - Server hostname
-p - Destination SSH port
-u - remote username
-m - VPN Client Address
-r - VPN remote Address
stop
config" 1>&2; exit 1;
}


# This tells ssh to use unprivileged high ports, even though it's
# running as root. This way, you don't have to punch custom holes
# through your firewall.

LOCAL_SSH_OPTS="-P"

#
# The rest of this file should not need to be changed.
#
PATH=/usr/local/sbin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/bin/X11/:
#
# required commands...
#
PPPD=/usr/sbin/pppd
TMP_CONF=/tmp/ppp-ssh.inf
SSH=/usr/bin/ssh

if ! test -f $PPPD ; then echo "can't find $PPPD"; exit 3; fi
if ! test -f $SSH ; then echo "can't find $SSH"; exit 4; fi

start_flag=0
stop_flag=0
config_flag=0
DEST_PORT=22

for (( i=1; i<=$#; i++)); do
case "${!i}" in
-s)
j=$((i+1))
SERVER_HOSTNAME="${!j}"
;;
-u)
j=$((i+1))
SERVER_USERNAME="${!j}"
;;
-r)
j=$((i+1))
SERVER_IFIPADDR="${!j}"
;;
-m)
j=$((i+1))
CLIENT_IFIPADDR="${!j}"
;;
-p)
j=$((i+1))
DEST_PORT="${!j}"
;;
start)
start_flag=1
;;
stop)
stop_flag=1
;;
config)
config_flag=1
;;
esac
done


if [[ start_flag -eq 1 ]] && [[ stop_flag -eq 0 ]] && [[ config_flag -eq 0 ]] ; then

# The host name or IP address of the SSH server that we are
if [[ -z ${SERVER_HOSTNAME} ]]; then
echo "No Server Hostname Specified"
usage
fi

# The username on the VPN server that will run the tunnel.
# For security reasons, this should NOT be root. (Any user
# that can use PPP can intitiate the connection on the client)
if [[ -z ${SERVER_USERNAME} ]]; then
echo "No remote user provided"
usage
fi

# The VPN network interface on the server should use this address:
if [[ -z ${SERVER_IFIPADDR} ]]; then
echo "No remote VPN IP address specified"
usage
fi

# ...and on the client, this address:
if [[ -z ${CLIENT_IFIPADDR} ]]; then
echo "No local VPN IP address specified"
usage
fi

echo "SERVER_HOSTNAME=${SERVER_HOSTNAME}" > ${TMP_CONF}
echo "SERVER_USERNAME=${SERVER_USERNAME}" >> ${TMP_CONF}
echo "SERVER_IFIPADDR=${SERVER_IFIPADDR}" >> ${TMP_CONF}
echo "CLIENT_IFIPADDR=${CLIENT_IFIPADDR}" >> ${TMP_CONF}

echo -n "Starting vpn to $SERVER_HOSTNAME: "
${PPPD} updetach noauth passive pty "${SSH} ${LOCAL_SSH_OPTS} ${SERVER_HOSTNAME} -p ${DEST_PORT} \
-l${SERVER_USERNAME} -o Batchmode=yes sudo ${PPPD} nodetach notty noauth" \
ipparam vpn ${CLIENT_IFIPADDR}:${SERVER_IFIPADDR}
echo "connected."

elif [[ stop_flag -eq 1 ]] && [[ start_flag -eq 0 ]] && [[ config_flag -eq 0 ]]; then
if [[ -f ${TMP_CONF} ]]; then
source ${TMP_CONF}
else
echo "No config file found. Not Running !"
exit 1
fi
echo -n "Stopping vpn to $SERVER_HOSTNAME: "
PID=`ps ax | grep "${SSH} ${LOCAL_SSH_OPTS} ${SERVER_HOSTNAME} -p ${DEST_PORT} -l${SERVER_USERNAME} -o" | grep -v ' passive ' | grep -v 'grep ' | awk '{print $1}'`
if [ "${PID}" != "" ]; then
kill $PID
echo "disconnected."
rm -f /tmp/ppp-ssh.inf
else
echo "Failed to find PID for the connection"
fi

elif [[ stop_flag -eq 0 ]] && [[ start_flag -eq 0 ]] && [[ config_flag -eq 1 ]]; then
if [[ -f ${TMP_CONF} ]]; then
source ${TMP_CONF}
else
echo "No config file found. Not Running !"
exit 1
fi

source ${TMP_CONF}

echo "SERVER_HOSTNAME=$SERVER_HOSTNAME"
echo "SERVER_USERNAME=$SERVER_USERNAME"
echo "SERVER_IFIPADDR=$SERVER_IFIPADDR"
echo "CLIENT_IFIPADDR=$CLIENT_IFIPADDR"

else
echo "One of the option must be specified [start|stop|config]"
usage
fi

exit 0

once the script is in place we need to make sure it is executable :

# chmod a+x /usr/local/sbin/vpn-pppssh

Now we can run the script with the following argument in order to start the VPN :

# export SERVER="server.example.com"
# vpn-pppssh start -s ${SERVER} -u vpn -r 172.29.10.2 -m 172.29.10.1

the script does not add a NAT from the client to the Server network. if you want to add the remote Network in order to access the network through the VPN then you can add it with the following command.

In this example the destination network is “192.168.0.0/24” and the GW is the VPN network IP address of the Server end.

# route add -net 192.168.0.0 netmask 255.255.255.0 gw 172.29.10.2

Now your VPN is set and you can access the remote network through it.

That is it. Have fun!

If you have any question feel free to respond/ leave a comment.
You can find me on linkedin at : https://www.linkedin.com/in/orenoichman
Or twitter at : https://twitter.com/ooichman

--

--