TCPImplementation

Project Goal

  • Learn how to write rust
  • Learn how to implement TCP protocal

Implement TCP in Rust

For this page, I will take notes on tcp concepts

Getting the basics


1. Three-Way Handshake (Connection Establishment)

The TCP connection starts with the SYN → SYN-ACK → ACK process.

Client                               Server
   |                                   |
   | ---------- SYN ---------->        |  (Client requests connection)
   |                                   |
   | <-------- SYN-ACK --------        |  (Server acknowledges)
   |                                   |
   | ---------- ACK ---------->        |  (Client confirms connection)
   |                                   |
Connection Established!
  • SYN: Client sends a connection request to the server.
  • SYN-ACK: Server acknowledges the request and sends its own connection request.
  • ACK: Client acknowledges the server’s response.

2. Data Transmission

Once the connection is established, data flows in both directions with sequence numbers and acknowledgments.

Client                               Server
   |                                   |
   | ------ DATA [SEQ=1] ------>       |  (Send data with sequence number 1)
   | <--- ACK [SEQ=2, ACK=2] ----      |  (Server acknowledges receipt)
   |                                   |
   | <------ DATA [SEQ=2] ------       |  (Server sends data back)
   | ------ ACK [SEQ=3, ACK=3] -->     |  (Client acknowledges receipt)
   |                                   |
  • SEQ: Sequence number identifies the position of the data.
  • ACK: Acknowledgment ensures the previous data was received.

TCP guarantees reliability through:

  1. Sequence Numbers: To order packets.
  2. Acknowledgments: To confirm delivery.
  3. Retransmission: If a packet is lost.

3. Connection Termination (Four-Way Handshake)

To close the connection, TCP uses a FIN → ACK → FIN → ACK sequence.

Client                               Server
   |                                   |
   | -------- FIN -------->            |  (Client wants to close connection)
   | <-------- ACK --------            |  (Server acknowledges FIN)
   |                                   |
   | <-------- FIN --------            |  (Server closes connection)
   | -------- ACK -------->            |  (Client acknowledges FIN)
   |                                   |
Connection Closed!
  • FIN: A “finish” flag to indicate one side is done sending data.
  • ACK: Acknowledge the connection is closing.

Overall Summary

  Connection Start: Three-Way Handshake
  -------------------------------------
  SYN -> SYN-ACK -> ACK

  Data Transmission: Reliable Data Flow
  -------------------------------------
  DATA [SEQ] -> ACK [SEQ]
  (Ensures order, error checking, and retransmission)

  Connection End: Four-Way Handshake
  ----------------------------------
  FIN -> ACK -> FIN -> ACK

This is a simplified version of TCP to help you visualize how it works step by step. Each step ensures a reliable connection with ordered data delivery, acknowledgments, and connection closure.

Code

References

3.1.  Internet Header Format

  A summary of the contents of the internet header follows:

                                    
    0                   1                   2                   3   
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |Version|  IHL  |Type of Service|          Total Length         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |         Identification        |Flags|      Fragment Offset    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Time to Live |    Protocol   |         Header Checksum       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                       Source Address                          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Destination Address                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

                    Example Internet Datagram Header

                               Figure 4.

  Note that each tick mark represents one bit position.

  Version:  4 bits

    The Version field indicates the format of the internet header.  This
    document describes version 4.

  IHL:  4 bits

    Internet Header Length is the length of the internet header in 32
    bit words, and thus points to the beginning of the data.  Note that
    the minimum value for a correct header is 5.

What is a TUN Interface

let nic = tun_tap::Iface::new("tun0", tun_tap::Mode::Tun)?;
  • a TUN interface is a virtual network device that operates at the IP level(layer 3)
  • it is used for packet routing and is often employed for VPNs, network simulations, or custom routing logic

receiving data

let nbytes = nic.recv(&mut buf[..])?;
  • need to grant capabilities to executable files.

  • linix command: setcap

  • create a ip

extern crate tun_tap;  // Declares the use of the tun_tap crate, which provides bindings for creating and managing TUN/TAP interfaces.
extern crate hex; // hexdecimal encoding/decoding

use std::io;
use hex_fmt::HexFmt;

fn main() -> io::Result<()> {
    // see if tun_tap working
    /*
    * Ok(()) : Success
    * Err(io::Error) : an error from the standard I/O library
    * */

    print!("======run till here 0=============\n");
    // create a TUN interface
    // network interface name: `tun0`
    // `tun_tap::Mode::Tun`: specifies that this is a **TUN interface** (layer 3 - IP packets)
    let nic = tun_tap::Iface::new("tun0", tun_tap::Mode::Tun)?;
    print!("======run till here 1=============\n");

    // create a new interface
    // this buffer will hold the incoming packet data from the TUN interface
    let mut buf = [0u8; 1504]; // create a buffer of size 1504 bytes
    print!("======run till here 2=============\n");

    // receiving data
    // this is a **blocking operation**, the program will wait until a packet is received
    let nbytes = nic.recv(&mut buf[..])?;  // Receives a packet from the interface, return #bytes copied into the buffer
    print!("======run till here 3=============\n");

    // nbytes: the number of bytes read into buffer
    // &buf[..nbytes] : a slice of the buffer containing only the received data
    // {:x} : attempts to format the buffer slice as hexadecimal
    print!("======run till here 4=============\n");
    eprintln!("read {} bytes: {}", nbytes, HexFmt(&buf[..nbytes]));

    Ok(()) // success
}

run Rust program

cargo r --release

we will get this output

root@61cbf190478a:/home/developer/tcp_implementation# cargo r --release
    Finished `release` profile [optimized] target(s) in 0.12s
     Running `target/release/tcp_implementation`
^@^@

execute the excutable

get perminssion for the excutatble

setcap cap_net_admin=eip target/release/tcp_implementation

check the ip address

ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
3: gre0@NONE: <NOARP> mtu 1476 qdisc noop state DOWN group default qlen 1000
    link/gre 0.0.0.0 brd 0.0.0.0
4: gretap0@NONE: <BROADCAST,MULTICAST> mtu 1462 qdisc noop state DOWN group default qlen 1000
    link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
5: erspan0@NONE: <BROADCAST,MULTICAST> mtu 1450 qdisc noop state DOWN group default qlen 1000
    link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
6: ip_vti0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
7: ip6_vti0@NONE: <NOARP> mtu 1332 qdisc noop state DOWN group default qlen 1000
    link/tunnel6 :: brd :: permaddr 3e76:a20:6dc::
8: sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000
    link/sit 0.0.0.0 brd 0.0.0.0
9: ip6tnl0@NONE: <NOARP> mtu 1452 qdisc noop state DOWN group default qlen 1000
    link/tunnel6 :: brd :: permaddr 56e3:7e5a:1fcb::
10: ip6gre0@NONE: <NOARP> mtu 1448 qdisc noop state DOWN group default qlen 1000
    link/gre6 :: brd :: permaddr 1256:ba4f:67ba::
20: tun0: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN group default qlen 500
    link/none
22: eth0@if23: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:15:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.21.0.2/16 brd 172.21.255.255 scope global eth0
       valid_lft forever preferred_lft forever
  • notice there is one called tun0 which created by us

Then we use the following commands:


ip addr add 192.168.0.1/24 dev tun0

ip link set up dev tun0  # notice this line

whenever we execute this line, our rust program(which running also) will run main

root@61cbf190478a:/home/developer/tcp_implementation# target/release/tcp_implementation
======run till here 0=============
======run till here 1=============
======run till here 2=============
======run till here 3=============
======run till here 4=============
read 52 bytes: 000086dd6000000000083afffe80000000000000707e1f5329cf04abff0200000000000000000000000000028500beeb00000000

we are getting the hexdecimal, lets split it into arr:

eprintln!("read {} bytes: {}", nbytes, HexFmt(&buf[..nbytes]));

change to

eprintln!("read {} bytes: {:02x?}", nbytes, &buf[..nbytes]);

Lets also create a run.sh

#!/bin/bash

# 1. build the rust project
cargo b --release

# 2. add executable permission (no need as I'm root)
# setcap cap_net_admin=eip ../target/release/tcp_implementation

# 3. run rust executable in background
../target/release/tcp_implementation &
pid=$! # create a thread to wait response
# 4. add tun0 ip addr
ip addr add 192.168.0.1/24 dev tun0
ip link set up dev tun0

# 5. check ip addr
# ip addr
# wait process to finish
wait $pid

we see the bytes:

read 52 bytes: [00, 00, 86, dd, 60, 00, 00, 00, 00, 08, 3a, ff, fe, 80, 00, 00, 00, 00, 00, 00, af, ef, b2, 6a, 91, 4b, 1b, ed, ff, 02, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 02, 85, 00, 6d, a4, 00, 00, 00, 00]

but we don’t know what exactly they are, so let’s try to interpret them

  • package use: tshark

create a infinite loop to keep recieving data

    // receiving data
    // this is a **blocking operation**, the program will wait until a packet is received
    loop{
        let nbytes = nic.recv(&mut buf[..])?;  // Receives a packet from the interface, return #bytes copied into the buffer
        //print!("======run till here 3=============\n");

        // nbytes: the number of bytes read into buffer
        // &buf[..nbytes] : a slice of the buffer containing only the received data
        // {:x} : attempts to format the buffer slice as hexadecimal
        //print!("======run till here 4=============\n");
        eprintln!("read {} bytes: {:02x?}", nbytes, &buf[..nbytes]);
    }

if we first ping the tun0 with a address, we will get continuing packet recieved

  1. run the rust program with the shell script we created
sh run.sh
  1. ping the tun0 we created with a ip address
ping -I tun0 192.168.0.2

we will get continue packets received

root@61cbf190478a:/home/developer/tcp_implementation/src# read 52 bytes: [00, 00, 86, dd, 60, 00, 00, 00, 00, 08, 3a, ff, fe, 80, 00, 00, 00, 00, 00, 00, 33, 83, 88, 94, bd, 02, be, f2, ff, 02, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 02, 85, 00, 45, 2a, 00, 00, 00, 00]
read 88 bytes: [00, 00, 08, 00, 45, 00, 00, 54, 27, 0d, 40, 00, 40, 01, 92, 48, c0, a8, 00, 01, c0, a8, 00, 02, 08, 00, 85, dd, 00, 01, 00, 01, e1, 01, 66, 67, 00, 00, 00, 00, 6b, e4, 00, 00, 00, 00, 00, 00, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 1a, 1b, 1c, 1d, 1e, 1f, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 2a, 2b, 2c, 2d, 2e, 2f, 30, 31, 32, 33, 34, 35, 36, 37]
read 88 bytes: [00, 00, 08, 00, 45, 00, 00, 54, 28, 5c, 40, 00, 40, 01, 90, f9, c0, a8, 00, 01, c0, a8, 00, 02, 08, 00, 41, db, 00, 01, 00, 02, e2, 01, 66, 67, 00, 00, 00, 00, ad, e5, 01, 00, 00, 00, 00, 00, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 1a, 1b, 1c, 1d, 1e, 1f, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 2a, 2b, 2c, 2d, 2e, 2f, 30, 31, 32, 33, 34, 35, 36, 37]
read 88 bytes: [00, 00, 08, 00, 45, 00, 00, 54, 29, f3, 40, 00, 40, 01, 8f, 62, c0, a8, 00, 01, c0, a8, 00, 02, 08, 00, 65, 7c, 00, 01, 00, 03, e3, 01, 66, 67, 00, 00, 00, 00, 88, 43, 02, 00, 00, 00, 00, 00, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 1a, 1b, 1c, 1d, 1e, 1f, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 2a, 2b, 2c, 2d, 2e, 2f, 30, 31, 32, 33, 34, 35, 36, 37]

video timestamp: Implementing TCP in Rust 31:02, Jon Gjengset

Issue face and fixing

Issue face and fixing

functions learning:

The u16::from_be_bytes() function in Rust constructs a u16 integer from a 2-element byte array interpreted in big-endian (network) byte order. This means the most significant byte is at the first position of the array.

Parameters:

  • bytes: A [u8; 2] array representing the bytes in big-endian order.

Output:

  • Returns a u16 integer composed from the provided byte array.

Example:

fn main() {
    let byte_array: [u8; 2] = [0x12, 0x34];
    let number = u16::from_be_bytes(byte_array);
    println!("The number is: {}", number);
    // Output: The number is: 4660
}

Explanation: In this example, the byte array [0x12, 0x34] is interpreted as the hexadecimal number 0x1234, which equals 4660 in decimal. The u16::from_be_bytes() function correctly assembles these bytes into the u16 integer 4660.

This function is particularly useful when dealing with data from network protocols or file formats that use big-endian byte ordering. It ensures that the byte sequence is interpreted correctly as a u16 value on any platform.

For more details, refer to the official Rust documentation

pgrep -af target

ex. Output

root@61cbf190478a:/home/developer/tcp_implementation/src# pgrep -af target
5153 ../target/release/tcp_implementation

is used to search for processes whose name or arguments match the string target and display detailed information about them.

pgrep -af <pattern>

What It Does:

  1. Searches for processes where the full command line contains the specified pattern (in this case, “target”).
  2. Prints the process IDs (PIDs) and their full command lines for all matching processes.

we run run.sh, we get

read 48 bytes (flags: 0, proto: 86dd: [60, 0, 0, 0, 0, 8, 3a, ff, fe, 80, 0, 0, 0, 0, 0, 0, 64, 35, cb, f9, 8f, e3, aa, 94, ff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 85, 0, 12, 90, 0, 0, 0, 0])

so, what does protocal number 86dd do:

The hexadecimal value 0x86DD is the EtherType that indicates the payload of an Ethernet frame contains an IPv6 (Internet Protocol version 6) packet.

EtherType in Ethernet Frames:

In Ethernet networking, the EtherType field is a two-octet (16-bit) value within an Ethernet frame that specifies the protocol encapsulated in the payload. This field helps the receiving device determine how to process the incoming data.

Significance of 0x86DD:

When the EtherType field is set to 0x86DD, it signifies that the payload is an IPv6 packet. This designation is crucial for network devices to correctly interpret and route IPv6 traffic.

Example Ethernet Frame Structure with IPv6:

+-------------------+-------------------+-------------------+
| Destination MAC   | Source MAC        | EtherType = 0x86DD|
+-------------------+-------------------+-------------------+
| IPv6 Header and Payload                               |
+-------------------------------------------------------+

In this structure:

  • Destination MAC: The hardware address of the destination device.
  • Source MAC: The hardware address of the source device.
  • EtherType: 0x86DD, indicating the payload is an IPv6 packet.
  • IPv6 Header and Payload: The actual IPv6 data being transmitted.

Note:

It’s important to distinguish between EtherType values and IP protocol numbers. EtherType values, like 0x86DD, are used in Ethernet frames to indicate the encapsulated protocol. In contrast, IP protocol numbers are used within the IP header to specify the next-level protocol (e.g., TCP, UDP).

For more detailed information, you can refer to the EtherType Wikipedia page.

[!NOTE] When receiving packets from a TUN interface (or similar network tap), you may get both IPv4 and IPv6 packets, and you have to manually filter them based on your requirements. Here’s why and how this works:


Why Both IPv4 and IPv6 Packets Are Received

  1. TUN Interface Behavior:

    • A TUN interface operates at the network (IP) layer and captures raw IP packets.
    • Since IPv4 and IPv6 are both network layer protocols, a TUN interface doesn’t inherently differentiate between them—it passes everything it receives from the kernel’s networking stack.
  2. Mixed Traffic in Networks:

    • Many networks support both IPv4 and IPv6 traffic simultaneously (dual-stack environments).
    • Without filtering, you might receive a mix of packets intended for both protocols, leading to unnecessary complexity if you’re only interested in one.

How to Differentiate IPv4 and IPv6 Packets

The protocol field in the packet header helps identify whether a packet is IPv4 or IPv6:

IPv4 Packets:

  • EtherType/Protocol Number: 0x0800 (decimal: 2048).

IPv6 Packets:

  • EtherType/Protocol Number: 0x86DD (decimal: 34525).

Manually Filtering Packets in Your Code

You can use the protocol field from the packet’s metadata to filter out unwanted packets.

Example in Rust:

loop {
    let nbytes = nic.recv(&mut buf[..])?;
    
    // Extract protocol field (bytes 2-3)
    let proto = u16::from_be_bytes([buf[2], buf[3]]);

    // Check protocol type
    match proto {
        0x0800 => {
            // Handle IPv4 packet
            eprintln!("Skipping IPv4 packet");
        },
        0x86DD => {
            // Handle IPv6 packet
            eprintln!("Processing IPv6 packet: {:02x?}", &buf[4..nbytes]);
        },
        _ => {
            // Unknown protocol
            eprintln!("Unknown protocol: {:x}", proto);
        }
    }
}

Output:

  • IPv4 packets will be skipped with a message: Skipping IPv4 packet.
  • IPv6 packets will be processed further and logged.

Advantages of Filtering:

  1. Streamlining Data Processing:

    • By focusing only on the relevant protocol, you reduce the complexity of your packet analysis or handling logic.
  2. Resource Optimization:

    • Ignoring unnecessary packets saves computational resources and improves performance, especially in high-traffic environments.
  3. Focused Debugging/Analysis:

    • Isolating a specific protocol (e.g., IPv6) simplifies debugging and makes the data stream more manageable.
  • we need to add a filter ipv4(0x0800) to the program:
if proto != 0x0800 {
    // no ipv4
    continue
}

Now we need to encode the ipv4 header to our rust code

match etherparse::Ipv4HeaderSlice::from_slice(&buf[4..nbytes]) {
    Ok(p) => {
        /*
        nbytes: the number of bytes read into buffer
        &buf[..bytes]: a slice of the buffer containing only the received data
        {:x} : attempts to format the buffer slice as hexadecimal*/
        eprintln!(
                    "read {} bytes (flags: {:x}, proto: {:x}: {:x?})",
                    nbytes - 4,
                    flags,
                    proto,
                    p
                );
    }
    Err(e) => {
        eprintln!("ignoring weird packet {:?}", e)
    }
}

when ping 192.168.2.2 using tun0, we will get

read 84 bytes (flags: 0, proto: 800: Ipv4HeaderSlice { slice: [45, 0, 0, 54, ab, fa, 40, 0, 40, 1, 1f, ed, ac, 15, 0, 2, c0, a8, 2, 2]})

Lets parse and print out things we actually care about

add log when recieve ping

loop {
        let nbytes = nic.recv(&mut buf[..])?; // Receives a packet from the interface(when a packet arrives), return #bytes copied into the buffer
                                              //print!("======run till here 3=============\n");

        /*
         * u16::from_be_bytes
         * constructs a `u16` integer from a 2-element byte array interpreted in big-endian
         * (network) byte order. This means the most significant byte is at the first position
         * of the array
         *
         * Parameters:
         * `bytes`: a `[u8; 2] array representing the bytes in big-endian order
         *
         * Output
         * returns a `u16` integer composed from the provided byte array*/
        // the first 4 bytes of the buffer ar einterpreted as metadata
        // enthernet frame we got,
        // link level protocal
        let _eth_flags = u16::from_be_bytes([buf[0], buf[1]]); // byte 0-1: represent flags (interpreted as a u16 integer in big-endian order)
        let eth_proto = u16::from_be_bytes([buf[2], buf[3]]); // protocol type (e.g. IPv4, IPv6)

        // filter anything that is not ipv4 packet
        if eth_proto != 0x0800 {
            // no ipv4
            continue;
        }

        match etherparse::Ipv4HeaderSlice::from_slice(&buf[4..nbytes]) {
            Ok(p) => {
                // nbytes: the number of bytes read into buffer
                // &buf[..nbytes] : a slice of the buffer containing only the received data
                // {:x} : attempts to format the buffer slice as hexadecimal
                //print!("======run till here 4=============\n");
                // NOTE: get proto type : 86dd , which is the enthertype that indicates the payload of an
                // enthernet frame contains an IPV6 (internet protocol version 6) packet

                // ip level protocal, should be set to tcp
                let src = p.source_addr();
                let destination = p.destination_addr();
                let protocal = p.protocol();

                eprintln!(
                    "{} -> {} {}b of proto: {:x}  ",
                    src,
                    destination,
                    p.payload_len(),
                    protocal,
                );
            }
            Err(e) => {
                eprintln!("ignoring weird packet {:?}", e)
            }
        }
    }

and I got

192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1
192.168.2.1 -> 192.168.2.2 64b of proto: 1

run ip addr show tun0, get

root@61cbf190478a:/home/developer/tcp_implementation# ip addr show tun0
33: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 500
    link/none
    inet 192.168.2.1/24 scope global tun0
       valid_lft forever preferred_lft forever
    inet6 fe80::cc61:19f0:9760:9006/64 scope link stable-privacy
       valid_lft forever preferred_lft forever

the tun0 interface is configured correctly and active (state UP). The IP address 192.168.2.1/24 is assigned, and is in the UP state. The interface is ready to handle both IPv4 and IPv6 traffic.

  • assign an IPv4 Address to tun0
sudo ip addr add 192.168.2.1/24 dev tun0
sudo ip link set dev tun0 up
  • ensure proper routing

    • add a route for the IPv4 subnet:
    sudo ip route add 192.168.2.0/24 dev tun0
    

When your program prints Packet captured on tun0: protocol 1, length 88, it indicates that a packet with protocol number 1 and a total length of 88 bytes was captured on the tun0 interface.

Understanding Protocol Number 1:

In the IPv4 header, the “Protocol” field specifies the next-level protocol used in the data portion of the packet. The protocol number 1 corresponds to the Internet Control Message Protocol (ICMP). ICMP is primarily used for diagnostic or control purposes, such as the ping command, which sends ICMP Echo Request messages to test network connectivity.

Why is the Protocol Number Displayed as 1?

The protocol number is displayed as 1 because your program is likely printing the decimal value of the “Protocol” field from the IPv4 header. In the IPv4 header, protocol numbers are represented as 8-bit integers. While these numbers can be expressed in hexadecimal (base 16), they are often displayed in decimal (base 10) for readability. In this case, the decimal value 1 corresponds to the hexadecimal value 0x01, both representing ICMP.

Interpreting the Packet Length:

The length 88 indicates the total size of the captured packet in bytes. This includes the IPv4 header, the ICMP header, and any additional data payload. An ICMP Echo Request (commonly used by ping) typically consists of:

  • IPv4 Header: Usually 20 bytes.
  • ICMP Header: 8 bytes.
  • Data Payload: Variable length; in this case, approximately 60 bytes to reach a total packet size of 88 bytes.

Conclusion:

The message Packet captured on tun0: protocol 1, length 88 signifies that your program has intercepted an ICMP packet of 88 bytes on the tun0 interface. This is typical behavior when tools like ping are used, as they generate ICMP traffic to test network connectivity.

For a comprehensive list of IP protocol numbers and their corresponding protocols, you can refer to the IANA Protocol Numbers.