VPN Tunneling

This lab is built on the SEED Labs for Security Education project by Prof. Wenliang Du, at Syracuse University.

Overview

A Virtual Private Network (VPN) is a private network built on top of a public network, usually the Internet. Computers inside a VPN can communicate securely, just like if they were on a real private network that is physically isolated from outside, even though their traffic may go through a public network. VPN enables employees to securely access a company’s intranet while traveling; it also allows companies to expand their private networks to places across the country and around the world.

The objective of this lab is to understand how a VPN works. We will focus on a specific type of VPN, built on top of the transport layer. We will build a very simple VPN from the scratch, and use the process to illustrate each piece of the VPN system design.

A real VPN program has two parts: tunneling and encryption. This lab only focuses on tunneling packets (since we haven’t covered encryption in this course). This tunnel is therefore not encrypted. This project covers the following topics:

  • Understanding what a Virtual Private Network is

  • The TUN/TAP virtual interface

  • IP tunneling

  • Network Routing, and Route configuration

Lab Setup

  • The lab uses the CS88 VM.

  • Before you follow the instructions to setup the VM you want to first copy the VM over to the /local on the machine you currently are on. Alternatively, you can also copy the VM onto your personal laptop and run it. You will first have to install VirtualBox on your machine.

    # let's assume you are logged into poppy in the CS lab:
    poppy[~]$ cd /local
    
    # copy the VM from zucchini over to poppy
    poppy[local]$ scp zucchini:/local/CS88-Ubuntu20.04.ova .
    
    # set the permissions for the `.ova` file:
    poppy[local]$ chmod 644 CS88-Ubuntu20.04.ova
  • You can follow along with the instructions on the lab page to setup the VM.

Network Setup

We will create a VPN tunnel between a computer (client) and a gateway, allowing the computer to securely access a private network via the gateway. We need at least three machines: VPN client (also serving as Host U), VPN server (the router/gateway), and a host in the private network (Host V). The network setup is depicted in Figure 1 below.

VPN setup

In this lab, we will use two subnets. One subnet is 10.9.0.0/24 and the other is 192.168.60.0/24 each of these subnets are connected via a Local Area Network. A Local Area Network or LAN is used typically on university and corporate campuses to connect end hosts. These machines are connected usually using Ethernet. Wired Ethernet typically refers to twisted-pair copper wire physical links, as well as the link-layer protocol that defines how the link should be shared between different machines.

  • While we see that the two subnets are connected by a "single link" in this lab, in practice the VPN client and VPN server (that are on two distinct subnets shown above) are connected via the Internet (i.e., a number of different Internet Service Providers are in between these two subnets).

  • The machine Host V, is a computer inside the private network, (of say a university campus) 192.168.60.0/24. Users on Host U (outside of the private network) want to communicate with Host V via the VPN tunnel. To simulate this setup, we connect Host V to VPN Server (also serving as a gateway). In this a setup, Host V is not directly accessible from the Internet; nor is it directly accessible from Host U.

Setting up Docker containers

In order to implement the setup above, we will need to ssh into the docker containers setup on the vm.

In the last lab we saw how we can setup a virtual machine and we talked about what a VM does - i.e., creates virtual instances of the entire system hardware up!

You can think of a Docker container as a light-weight VM. Docker virtualizes the application environment in which all of your software applications, code and data live. If you think of the hardware as "bare metal" and the OS as managing computer resources, then the Docker container sits on top of the OS, giving us a sandboxed environment to work in, without the overhead of the entire virtual machine. You can read more about Docker containers here.

In this lab, we are going to run a docker VM inside our virtual machine.

Can you run a docker inside a VM? Yes! How about a docker inside a docker inside a VM? also yes!…​ it’s turtles all the way down…​

  1. First, you will need to git clone your lab in the VM.

  2. Next we will build our docker image just like we built our vm.

    The next two steps will need to be performed every time you login to your VM and want to work on your lab.
    seed@VM:~$ cd vpn
    seed@VM:~/.../vpn$ dcbuild # this step will take a couple of minutes
  3. Next, we want to run dcup which starts the docker container. This is equivalent to starting our VM.

    seed@VM:~/.../vpn$ dcup

    You should now have a docker container up and running! The dcup command will swallow the cursor. To interact with the docker, we will need to open up a new terminal.

  4. All the containers will be running in the background. To run commands on a container, we often need to get a shell on that docker container. We first need to use the dockps command to find out the ID of the container,and then use docksh to start a shell on that container. The following command showsn you can get a shell inside hostC.

    seed@VM:~/.../vpn$ dockps
    894111a9fb6c  host-192.168.60.5
    0be16903419c  client-10.9.0.5
    b1a95c857b54  server-router
    30b6f7b81416  host-192.168.50.5
    b6e1ef869a2f  host-192.168.60.6
    c89a056db3b3  host-192.168.50.6
    
    # here, we are "logging in" to the client docker container
    $ docksh 0b
    root@0be16903419c:/#
    // Note: If a docker command requires a container ID, you do not need t type the entire ID string.Typing the first few characters will be sufficient, as long as they are unique among all the containers.
    You may not see the same ID for the docker containers on your end. They are randomly generated when the docker container starts up.
  5. Once you are done with your work you can run dcdown to shutdown the docker container.

    seed@VM:~/.../vpn$ dcdown

Setting up a shared folder between Docker and the VM

In this lab, we will write code that needs to run on the docker containers.

  • Code editing is more convenient inside the VM using your favorite editor than on the containers. To make our task of editing code and inspecting packets easier, we will use the VM to write code (say in vscode) and then move the code into the ./volumes folder on the the VM.

  • The VM and container are setup to share files via the shared ./volumes foler. Any change you make on the VM, will be reflected in teh docker containers.

Packet Sniffing

Being able to sniffing packets will be a necessary debugging tool for this lab. If your code does not run as expected, being able to look at where the packets are going can help us identify problems. There are several different ways to do packet sniffing:

  1. Running tcpdump on the Docker containers tcpdump is installed on each docker container in Figure 1. To sniff the packets going through a particular interface, we just need to find out the interface name, and then do the following (assume that the interface name is eth0):

    //once you docksh into a docker container you can run the following command:
      # tcpdump -i eth0 -n

    tcpdump is command-line packet analyzer used for network troubleshooting. You can capture and display the packets being transmitted or received on a network interface in realtime similar to Wireshark. tcpdump syntax is the following: tcpdump [options] [expression]. You can type in man tcpdump on the command line to learn more.

    When you run tcpdump inside a docker container, each container is isolated by default, which means we can only sniff the packets going in and out of this container. You won’t be able to sniff the packets between other containers, unless a container uses the host mode in its network setup (more on this later)
  2. Running tcpdump on the VM: If you run tcpdump on the VM, you can sniff all the packets going amongst the containers. The interface name for a network is different on the VM than on the container. On containers, each interface name usually starts with eth; on the VM, the interface name for the network created by Docker starts with br-, followed by the ID of the network. You can use the ip address command to get the interface name on the VM and containers.

  3. Runnning Wireshark on the VM: You can also run Wireshark on the VM to sniff packets. Similar to tcpdump, you will need to select the interface you want Wireshark to sniff on.

Testing your setup

Please conduct the following tests to ensure that the lab environment is set up correctly:

  • Host U can communicate with VPN Server (try a ping from Host U to the VPN server)

  • The VPN Server can communicate with Host V (try a ping from the VPN server to Host V)

  • Host U should not be able to communicate with Host V (a ping from Host U to Host V should fail)

  • Successfully run tcpdump on the router, and sniff the traffic on each of the networks.

Task 1: Create and Configure Tunnel (TUN) Interface

The VPN tunnel that we are going to build is based on the TUN/TAP. TUN and TAP are virtual network kernel drivers; they implement network device that are supported entirely in software. TAP (as in network tap) simulates an Ethernet device and it operates with layer-2 packets such as Ethernet frames; TUN (as in network TUNnel) simulates a network layer device and it operates with layer-3 packets such as IP packets. With TUN/TAP, we can create virtual network interfaces.

A user-space program is usually attached to the TUN/TAP virtual network interface. Packets sent by an operating system via a TUN/TAP network interface are delivered to the user-space program. On the other hand, packets sent by the program via a TUN/TAP network interface are injected into the operating system network stack. To the operating system, it appears that the packets come from an external source through the virtual network interface.

When a program is attached to a TUN/TAP interface, IP packets sent by the kernel to this interface will be piped into the program. On the other hand, IP packets written to the interface by the program will be piped into the kernel, as if they came from the outside through this virtual network interface. The program can use the standard read() and write() system calls to receive packets from or send packets to the virtual interface.

In this task, we will learn how to setup a TUN/TAP interface. You can find the code in vpn/volumes/tun.py folder.

Task 1.A: Name the tunneling interface

Let’s first run tun.py on Host U.

//dockps to list all the containers
//docksh into Host U
# ip address // this should show you all the interfaces on Host U

Next, let’s run the following commands to set the executable permissions on tun.py and can run it as root.

//In host U, cd into volumes
# cd ./volumes
Make the Python program executable
# chmod a+x tun.py
// Run the program using the root privilege
# tun.py

Once the program executes, it will block. Let’s open another terminal in Host U and get a new shell. Next, print out all the interfaces on the machine.

# ip address
Note the names of the the interfaces you see before and after you run tun.py. You should be able to see an interface tun0. Your task, is to modify tun.py such that you can change the interface’s name to tun-cs43.

Task 1.B Set up the TUN Interface

At this point, the TUN interface is not usable, because it has not been configured yet. There are two things that we need to do before the interface can be used. First, we need to assign an IP address to it. Second, we need to bring up the interface, because the interface is still in the down state. We can use the following two commands for the configuration:

// Assign IP address to the interface
# ip addr add 192.168.53.99/24 dev tun-cs43

// Setup the interface to accept incoming/outgoing packets
# ip link set dev tun-cs43 up
Add the following lines of code to your tun.py code, to run the steps above in code and run tun.py

+

os.system("ip addr add 192.168.53.99/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

+ Now run ip address or ip a for short on the Host U. What do you observe?

Task 1.C Read from the TUN Interface

In this task, we will read from the TUN interface. Packets exiting the TUN interface are IP packets.

Before we proceed, let’s learn the basics of Scapy an interactive program to construct packets from the Link Layer all the way up to the Application Layer. Scapy integrates with Python, and we can import Scapy as a module and use the functions that Scapy provides. Here’s a basic overview of Scapy.

  • In your VM you can run the following commands, to create a simple IP packet.

    $sudo python3
    Python 3.8.5 (default, Jul 28 2020, 12:59:40)
    [GCC 9.3.0] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>> from scapy.all import *
    
    ## The next command creates an IP-layer packet a.
    >>> a = IP()  \\create an IP packet with default values.
    >>> a.show()
    ###[ IP ]###
     version   = 4
     ihl       = None
     tos       = 0x0
     len       = None
     id        = 1
     flags     =
     frag      = 0
     ttl       = 64
     proto     = hopopt
     chksum    = None
     src       = 127.0.0.1
     dst       = 127.0.0.1
     \options   \
     >>>> ls(IP)
    version    : BitField  (4 bits)                  = (4)
    ihl        : BitField  (4 bits)                  = (None)
    tos        : XByteField                          = (0)
    len        : ShortField                          = (None)
    id         : ShortField                          = (1)
    flags      : FlagsField  (3 bits)                = (<Flag 0 ()>)
    frag       : BitField  (13 bits)                 = (0)
    ttl        : ByteField                           = (64)
    proto      : ByteEnumField                       = (0)
    chksum     : XShortField                         = (None)
    src        : SourceIPField                       = (None)
    dst        : DestIPField                         = (None)
    options    : PacketListField                     = ([])
You should play around with Scapy to understand packet maniputation before proceeding. Next, let’s cast the data received from the TUN interface into a Scapy IP object, so we can print out each field of the IP packet. Uncomment the while loop for Task 1.C in tun.py. Run the revised tun.py program on Host U, configure the TUN interface, and then conduct the following experiments.
Deliverables
  1. On Host U, ping a host in the 192.168.53.0/24 network. What was the output from tun.py? Explain the output.

  2. On Host U, ping a host in the internal network 192.168.60.0/24. What was the output from tun.py? Explain the output.

Task 1.C Write to the TUN Interface

In this task, we will write to the TUN interface. Since this is a virtual network interface, whatever is written to the interface by the application will appear in the kernel as an IP packet.

Let’s modify tun.py, so that once we get a packet from the TUN interface we do the following:

  1. if this packet is an ICMP echo request packet, construct a corresponding echo reply packet and write it to the TUN interface.

  2. Next, instead of writing an IP packet to the interface, write some arbitrary data to the interface, and report your observations.

Spoofing ICMP packets

As a packet spoofing tool, Scapy allows us to set the fields of IP packets to arbitrary values. Here is an example of spoofing IP packets with an arbitrary source IP address.

  • In this example we spoof ICMP echo request packets and send them to another VM on the same network. You can use Wireshark to observe whether our request will be accepted by the receiver. Read up on ICMP packets in the Handy References section.

  • If it is accepted, an echo reply packet will be sent to the spoofed IP address. Here’s an example of how you might set this up in Scapy:

    $ sudo python3
    >>> from scapy.all import *
    >>> a = IP()
    >>> a.dst = '1.2.3.4'
    >>> b = ICMP()          # create an ICMP echo request
    >>> pkt = a/b
    >>> send(pkt)
    .
    Sent 1 packet
  • If we now run ls(a) we should see the following:

    >>> ls(a)
    version    : BitField (4 bits)   = 4                 (4)
    ihl        : BitField (4 bits)   = None              (None)
    tos        : XByteField          = 0                 (0)
    len        : ShortField          = None              (None)
    id         : ShortField          = 1                 (1)
    flags      : FlagsField (3 bits)  = <Flag 0 ()>      (<Flag 0 ()>)
    frag       : BitField (13 bits)   = 0                (0)
    ttl        : ByteField            = 64               (64)
    proto      : ByteEnumField        = 0                (0)
    chksum     : XShortField          = None             (None)
    src        : SourceIPField        = ’127.0.0.1’      (None)
    dst        : DestIPField             = ’127.0.0.1’     (None)
    options    : PacketListField         = []              ([])
Use the above code snippet as a starting point to modify your tun.py code.

Task 2: Send IP Packets to the VPN Server Through a Tunnel

In this task, our goal is to access the hosts inside the private network 192.168.60.0/24 using the VPN tunnel. We will put the IP packet received from the TUN interface into the UDP payload field of a new IP packet, and send it to another docker machine. We will place the original packet inside a new packet as shown in the figure below. We will run all the steps shown in the figure except encryption and authentication.

IPTunneling withTLS

Task 2.A Setup the Tunnel Server and Client

  1. Run tun_server.py on the VPN docker container. This is a standard UDP server, that listens on port 9090, and prints out packets that are received. The program assuems that the data in teh UDP payload field is an IP packet, and casts the payload to a scapy IP object, and prints out the soruce and destination IP addresses of the enclosed IP packet.

Run the tun_server.py on the VPN Server, and then run 1 on Host U. To test whether the tunnel works or not, ping any IP address belonging to the 192.168.53.0/24 network. What did the VPN server print out? What are the IP source and destination addresses on the packet?

Task 2.B Setup routing for the VPN tunnel

Next, let us ping Host V, and see whether the ICMP packet is sent to VPN Server through the tunnel. Think about why this doesn’t work.

In order to send our ICMP ping through the tunnel, we need to setup IP routes, such that packets going to the 192.168.60.0/24 network are routed to the TUN interface and be given to the tun_client.py program. The following command shows how we might add an entry to the routing table:
# ip route add <network> dev <interface> via <router ip>
Demonstrate how you would successfully setup network interface and router ip such that when you ping an IP address in the 192.168.60.0/24 network, the ICMP packets are received by tun_server.py through the VPN tunnel.

Task 3: Setup a VPN Server

After tun_server.py gets a packet from the tunnel, it needs to feed the packet to the kernel, so the kernel can route the packet towards its final destination. This needs to be done through a TUN interface, just like what we did in Task 1.

Modify tun_server.py, so it can do the following: . Create a TUN interface and configure it. . Get the data from the socket interface; treat the received data as an IP packet. . Write the packet to the TUN interface.

Testing If everything is set up properly, you should now be able to ping Host V from Host U! The ICMP echo request packets should eventually arrive at Host V through the tunnel. Demonstrate that you are able to do so either through Wireshark, or tcpdump.

INFO: Although Host V should be able to respond to the ICMP packets, the reply will not get back to Host U, because we have not set up everything yet. Therefore, for this task, it is sufficient to show (using Wireshark or tcpdump) that the ICMP packets have arrived at Host V.

Task 4: Setting up bi-directional packet flow

At this point we have successfully setup a one directional VPN tunnel, i.e., we can send packets from Host U to Host V via the tunnel. If we look at the Wireshark trace on Host V, we can see that Host V has sent out the response, but the packet gets dropped somewhere. This is because our tunnel is only one directional; we need to set up its other direction, so returning traffic can be tunneled back to Host U.

Task 4.A Use select() to read data from both the TUN and socket interfaces

To setup a bi-directional VPN, our TUN client and server programs need to read data from two interfaces, the TUN interface and the socket interface. We know that by default reading from an interface is a blocking system call. We will use select() to store a list of the sockets/interfaces that we want to read on. select() will block until at least one of the interfaces has data available. Take a look at the select_for_client_server.py code snippet that uses select() to minitor the TUN interface and the socket file descriptor.

Testing Once this is done, we should be able to communicate with Machine V from Machine U, and the VPN tunnel (un-encrypted). Verify that your packets pass through (in both directions) using wireshark using both ping and telnet commands.

Task 4.B Setup realistic routing rules for encrypted traffic

In an real VPN system, the traffic will be encrypted. This means that the return traffic must come back from the same tunnel, that we sent traffic.

In our setup, Host V’s routing table has a default setting: packets going to any destination, except the 192.168.60.0/24 network, will be automatically routed to the VPN server. In the real world, Host V may be a few hops away from the VPN server, and the default routing entry may not guarantee a route for the return packet that goes back to the VPN server.

Routing tables inside a private network have to be set up to ensure that packets going to the tunnel egress are routed to the VPN server. To simulate this scenario, we will remove the default entry from Host V, and add a more specific entry to the routing table, so the return packets can be routed back to the VPN server.

Your task is to use the following commands to remove the default entry and add a new entry:

+

// Delete the default entry in the VPN server
# ip route del default
// Add an entry
# ip route add <network prefix> via <router ip>

Task 5: Setup a VPN between two private networks

In this task, we will setup up a VPN between two private networks — a more realistic real-world scenarop. The setup is illustrated in Figure 2 below.

VPN setup2

This setup simulates a situation where an organization has two sites, each having a private network. The only way to connect these two networks is through the Internet. Your task is to set up a VPN between these two sites, so the communication between these two networks will go through a VPN tunnel. You can use the code developed earlier, but you need to think about how to set up the correct routing, so packets between these two private networks can get routed into the VPN tunnel.

Lab setup

  1. First, let’s shutdown the previous docker container. Exit out of all the docker containers you are curently logged into using the exit command.

  2. Next, run dcdown in your VM as shown below to shutdown the docker container.

    seed@VM:~/.../vpn$ dcdown
  3. Then in your vpn folder, on your VM run the following commands to setup the second VPN environment.

    $ docker-compose -f docker-compose2.yml build
    $ docker-compose -f docker-compose2.yml up
    $ docker-compose -f docker-compose2.yml down
Describe and explain in your presentation, the steps you took to connect the two networks.