---
# System prepended metadata

title: VPN Tunneling Lab
tags: [Network Security, SUTD, Lab, SEED Labs]

---

# VPN Tunneling Lab

###### tags: `SUTD` `SEED Labs` `Network Security` `Lab`

*Done by: Lin Huiqing (1003810)*

Web view: https://hackmd.io/@ephemeral-instance/B1VpjbuEu

Below are the machine-to-IP mappings used for this lab:

| Machine              | "NAT Network" Adapter | "Internal Network" Adapter | TUN Interface   |
| -------------------- | --------------------- | -------------------------- | --------------- |
| Host U / VPN Client  | `10.0.2.4`            | -                          | `192.168.53.99` |
| Gateway / VPN Server | `10.0.2.5`            | `192.168.60.1`             | `192.168.53.98` |
| Host V               | -                     | `192.168.60.101`           | -               |

## Task 1: Network Setup

We will first set up the network for the gateway, followed by Host V then Host U. After which, we will test that the network behaviour is in line with what is suggested for the lab.

For the Gateway, set up a additional network adapter. Before booting up the VM, navigate to the Network panel by selecting the right machine, followed by `Settings` and then `Network`. Navigate to `Adapter 2`, check the box for "Enable Network Adapter", and fill in the following fields as according to the image below:

1) Attached to
2) Name
3) Promiscuous Mode

![](https://i.imgur.com/y5BKJL2.png)

Boot up the Gateway VM. Click on the network icon on the top right of the screen. After clicking "Edit Connections", the pop-up for the Network Connections should have at least 2 networks as seen below.

![](https://i.imgur.com/GLEGINq.png)

Although my Gateway machine has 3 Wired Connections, only Wired connection 2 and Wired connection 3 are used in this lab. Wired connection 2 will be the "NAT Network" Adapter while Wired connection 3 will be the "Internal Network" Adapter. As the "NAT Network" Adapter has already been set up for previous labs, only the setup for the "Internal Network" Adapter will be explained below.

Double click "Wired connection 3" and select the "IPv4 Settings" tab. Change the "Method" field to the "Manual" option and add a new address as shown below.

![](https://i.imgur.com/DuEUu7V.png)

Next, we will configure Host V.

Before booting up the machine, adapter should first be configured. Navigate to the Network panel by selecting the right machine, followed by `Settings` and then `Network`. Change Adapter 1 to the Internel Network Adapter by filling in the following fields as according to the image below:

1) Attached to
2) Name
3) Promiscuous Mode

![](https://i.imgur.com/oEyIL36.png)

Ensure that the MAC address is different from that of the Gateway VM.

After booting up the machine and clicking the network icon followed by "Edit Connections", the following window should appear. We will only be using Wired connection 3 for this lab.

![](https://i.imgur.com/PiCJJ24.png)

Similar to when configuring the Gateway machine, double click the Wired connection 3, switch to the IPv4 tab and fill in the Method and Addresses as shown below.

![](https://i.imgur.com/c4QN4WW.png)

> **Testing.** Please conduct the following testings to ensure that the network setup is performed correctly:

The network setup will be tested with `ping`, and results are determined by whether machines are able to receive ICMP packets from one another.

> * Host U can communicate with VPN Server.

Host U is able to `ping` VPN Server as as shown below.

![](https://i.imgur.com/kPJAw8F.png)

VPN Server is able to `ping` Host U as shown below.

![](https://i.imgur.com/2r0HYD3.png)

Hence, Host U and VPN Server are able to communicate with each other.

> * VPN Server can communicate with Host V.

VPN Server is able to `ping` Host V as shown below.

![](https://i.imgur.com/o5nPNlA.png)

Host V is able to `ping` VPN Server as shown below.

![](https://i.imgur.com/OcznHSh.png)

Hence, VPN Server and Host V are able to communicate with one another.

> * Host U should not be able to communicate with Host V.

Host U is unable to `ping` Host V as shown below.

![](https://i.imgur.com/wI26S1j.png)

Host V is unable to `ping` Host U as shown below.

![](https://i.imgur.com/vI8vYni.png)

Hence, Host U and V are unable to communicate with one another.

## Task 2: Create and Configure TUN Interface

### Task 2.a: Name of the Interface

*Note: The `tun.py` code for this task is saved as `host_u/task2a.py`.*

> Your job in this task is to change the tun.py program, so instead of using tun as the prefix of the interface name, use your last name as the prefix. For example, if your last name is smith, you should use smith as the prefix. If your last name is long, you can use the first five characters. Please show how your results.

To change the prefix of the interface name to my last name "lin", the `ifr` variable was defined in `tun.py` as follows.

``` python
ifr = struct.pack('16sH', b'lin%d', IFF_TUN | IFF_NO_PI)
```

After saving the file, make the python an executable with `chmod +x tun.py` and run it with root privilege with `sudo ./tun.py`. After which, open a new terminal and run the following command to check that the tun interface is successfully named accordingly:

``` shell
ip address | grep lin0
```

The output seen should be as follows:

![](https://i.imgur.com/FPQXGsN.png)

### Task 2.b: Set up the TUN Interface

*Note: The `tun.py` code for this task is saved as `host_u/task2b.py`.*

> 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.

The following code is added to `tun.py`:

``` python
# imports, define variables, create tun interface

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

# while true loop
```

> After running the two commands above, run the "ip address" command again, and report your observation. How is it different from that before running the configuration commands?

Run the code again with `sudo ./tun.py`. Then, open a new terminal and run `ip address | grep lin0` again. The output is seen below.

![](https://i.imgur.com/TXDx5Lx.png)

In the first line, `UP, LOWER_UP` indicates that the `lin0` tunnel interface is enabled, and the device is connected to the network.

The last line indicates that the IP address subnet `192.168.53.99/24` was assigned to the tun interface.

### Task 2.c: Read from the TUN Interface

*Note: The `tun.py` code for this task is saved as `host_u/task2c.py`.*

> Please run the revised `tun.py` program on Host U, configure the TUN interface accordingly, and then conduct the following experiments. Please describe your observations:
> * On Host U, `ping` a host in the `192.168.53.0/24` network. What are printed out by the `tun.py` program? What has happened? Why?

The host `192.168.53.20` is pinged with the following command:

``` shell
ping 192.168.53.20
```

ICMP packets are sent as a result, as seen below.

![](https://i.imgur.com/YLpX1hJ.png)

At the same time, `tun.py` prints `IP / ICMP 192.168.53.99 > 192.168.53.20 echo-request 0 / Raw` as seen below.

![](https://i.imgur.com/a8faVyT.png)

This means that the tun interface (`192.168.53.99`) is sending ICMP packets to the host queried (`192.168.53.20`). This happens because when the host `192.168.53.20` is pinged, the ICMP packet would be sent through the tun interface as it is in the same subnet.

> * On Host U, `ping` a host in the internal network `192.168.60.0/24`, Does `tun.py` print out anything? Why?

The host `192.168.60.20` is pinged with the following command:

``` shell
ping 192.168.60.20
```

Although such a host does not exist, it can be seen that the ICMP packets are sent out as follows:

![](https://i.imgur.com/V99AuXy.png)

However, `tun.py` does not print anything. This is because the host would be within the internal network and thus the packets sent would not be passed to the tun interface. Therefore, as `tun.py` only prints when packets are received, `tun.py` would not print out anything.

![](https://i.imgur.com/t9Cj0cV.png)

### Task 2.d: Write to the TUN Interface

> Please modify the `tun.py` code according to the following requirements:
> * After getting a packet from the TUN interface, if this packet is an ICMP echo request packet, construct a corresponding echo reply packet and write it to the TUN interface. Please provide evidence to show that the code works as expected.

*Note: The `tun.py` code for this part of the task is saved as `host_u/task2d_1.py`.*

To check for the echo request packet, the type of packet is checked with the following function:

``` python
def check_icmp_req(bytes_in):
    pkt_in = IP(bytes_in)
    if ICMP in pkt_in: # checks for ICMP packet
        if pkt_in[ICMP].type == 8: # checks for echo-request type
            return True
    return False
```

The function `create_icmp_reply` is then defined to create an ICMP echo reply packet based on any ICMP echo request packet received. This is done as seen below.

``` python
def create_icmp_reply(bytes_in):
    pkt_in = IP(bytes_in)
    ip_out = IP(src=pkt_in.dst, dst=pkt_in.src)
    pkt_out = ip_out / pkt_in.payload
    pkt_out[ICMP].type = 0 # set ICMP packet type as echo-reply
    return bytes(pkt_out)
```

In the `while True` loop, when a packet is read from the tun interface, it is first checked by the `check_icmp_req` function to check if the packet is an ICMP echo request packet. If it is, the `create_icmp_reply` function is called and the created reply is written to the tun interface. This is done as seen below.

``` python
while True:
    # Get a packet from the tun interface
    packet = os.read(tun, 2048)
    if check_icmp_req(packet):
        reply_bytes = create_icmp_reply(packet)
        os.write(tun, reply_bytes)
```

After saving the code, run the code with `sudo ./tun.py`. In a new terminal, run `ping 192.168.53.20`. The pings are replied as seen below.

![](https://i.imgur.com/2ihZp54.png)

As the ICMP request packets are replied, the code works as expected.

> * Instead of writing an IP packet to the interface, write some arbitrary data to the interface, and report your observation.

*Note: The `tun.py` code for this part of the task is saved as `host_u/task2d_2.py`.*

The code can be modified as seen below to sent arbitrary data instead.

``` python
while True:
    # Get a packet from the tun interface
    packet = os.read(tun, 2048)
    if check_icmp_req(packet):
        os.write(tun, bytes("arbitrary data", encoding="utf-8"))
```

When `sudo ./tun.py` is run, WireShark can be used to capture the packets in `lin0` interface. After `ping 192.168.53.20` is executed in a new terminal, WireShark logs the following packets:

![](https://i.imgur.com/dQneRcc.png)

The `ping` does not receive any responses as well as seen below.

![](https://i.imgur.com/ZKGWVCR.png)

Based on the above, it can be observed that because arbitrary data instead of IP packets were written to the interface, the bytes written to the tunnel were not valid and the request pings do not receive responses.

## Task 3: Send the IP Packet to VPN Server Through a Tunnel

*Note: The `tun_server.py` code is saved as `gateway/task3.py`, and the `tun_client.py` code is saved as `host_u/task3.py`.*

> **Testing.** Run the `tun_server.py` program on VPN Server, and then run `tun_client.py` 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 is printed out on VPN Server? Why?

Run `tun_server.py` on Gateway with `sudo ./tun_server.py` and run `tun_client.py` on Host U with `sudo ./tun_client.py`.

On Host U, a host in `192.168.53.0/24`, `192.168.53.20` was pinged as seen below.

![](https://i.imgur.com/fWNMyUy.png)

As seen, the packets are transmitted but there were no ICMP echo reply packets.

At the same time, `Inside: 192.168.53.99 --> 192.168.53.20` statements are printed on the VPN Server as seen below.

![](https://i.imgur.com/7doFiMM.png)

These printed statements indicate that the VPN Server received the ICMP packets from the tun interface (`192.168.53.99`) which are intended for the chosen host in `192.168.53.0/24` (`192.168.53.20`).

> Our ultimate goal is to access the hosts inside the private network `192.168.60.0/24` using the tunnel. Let us ping Host V, and see whether the ICMP packet is sent to VPN Server through the tunnel. If not, what are the problems?

On Host U, a host in `192.168.60.0/24`, `192.168.60.20`, is pinged as shown below. Note that `tun_client.py` should still be running in a separate terminal.

![](https://i.imgur.com/SODFtOC.png)

However, the VPN Server does not receive the packets and thus the `tun_server.py` script does not output anything as seen below.

![](https://i.imgur.com/96omTOf.png)

This is expected as packets meant for the `192.168.60.0/24` subnet are not told to be routed through the tunnel yet.

> You need to solve this problem, so the ping packet can be sent through the tunnel. This is done through routing, i.e., packets going to the `192.168.60.0/24` network should be routed to the TUN interface and be given to the `tun_client.py` program.

In `tun_client.py`, the following line can be added to route packets meant for `192.168.60.0/24` through the `lin0` tunnel.

``` python
os.system("ip route add 192.168.60.0/24 dev {}".format(ifname))
```

> Please provide proofs to demonstrate 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 tunnel.

On Host U, after the routing is done, `192.168.60.20` is pinged again as shown below.

![](https://i.imgur.com/pYNS4kF.png)

On the VPN Server, we can now see that the ICMP packets are received as shown below.

![](https://i.imgur.com/e90eyzF.png)

## Task 4: Set Up the VPN Server

*Note: The `tun_server.py` code is saved as `gateway/task4.py`.*

> 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 2. Please modify `tun_server.py`, so it can do the following:
> * Create a TUN interface and configure it.

The TUN interface is created with the code below. The IP address is set to be `192.168.53.98` so that it is on the `192.168.53.0/24` subnet and not the IP address of the TUN interface on the VPN Client (`192.168.53.99`). The TUN interface is named as `lin0` so as to match the name of the TUN interface on the VPN Client.

``` python
import fcntl
import struct
import os
from scapy.all import *

TUNSETIFF = 0x400454ca
IFF_TUN = 0x0001
IFF_TAP = 0x0002
IFF_NO_PI = 0x1000
TUN_IP = "192.168.53.98"

# Create the tun interface
tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'lin%d', IFF_TUN | IFF_NO_PI)
ifname_bytes = fcntl.ioctl(tun, TUNSETIFF, ifr)
```

After the TUN interface is created, it can be configured with the following.

``` python
# Get the interface name
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")
print("Interface Name: {}".format(ifname))

# Set up the tun interface
os.system("ip addr add {}/24 dev {}".format(TUN_IP, ifname))
os.system("ip link set dev {} up".format(ifname))
```

> * Get the data from the socket interface; treat the received data as an IP packet.

The code below is used to get data from the socket interface. The IP address `IP_A` is set to `10.0.2.5` because the socket should bind to the VPN Server's IP on NAT Network, which is `10.0.2.5`. The port number is set as `9090` as it is an unused port.

``` python
IP_A = "10.0.2.5"
PORT = 9090

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((IP_A, PORT))
```

After the socket is bound, the code listens to the port, then treats the received data as an IP packets as seen below.

``` python
while True:
    data, (ip, port) = sock.recvfrom(2048)
    print("{}:{} --> {}:{}".format(ip, port, IP_A, PORT))
    pkt = IP(data)
```

> * Write the packet to the TUN interface.

After the packet has been treated as an IP packet, the packet is then written to the TUN interface as seen below. This is done within the `while True` loop.

``` python
os.write(tun, bytes(pkt))
```

> **Testing.** If everything is set up properly, we can `ping` Host V from Host U. The ICMP echo request packets should eventually arrive at Host V through the tunnel. Please show your proof. It should be noted that although Host V will 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) that the ICMP packets have arrived at Host V.

Before the code is run, the VPN Server should be set up to behave as a gateway with the following command:

``` shell
sudo sysctl net.ipv4.ip_forward=1
```

On the VPN Server, the `tun_server.py` code is run with `sudo ./tun_server.py`. Start up Host V if not done so, and start up Wireshark to trace packets. On the VPN Client, run `tun_client.py` created in task 3. In a separate terminal in VPN Client, run `sudo ip route add 192.168.60.0/24 dev lin0 via 192.168.53.99` and then ping Host V with `ping 192.168.60.101`.

The packet trace on Host V is as seen below.

![](https://i.imgur.com/jmqaCvV.png)

As seen, Host V receives the echo request packets from the Host U / VPN Client, meaning that the task was performed successfully.

## Task 5: Handling Traffic in Both Directions

*Note: The `tun_server.py` code is saved as `gateway/task5.py`, and the `tun_client.py` code is saved as `host_u/task5.py`.*

On Host V, the default gateway has to be set as the Gateway with the following command:

``` shell
sudo route add default gw 192.168.60.1
```

On the VPN Client, the `while True` loop in `tun_client.py` was modified such that the Linux's `select.select()` system call can be used to block the process until data is available on one of the file descriptors in the set. This is done with the `ready, _, _ = select.select([sock, tun], [], [])` line. 

If the file descriptor is pointing to the socket, the packet should be written to the TUN interface with `os.write(tun, bytes(pkt))`. If the file descriptor is pointing to the TUN interface, the packet should be written to the socket with `sock.sendto(packet, (SERVER_IP, SERVER_PORT))`.

As such, the `while True` loop should look like the following:

``` python
while True:
    # this will block until at least one interface is ready
    ready, _, _ = select.select([sock, tun], [], [])
    for fd in ready:
        if fd is sock:
            data, (ip, port) = sock.recvfrom(2048)
            pkt = IP(data)
            print("From socket <==: {} --> {}".format(pkt.src, pkt.dst))
            os.write(tun, bytes(pkt))

        if fd is tun:
            packet = os.read(tun, 2048)
            pkt = IP(packet)
            print("From tun ==>: {} --> {}".format(pkt.src, pkt.dst))
            sock.sendto(packet, (SERVER_IP, SERVER_PORT))
```

On the VPN Server, similar to the VPN Client, `select.select()` is used as well to block the process until data is available on one of the file descriptors in the set.

If the file descriptor is pointing to the socket, the packet should be written to the TUN interface with `os.write(tun, data)`. If the file descriptor is pointing to the TUN interface, the packet should be writte to the socket with `sock.sendto(packet, (ip, port))`. As the variables `ip` and `port` may not be defined by the socket before it is used, we initially define `ip` with the Gateway's NAT Network IP `10.0.2.4` and define `port` as an arbitrary integer.

These modifications are then added to the `tun_server.py` file, so the last part of the file should look like the following.

``` python
# imports and setting constants
# create and configure tun interface
# set up and bind socket

ip = "10.0.2.4" # NAT Network IP for VPN Client
port = 10000 # dummy port to be replaced when values are read from socket

while True:
    # this will block until at least one interface is ready
    ready, _, _ = select.select([sock, tun], [], [])
    for fd in ready:
        if fd is sock:
            data, (ip, port) = sock.recvfrom(2048)
            pkt = IP(data)
            print("From socket <==: {} --> {}".format(pkt.src, pkt.dst))
            os.write(tun, data)

        if fd is tun:
            packet = os.read(tun, 2048)
            pkt = IP(packet)
            print("From tun ==>: {} --> {}".format(pkt.src, pkt.dst))
            sock.sendto(packet, (ip, port))
```

> **Testing.** Once this is done, we should be able to communicate with Machine V from Machine U, and the VPN tunnel (un-encrypted) is now complete. Please show your wireshark proof using about ping and telnet commands. In your proof, you need to point out how your packets flow.

To proof that the VPN tunnel works and trace the sequence of events, a single ping can be done from Host U to Host V with `ping 192.168.60.101` as seen below.

![](https://i.imgur.com/NN4wT3H.png)

As the time across machines is not synchronised, we will use the gaps in time between echo requests and replies to determine the sequence of events.

On Host U, Wireshark captured the following trace. The time gap is 0.015716125.

![](https://i.imgur.com/OAQOvMa.png)

On Gateway, Wireshark captured the following trace. The time gap is 0.000453958.

![](https://i.imgur.com/sAOv4BZ.png)

On Host V, Wireshark captured the following trace. The time gap is 0.00002854.

![](https://i.imgur.com/wToIW10.png)

As such, based on the time differences logged on Wireshark, the echo request packets are first sent from Host U's TUN interface (`192.168.53.99`) to Gateway's TUN interface (`192.168.53.98`). This is then written to the Gateway's socket on `10.0.2.5`, and the packet is subsequently forwarded by the Gateway (`192.168.60.1`) to Host V (`192.168.60.101`) through the Internal Network. 

After which, as the requested host `192.168.60.101` has been found, an echo reply packet is generated by Host V and sent back by the path through which the echo request packets were delivered: from Host V (`192.168.60.101`) to Gateway (`192.168.60.1`) through the Internal Network, then to Gateway's socket which writes to Gateway's TUN interface (`192.168.53.98`), then from Gateway's TUN interface to Host U's TUN interface (`192.168.53.99`), then to Host U's socket on `10.0.2.4`.

A similar process can be observed with `telnet` as well. Host U can `telnet` to Host V using `telnet 192.168.60.101` as shown below.

![](https://i.imgur.com/7gLSOOi.png)

Below is the Wireshark packet trace on Host U.

![](https://i.imgur.com/3X3Siz4.png)

Below is the Wireshark packet trace on Gateway.

![](https://i.imgur.com/fiv1V2b.png)

Below is the Wireshark packet trace on Host V.

![](https://i.imgur.com/g2kWe0u.png)

As `ping` and `telnet` from Host U to Host V can be performed successfully, it can be seen that the VPN tunnel is complete and enables Host U to communicate with Host V and vice versa.

## Task 6: Tunnel-Breaking Experiment

*Note: The `tun_server.py` code is saved as `gateway/task5.py`, and the `tun_client.py` code is saved as `host_u/task5.py`.*

When Host U is successfully connected to Host V through `telnet`, and a letter is typed in the terminal, Wireshark on Host U captures the following packets.

![](https://i.imgur.com/tHnN7oW.png)

This shows that there is a pair of telnet packets being exchanged between `192.168.53.99` and `192.168.60.101`, followed by a TCP packet sent from `192.168.53.99` to `192.168.60.101`.

> On Host U, `telnet` to Host V. While keeping the `telnet` connection alive, we break the VPN tunnel by stopping the `tun_client.py` or `tun_server.py` program. We then type something in the `telnet` window. Do you see what you type? What happens to the TCP connection? Is the connection broken?

The VPN tunnel is broken by stopping the `tun_server.py` script. After which, I cannot see what I type. Wireshark on Host U picks up the following packet trace.

![](https://i.imgur.com/c2mOas0.png)

The first row indicating the telnet packet shows that the TUN interface on Host U is trying to send a telnet packet to Host V. However, the packet cannot be sent as the TCP connection is broken. As such, Host U sends TCP Keep-Alive packets instead to determine if Host V can be contacted at regular intervals. This is so as to send the TCP packet containing queued content once the connection is revived.

> Let us now reconnect the VPN tunnel (do not wait for too long). We will run the `tun_client.py` and `tun_server.py` programs again, and set up their TUN interfaces and routing (this is where you can find that including the configuration commands in the programs will make your life much easier). Once the tunnel is re-established, what is going to happen to the `telnet` connection? Please describe and explain your observations.

The VPN tunnel is reconnected by running `sudo ./tun_server.py` on the VPN server.

After the tunnel is re-established, the `telnet` connection is revived, the typed content appears on the terminal of Host U.

The Wireshark packet trace on Host U is as shown below.

![](https://i.imgur.com/JYl65gD.png)

Referring to the packet trace above, we can see that after the TCP connection is re-established, the telnet packets were successfully sent to Host V. Note that the length of the packets are more than 53, which means that more than one typed letter is contained in each of these packets, which makes sense as more than 1 character was typed when the connection was broken.

After the tunnel connection is established again, the terminal acts as it usually would, displaying each character as they are typed, with each telnet packet sent having a length of 53.
