MyFTP is a simple Lab designed to help students quickly understand POSIX APIs.
In this lab, you will need to complete a simple FTP Server and a command-line interface (CLI) FTP Client.
- The MyFTP Client should support the following commands:
open <IP> <port>
: Establish a connection to<IP>:<port>
.auth <username> <password>
: Perform authentication with the server.ls
: Retrieve a list of files in the current directory on the server side, an example output is as follows:123.txt
get <filename>
: Download the<filename>
file from the server's running directory to the client's running directory with the same<filename>
.put <filename>
: Upload the<filename>
file from the client's running directory to the server's running directory with the same<filename>
.quit
: Disconnect if connected, then close the client.
- The MyFTP Server should support the following features:
- Access control: User login simplified to username
user
and password123123
. - Fetching file list: File list generation using the
ls
command, which may employpopen
orpipe+fork+execv
to obtain output from other processes. - File download.
- File upload.
- Access control: User login simplified to username
Based on the description in section "3. How to Test Locally," after compilation, it is expected to generate an executable ftp_client
in the build directory. You can start the Client by executing ./ftp_client
directly in the build directory.
For actual testing, we will start ftp_client
using its absolute path in any directory without any parameters.
Based on the description in section "3. How to Test Locally," after compilation, it is expected to generate an executable ftp_server
in the build directory. You can start the Server by executing ./ftp_server 127.0.0.1 12323
in the build directory (the Server listens to 127.0.0.1:12323).
For actual testing, we will start ftp_server
using its absolute path with IP and Port parameters as arguments, indicating the IP address and port the Server should listen to.
Here's a simple example: cd /home/jeremyguo/tmp && /home/jeremyguo/ftp/build/ftp_server 127.0.0.1 12323
.
This command starts the ftp_server
in the /home/jeremyguo/tmp
directory.
struct {
byte m_protocol[MAGIC_NUMBER_LENGTH]; /* protocol magic number (6 bytes) */
type m_type; /* type (1 byte) */
status m_status; /* status (1 byte) */
uint32_t m_length; /* length (4 bytes) in Big endian*/
} __attribute__ ((packed));
The Client and Server will exchange protocol information to perform various functions. All protocol messages are preceded by the protocol header shown in the code, communicated over the TCP protocol.
This means that every message on the 802.3 network will have the following form:
|802.3 Header|IP Header|TCP Header|myFTP Header|myFTP Data|
The TCP Header and previous headers will be generated by the system. What we need to implement is an application layer protocol, which is the content following the myFTP Header.
We add __attribute__ ((packed))
to avoid issues that may arise due to data structure alignment.
The document refers to requests as messages sent from the Client to the Server, and replies are messages sent from the Server to the Client.
For all messages, the m_protocol
field in the myFTP Header should be set to "\xe3myftp"
(which is the character '\xe3' followed by the string "myftp" for a total of 6 bytes). This field helps identify if the correct protocol version, i.e., myFTP protocol, is being used.
The m_type
field identifies the type of message being sent, with specific values discussed in 2.3.
m_status
will only be 0 or 1. The Server uses this option to inform the client of the current status, with specific values discussed in 2.3.
The message length m_length
is represented in big-endian and includes the total length of both header and data. You can assume that the message length will never exceed INT_MAX
defined in limits.h
.
The above figure describes the Client's state machine. For each Client, as well as for the Server itself, a simple state machine should be maintained like this.
The Client can only execute one request at a time, and only after one request is completed can the next one begin.
The Client and Server interact in the following steps:
- When a user starts the Client and enters the command
open SERVER_IP SERVER_PORT
(whereSERVER IP
andSERVER_PORT
are the IP address and port number of the Server, respectively), a TCP connection should be established to the Server's IP address and port. - Next, the Client sends a protocol message
OPEN_CONN_REQUEST
to the Server, requesting to open a connection. - If the Server is running (i.e., listening to the Server's port number), it will reply with a protocol message of type
OPEN_CONN_REQUEST
. - Finally, the Client receives a reply from the Server.
Please note:
- When the Server starts, its
OPEN_CONN_REQUEST
type response hasm_status=1
. - When a field is described as unused, neither the Server nor the Server will read the content of that field.
After successfully connecting, users need to authenticate themselves with the command auth USER PASS
.
USER
and PASS
are the username and password, respectively. Afterward, the Client sends an AUTH_REQUEST
type of protocol message to the Server containing the username and password. For simplicity, the username is user
and the password is 123123
at this time, the message's myFTP Data segment should be "user 123123\0"
.
If authentication is successful, then the Server will reply with a message type AUTH_REPLY
, and set m_status
to 1.
Otherwise, the Server will reply with AUTH_REPLY
, setting m_status
to 0.
If the Client receives a status of 0, then it immediately closes the connection (i.e., reverts to the IDLE
state in the state machine).
After a user is authenticated, they can perform the primary functions. Suppose a user wants to list all files stored on the Server. The user will issue a ls
command. The Client will then send a LIST_REQUEST
type protocol message to the Server. The Server will reply with a LIST_REPLY
protocol message along with a list of available files.
All files are stored in the program's working directory (note that the working directory is not equivalent to the executable's location but rather where the program was started from). Here are the assumptions about the repository directory:
- The directory is created before the Server starts.
- When the Server starts, the directory may contain files.
- The directory contains only regular files (i.e., no subdirectories, no link files, etc.).
- The Server process has the necessary permissions to access the directory and the files within it.
- Files consist only of a set of characters: letters (a-z and A-Z) and numbers (0-9).
- Executing the
ls
command in this directory, the total length of the returned result will not exceed 2047 characters.
To read the directory on the Server side, you can use popen
to obtain the result of the execution of the ls
program in Linux and return the result, noting that the end of the returned content should be added with a \0
to indicate the end of the content.
Suppose a user wants to download a file from the Server and issues a command get FILE
, where FILE
is the name of the file to be downloaded. The Client will then send a GET_REQUEST
type protocol message to the Server. The Server will first check if the file is available in its repository directory.
If the file does not exist, the Server will reply with a GET_REPLY
protocol message, with m_status
set to 0.
If the file exists, the Server will reply with a GET_REPLY
protocol message, with m_status
set to 1, and a FILE_DATA
message containing the file content.
Please note that a file can be in ASCII or binary mode. You should ensure that both ASCII and binary files are supported. Moreover, the program will overwrite any existing local file. Here, we assume that for each Client, only one file is downloaded at a time before the user issues the next command. The following figure shows the message flow for downloading a file.
The process you've outlined describes a typical upload interaction in a client-server application. The steps for the put
command in your system work as follows:
- The Client checks if the file exists locally. If it doesn't, it shows an error message stating that the file does not exist.
- If the file exists locally, the Client sends a
PUT_REQUEST
message to the Server. - The Server responds with a
PUT_REPLY
message and waits for the file. - The Client sends the file content with a
FILE_DATA
message.
For implementing the file upload functionality considering the stated assumptions and processes, here is a conceptual overview of what each part of the application should do:
-
Client Side (for the
put FILE
command):- Verify local file existence.
- Send
PUT_REQUEST
to Server. - On Server
PUT_REPLY
, send file content inFILE_DATA
messages.
-
Server Side (on receiving
PUT_REQUEST
):- Send
PUT_REPLY
to acknowledge request. - Receive the file content through
FILE_DATA
messages. - Save the file in the working directory under the provided name, being aware of the possibility of overwriting existing files.
- Send
The considerations for the file being ASCII or binary are essential. To handle both types of files, you might want to open files in binary mode (wb
for writing binary data in Python, for instance) to ensure that all byte content is handled correctly, without any alterations that text mode might cause.
Closing the connection follows the conventional request-reply pattern seen in other network protocols.
-
Client Side (for the
quit
command):- Send
QUIT_REQUEST
to Server. - Upon receiving
QUIT_REPLY
, release the connection and close the TCP connection. - Exit the Client program.
- Send
-
Server Side (on receiving
QUIT_REQUEST
):- Send
QUIT_REPLY
. - Perform any necessary cleanup before closing the connection, if required.
- Send
The example provided shows a standard interaction where a client follows a command-response pattern, reading inputs from STDIN
. Clients can be designed with more elaborate or user-friendly interfaces but should adhere to the protocol specified.
The instructions provided are for setting up a local testing environment using a provided repository from GitHub Classroom. Should there be any issues with testing, contacting the teaching assistant is advised.
Following the steps for cloning and updating the repository should establish the local environment required for further steps in the testing process.
Compiling the local test program involves creating a build directory within the test_local
folder and running cmake
and make
to build the test executables. Copying the necessary files to the build/
directory sets up the local testing environment.
Running make
within the build
directory of the root folder compiles the client and server applications, resulting in the ftp_client
and ftp_server
executables being created.
Executing the ftp_test
program from the build directory initiates the local testing procedure, which will simulate the client-server interaction and verify the correctness of the implemented protocol and functionalities.
The total score for this Lab is 120 points.
Some test points will be released before the deadline, and others will be tested uniformly after the deadline, but we will describe in detail the characteristics of all test point data in the data point content.
Students can push the repository to Github for automated testing (only up to 90 points will be visible before the deadline).
The table below provides each test point's corresponding ID and content, which you can test against individually using .\ftp_test --gtest_filter=ID
Or use wildcards such as .\ftp_test --gtest_filter=FTPServer.*
to test only the Server.
Each test point name consists of ${Category}.${Test Point Name}
Unless otherwise specified, each test point runs with only one Client at a time.
Category | Test Point Name | Test Content | Score Percentage | Released Before Deadline | Data Point Content |
---|---|---|---|---|---|
FTPServer | Open | Test OPEN_REQUEST | 10 | Yes | Receives a correct OPEN_REQUEST |
Auth | Test AUTH_REQUEST | 10 | Yes | Rejects unauthorized authentication and passes authorized authentication | |
Get | Test GET_REQUEST | 10 | Yes | Retrieves a file of size 3 bytes with a filename no longer than 3 bytes, with randomly generated names and content | |
Put | Test PUT_REQUEST | 10 | Yes | Uploads a file of size 3 bytes with a filename no longer than 3 bytes, with randomly generated names and content | |
List | Test LIST_REQUEST | 10 | Yes | Retrieves a list of all files in a directory | |
GetBig | Test GET_REQUEST | 10 | No | Retrieves a 1MB file with an 8-byte filename, with randomly generated names and content | |
PutBig | Test PUT_REQUEST | 10 | No | Uploads a 1MB file with an 8-byte filename, with randomly generated names and content | |
MultiPut | Test PUT_REQUEST | 0 | Yes | Four Clients execute FTPServer.Put concurrently | |
MultiGet | Test GET_REQUEST | 0 | Yes | Four Clients execute FTPServer.Get concurrently | |
FTPClient | Open | Test OPEN_REQUEST | 10 | Yes | Successfully establishes a connection with the Server |
Auth | Test AUTH_REQUEST | 10 | Yes | Sends the correct username for verification and rejects the incorrect one | |
Get | Test GET_REQUEST | 10 | Yes | Retrieves a file of size 3 bytes with a filename no longer than 3 bytes, with the filename and content generated randomly | |
Put | Test PUT_REQUEST | 10 | Yes | Uploads a file of size 3 bytes with a filename no longer than 3 bytes, with the filename and content generated randomly | |
GetBig | Test GET_REQUEST | 5 | No | Retrieves a 1MB file with an 8-byte filename, with the filename and content generated randomly | |
PutBig | Test PUT_REQUEST | 5 | No | Uploads a 1MB file with an 8-byte filename, with the filename and content generated randomly |
Each RTP packet contains an RTP header that follows immediately after the UDP header with the format specified below:
typedef struct RTP_Header {
uint8_t type;
uint16_t length;
uint32_t seq_num;
uint32_t checksum;
} rtp_header_t;
For simplicity, all fields in the RTP header are in little-endian byte order.
type: Identifies the type of RTP packet, with 0:START
, 1:END
, 2:DATA
, 3:ACK
length: Indicates the length of the data segment of an RTP packet (i.e., the length of the packet following the RTP header). For START
, END
, and ACK
type packets, the length is 0.
seq_num: A sequence number used to identify the order for sequential delivery.
checksum: A 32-bit CRC value calculated over the RTP header and the RTP data segment. Note that when calculating the checksum, the checksum field should be initialized to 0.
Establishing Connection: The sender
initiates the connection by sending an RTP packet with type START
and a seq_num
as a random value. The sender then waits for an ACK
packet with the same seq_num
, and upon receipt, the connection is established.
Data Transmission: After the connection is established, the data to be sent is transmitted using DATA
type packets. The seq_num
for the sender's data packets starts at 0 and is incremented by 1 for each packet. Note that the seq_num
used for data transmission is unrelated to the seq_num
used for establishing the connection.
Terminating Connection: Once data transmission is complete, the sender sends a END
type packet to terminate the connection. To ensure all data has been transmitted, the seq_num
of the sender's END
packet should match the seq_num
of the next packet. The connection is disconnected after receiving an ACK
packet with the matching seq_num
.
Packet Size: When intending to transmit a set of data, it cannot be sent in one go as IP packets in the network typically do not exceed 1500 Bytes (otherwise they will be automatically fragmented), with a typical IP header being 20 bytes and the UDP header being 8 bytes. Therefore, you need to ensure that your RTP packet total length does not exceed 1472 bytes (RTP header + data).
The sender
should read the input message, send it to the specified recipient using the RTP protocol via a UDP socket.
The sender
needs to split the input message into appropriately sized chunks and attach a checksum
to each packet. Please use the 32-bit CRC calculation program provided in our src/util.c
to add checksums to the packets.
The seq_num
increases by 1 for every additional packet in the connection.
You will use a sliding window mechanism to implement reliable transmission. The window_size
is a startup parameter that ensures the number of packets currently in transmission and not yet acknowledged by the receiver
does not exceed the window_size
.
After transmitting the entire message, the sender
should send an END
packet to mark the termination of the connection.
The sender
must consider reliable transmission under the following network conditions:
- Packet loss occurring at any layer.
- Out-of-order arrival of
ACK
packets. - Reception of arbitrary numbers of any packets multiple times.
- Delayed
ACK
packets. - Corrupted packets.
Each DATA
packet will be acknowledged by an ACK
packet. To handle cases where DATA
packets are lost or ACK
packets are lost, you need to set a timer for automatic retransmission of unacknowledged DATA
packets.
The timer is reset upon sliding window movement and, if reached 100ms, necessitates the retransmission of all DATA
packets in the current window.
You are required to implement the following three functions in the sender_def.c
program, all of which have been declared in sender_def.h
.
/**
* @brief Establishes an RTP connection
* @param receiver_ip IP address of the receiver
* @param receiver_port Port of the receiver
* @param window_size Size of the window
* @return -1 indicates connection failure, 0 indicates success
**/
int initSender(const char* receiver_ip, uint16_t receiver_port, uint32_t window_size);
/**
* @brief Sends data
* @param message The name of the file to send
* @return -1 indicates sending failure, 0 indicates success
**/
int sendMessage(const char* message);
/**
* @brief Terminates the RTP connection and closes the UDP socket
**/
void terminateSender();
The purpose of implementing these three functions is for ease of testing, as the test program will call these functions directly. Therefore, be careful not to use incorrect function definitions. Note that you do not need to implement a complete
sender
. Of course, you can use these functions to implement your own executable program forsender
, which is straightforward.
Please avoid using commands like
exit
within function implementations that cause the process to exit, as this would also stop the evaluation program from executing.
The receiver
must accurately and completely receive and store the information sent by the sender
. Note that this should only consider scenarios with a single sender
.
The receiver
is responsible for calculating the CRC32 checksum
, and any packet with an incorrect checksum
should be discarded.
For every START
or END
packet that is acknowledged received, you need to send an ACK
packet, with seq_num
as described in the "RTP Technical Specification."
For each DATA
packet that is acknowledged received, an ACK
packet should be sent, with seq_num
set to the sequence number of the next expected DATA
packet.
For example, there are the following two scenarios (assuming the next expected packet has a seq_num
of N):
-
If the current packet has a
seq_num
greater than and not equal to N: receive and buffer the packet, and send anACK
withseq_num
=N. Note that this differs slightly from the Go-Back-N (GBN) mechanism discussed in class. GBN discards out-of-order packets entirely, while here, the receiver buffers them, making the mechanism more efficient than GBN. -
If the current packet's
seq_num
is equal to N: send anACK
withseq_num
=M, where M is the smallestseq_num
of the packet not yet buffered, and write all buffered packets to the file in order up to M (including the most recently received packet).
Packets with seq_num
>= N + window_size should be directly discarded to ensure the receiver
window size does not exceed window_size
.
You should implement the following three functions in the receiver_def.c
program, all of which have been declared in receiver_def.h
:
/**
* @brief Start the receiver and listen for connections on port across all IPs.
*
* @param port The port that the receiver is listening to.
* @param window_size The size of the window.
* @return -1 indicates a connection failure, 0 indicates success.
*/
int initReceiver(uint16_t port, uint32_t window_size);
/**
* @brief Receives data and then terminates the RTP connection after reception is complete.
* @param filename The filename to receive data.
* @return >0 represents the number of bytes of data received after completion, -1 represents other errors.
*/
int recvMessage(char* filename);
/**
* @brief Terminates the RTP connection and closes the UDP socket in case of data reception failure.
*/
void terminateReceiver();
The purpose of implementing these three functions is to facilitate testing. The test program will directly call these three functions, so please be careful not to use incorrect function definitions. Note, you do not need to implement a complete
receiver
; however, you can use these three functions to implement your own executablereceiver
program, which is not complicated.
Please do not use commands like
exit
within the functions that cause the process to exit, as this will cause the evaluation program to stop running as well.
In this section, you will make some modifications to the programs written in the previous parts. Consider how the programs written in the previous sections would behave in the following situation for a window size of 3 as shown below:
[The diagram is displayed]
In this situation, the receiver will return two ACK
packets with a seq_num
of 0, causing the sender to time out and retransmit DATA
packets 0, 1, and 2. However, since the receiver has already received and buffered DATA
packets 1 and 2, retransmitting these two packets is unnecessary.
To address this, you need to modify your receiver
and sender
accordingly:
-
The
receiver
no longer sends cumulativeACK
packets, meaning for every receivedDATA
packet, thereceiver
no longer sends anACK
carrying theseq_num
of the next expectedDATA
packet but sends anACK
with the sameseq_num
as the receivedDATA
packet. -
The
receiver
still maintains anN
representing theseq_num
of the next expectedDATA
packet and should directly discard anyDATA
packets carrying aseq_num
that is greater than or equal toN+window_size
. -
The
sender
needs to keep track of all receivedACK
packets in the current window and only retransmit thoseDATA
packets that have not been acknowledged upon timeout.
After optimization, in the previous example, as shown below, the optimized sender
would wait until timeout and then only retransmit DATA
packet 0.
You should implement the following functions in sender_def.c
and receiver_def.c
. Note that this function needs to be compatible with the previously written initSender, initReceiver
and terminateSender, terminateReceiver
functions:
/**
* @brief Used for sending data (optimized version of RTP)
* @param message The filename of the data to send
* @return -1 indicates sending failed, 0 indicates sending success
**/
int sendMessageOpt(const char* message);
/**
* @brief Used for receiving data and then disconnecting the RTP connection after completion (optimized version of RTP)
* @param filename The filename for receiving data
* @return >0 indicates the byte count of data received after completion, -1 indicates other errors occurred
*/
int recvMessageOpt(char* filename);
The purpose of implementing these two functions is to facilitate testing. The test program will directly call these functions, so be careful not to use the wrong function definitions. To be able to execute separately, you can also use these functions to implement your own executable program for
opt_sender
andopt_receiver
, which is not complicated.
Please do not directly use commands like
exit
inside the function that causes the process to exit, as this would cause the evaluation program to stop executing as well.
All test filenames will not exceed 100 characters in length and only contain the 26 letters a-z
.
The data size for transmission does not exceed 100MB
, and its character set consists of the union of any characters of the char type.
The window sizes of sender
and receiver
are the same and do not exceed 512
.
All test points have a one-to-one sender
and receiver
.
The test sender
will have the following four types of transmission faults: 1) Loss of DATA
packets, 2) Out-of-order DATA
packets, 3) Duplicate DATA
packets, 4) DATA
packets with checksum errors.
The test receiver
will have the following four types of transmission faults: 1) Loss of ACK
packets, 2) Out-of-order ACK
packets, 3) Duplicate ACK
packets, 4) ACK
packets with checksum errors.
There are 26 test points before the deadline, two of which have no transmission faults and test the implementation of the original and optimized RTP protocol, respectively.
The remaining 24 test points are divided into four groups of six, testing the optimized/non-optimized versions of sender
/receiver
. For each group of faults, the six test points are respectively for individual categories 1-4 transmission faults, a small mix of categories 1-4 transmission faults, and a large mix of categories 1-4 transmission faults. Note that 1) any fault category may appear repeatedly, 2) due to window size settings, these 24 test points are all likely to experience natural packet loss faults, the first two without transmission faults are not affected.
After the deadline, 10 new test points will be added, including larger windows, more extensive data, and higher fault probabilities, but all follow the above descriptions.
Each test point has the same score.
The provided source code mainly contains the following parts:
- The
third_party
folder contains the source code of googletest, which does not require modification. CMakeLists.txt
is a usable CMake template. After implementingsender_def.c
,receiver_def.c
,rtp.c
(where I define some necessary functions), executecmake .
andmake
to compile all the code.rtp.c
is not mandatory; you can selectively use these files based on actual needs, and you can also modifyCMakeList.txt
to meet your specific requirements.- The
src
folder contains header files for each function definition and RTP packet header formats.
After compiling all the code, execute make test CTEST_OUTPUT_ON_FAILURE=TRUE GTEST_COLOR=TRUE
to conduct local testing. Each test point has the same score.
If you want to test a single test point, you can call ./rtp_test_all --gtest_filter=Test point name
, for example, ./rtp_test_all --gtest_filter=RTP.OPT_RECEIVER_TEST_LOST_DATA
.
For details on the specific test files called at each test point, please see the source code of test.cpp.
The image below is an example of a successful test:
To modify test files, go to the https://github.com/N2Sys-EDU/Lab2-RTP-Test repository and download all executable files with the prefix test.
During the actual implementation of this protocol, because it is not a fully reliable connection-oriented protocol, there are many bugs. Hence, we have supplemented the following handling for special cases:
This is also why TCP requires a three-way handshake and a four-way disconnection process. We hope everyone can think this through in the process of completing this lab.
- If the
ACK
packet of theSTART
type is lost and thesender
times out, by which time thereceiver
has already established a connection and is ready to receive data, thesender
should directly send anEND
packet to thereceiver
. - If the
sender
determines that theACK
packet of theSTART
type is corrupted, it should directly send anEND
packet to thereceiver
. - If the
ACK
packet of theEND
type is lost, thesender
will conclude the process upon timeout.
- Set a timeout period (recommended to be 10 seconds). If no packet is received within this time, it is assumed that the connection has been closed.
- If the
checksum
of theSTART
packet is incorrect, thereceiver
process should exit immediately.
In Lab4, you will be required to implement a simple Layer 2 switch that is capable of performing straightforward forwarding, broadcasting, and learning of forwarding tables.
Specifically, you will need to implement your switch class as a C++ class. A single-process simulator will be used for simulation and testing purposes. This simulator will instantiate one or more objects of your switch class, forming a virtual physical link topology with virtual hosts within the simulator. The simulator will invoke interfaces of your implemented switch class to accomplish the forwarding of Ethernet frames, broadcasting, and learning of the switch's forwarding tables.
To simplify the lab, we will only consider the link layer within the network. The following agreements are made:
- Each host in the network has a unique MAC address for identification.
- The experiment will only involve frame transmission at the link layer and will not cover protocols in the network layer and above.
- The experiment will only simulate single-threading and will not involve multi-thread testing.
It's important to note that we will use a simulator to model the network environment, not actual environment testing, which means you do not need to program using sockets.
This section primarily describes the functionality that needs to be implemented in the lab's switch.
In a nutshell, upon receiving a frame from the link layer, the switch must determine the forwarding port based on the destination MAC address in the frame header. Also, the switch should automatically learn the forwarding table by updating the mapping between the frame's incoming port and the source MAC address.
We expect your switch to correctly forward frames and learn the forwarding table.
More specifically, when your switch receives a frame from a port inPort
, it will check the frame's destination MAC address and look for the corresponding outgoing port outPort
in the forwarding table. Your switch should handle the following situations:
(1) If it is a control frame from the controller, the switch should execute the above instructions and then discard it.
(2) If the corresponding forwarding port number cannot be found, broadcast the frame to other ports excluding the incoming port while recording the mapping of the incoming port and the frame's source MAC address.
(3) If the corresponding forwarding port outPort
is found and outPort == inPort
, the frame should be discarded.
(4) If the corresponding forwarding port outPort
is found and outPort != inPort
, forward the frame to the port outPort
.
Your switch should implement an aging feature.
Specifically, your switch should be able to age entries in its forwarding table to adapt to changes in the physical link status. In addition to recording the destination MAC address and corresponding outgoing port, each table entry in the forwarding table should also have a timestamp. Each time the forwarding table is learned, initialize the timestamp if a new entry is inserted, or update the timestamp if an existing entry is refreshed. The switch can set an aging time to delete any entries that have not been refreshed after exceeding this time.
To simulate aging, you should equip each forwarding table entry with an int
type variable counter
and implement aging as follows:
- Upon inserting a new entry, initialize the corresponding
counter
to 10 (corresponding to an aging time of 10 seconds). - If an existing entry is being inserted, reset the
counter
to 10. - When the switch receives an
Aging
control command from the Controller, it should decrement thecounter
by 1 for all entries in the forwarding table, removing any entries from the table whosecounter
has reached 0.
The Controller is responsible for notifying the switch to age the entries in its forwarding table. It achieves this by sending control frames.
For testing purposes, the switch class you implement must be able to handle control frames from the Controller and perform the appropriate actions. Control frames from the Controller will be passed to the switch just like other data frames, through the Simulator invoking class member functions.
We will describe the specifics of the control frames in section 3.3.
In Lab 4
, each frame consists of a header + Payload, with each frame containing only one header.
To differentiate frames coming from hosts and those from the controller, we refer to frames sent from hosts as data frames and frames from the controller containing control commands as control frames. Please note: Both control and data frames adhere to the frame format described in this section.
We use a 6-byte uint8_t
array to represent a MAC address. For readability, we use typedef to define an alias for the MAC address as mac_addr_t
, which you can find in the types.h
file.
// In "types.h"
typedef uint8_t mac_addr_t[6];
The frame header in Lab 4
follows the definition below, which you can also find in the types.h
file.
// In "types.h"
typedef struct {
mac_addr_t ether_dest;
mac_addr_t ether_src;
uint16_t ether_type;
uint16_t length;
} ether_header_t;
Here, the ether_dest
field occupies 6 bytes and represents the destination MAC address of the frame.
The ether_src
field also occupies 6 bytes and represents the source MAC address of the frame.
The ether_type
field occupies 2 bytes and indicates the type of frame. In Lab 4
, there are two different types of frames involved: data frames and control frames, corresponding to the following two possible values for ether_type
:
const uint16_t ETHER_DATA_TYPE = 0;
const uint16_t ETHER_CTRL_TYPE = 1;
For ETHER_DATA_TYPE
, it indicates that this is a data frame, and its payload contains communication data between hosts.
For ETHER_CTRL_TYPE
, such frames are only issued by the Controller, indicating that this is a control frame. We will introduce control frames in section 3.3.
The length
field occupies 2 bytes and indicates the length of the payload.
Note: Both ether_type
and length
are represented using little-endian format.
We guarantee that the size of each message sent during testing will not exceed 1500 bytes, so the total size of an individual frame will not exceed the 16 bytes of the frame header + a maximum of 1500 bytes of the message = 1516 bytes.
A control frame refers to the frame received by a switch from the Controller that contains the Controller's control commands.
A control command refers to the control information contained within a control frame. Your switch only needs to support the single control command AGING
.
Upon receiving this command, the switch should immediately age the entries in its forwarding table.
In terms of format, the AGING
command does not have an additional payload; it carries no information other than the frame header, with the header's length field set to 0.
For ease of testing, you are required to implement your switch program according to the given class interface.
Specifically, the abstract class SwitchBase
for the switch is defined in the provided file switch.h
, as shown in the following code snippet.
class SwitchBase {
public:
virtual void InitSwitch(int numPorts) = 0;
virtual int ProcessFrame(int inPort, char* framePtr) = 0;
}
Your implemented switch class must inherit from the above pure virtual class and implement the two pure virtual functions described above.
The following will explain the parameters and their meanings for these two pure virtual functions.
The InitSwitch
function is used to initialize the state of the switch, with the following parameter significance:
numPorts
indicates the number of ports on this switch, ensuring thatnumPorts
is greater than 0.
Note: As with Lab3, we stipulate that port numbers start from 1. Specifically, each switch's port number 1 is linked to the Controller.
The ProcessFrame
function is responsible for handling and forwarding the frames received by the switch. It contains two parameters:
inPort
indicates the port number where the frame was received.framePtr
is a pointer to the received frame, which conforms to the frame format specified in section 3.1.
The return value of ProcessFrame
indicates the port number to which this frame should be forwarded. In particular:
- A return port number of
-1
indicates that the frame should be discarded. - A return port number of
0
means that the frame should be broadcast to all other ports except the incoming port.
Note:
1. Do not free the framePtr inside the ProcessFrame function.
2. After processing the AGING frame from the controller, the switch should directly discard this frame. That is, for control frames from the controller, your switch's ProcessFrame function should return -1.
Next, we will introduce the workflow of the simulator, which will call your implemented switch according to the interface agreed upon above.
During testing, the simulator will proceed with the following logic:
- The Simulator creates a Controller.
- The Simulator calls the
CreateSwitchObject
function multiple times according to the corresponding physical topology to create multiple switch instances and uses theInitSwitch
function to complete the initialization of each switch. - For each switch, the Simulator will first check if there are any unprocessed frames in its corresponding buffer; if so, it will call the
ProcessFrame
function for processing the frame, which may involve broadcasting, forwarding, or discarding the frame. - Before officially starting the test, the test program will ensure to send messages between pairs of hosts in the network to allow each switch to learn the correct port forwarding (testing will not occur at this stage).
- When officially starting the test, the test program will generate some random Payloads and assemble them into frames for testing. At the same time, the Simulator will track the forwarding path of these frames.
- The test program will check whether these frames have been correctly forwarded.
In steps 1 and 2, the Simulator creates a Controller instance and multiple switch instances to simulate a real-machine environment. To allow the Simulator to create your defined switch class, we declare a helper function in switch.h
as follows:
SwitchBase *CreateSwitchObject();
You should implement this function so that each call creates a new instance of your implemented Switch and returns this pointer. For example, suppose the switch class you implemented is EthernetSwitch
:
class EthernetSwitch : public SwitchBase {
public:
void InitSwitch(int numPorts) override { /* your implementation ... */ }
int ProcessFrame(int inPort, char* framePtr) override { /* your implementation ... */ }
};
Then you should implement the following function:
SwitchBase * CreateSwitchObject() {
return new EthernetSwitch();
}
We use the same method as in lab3
for local testing and running of code.
We provide an automated test program, and each student has a separate Github repository after accepting the Github Classroom invitation.
If you cannot test normally, please contact the teaching assistant.
- Clone from the remote repository.
- Execute
git submodule update --init
in the root directory. - Execute
git submodule update --remote
in the root directory. - Execute
mkdir build
in the root directory.
We have provided a local test program, if you want to test locally, please follow these steps:
- In the root directory, execute
cd test_local && mkdir build && cd build && cmake .. && make
, which will compile the local test program. - Copy the file
test_local/build/lab4_test
tobuild/
, which you only need to do once when first getting the test program or after the test program is updated.
Note, if the test program is updated, please perform the following command to get the latest test program (we will notify through the teaching website and WeChat group):
Execute git submodule update --remote
in the root directory and re-execute steps 1 and 2 above.
We issued a CMake template in the template repository; you can modify CMakeList.txt
to add source files.
Note that there should be a distributed static library liblab4runtime.a
in the test_local
directory. Before compiling, you need to copy this file to the build/
directory in the root directory.
If liblab4runtime.a
is updated, you will need to execute git submodule update --remote
in the root directory and then copy it back to build/
.
We compile switch.cc
into a static library and link liblab4runtime.a
with libswitch.a
to the simulator
to create a complete executable simulator program. You can compile and test your switch locally by executing the cmake .. && make
command in the build
directory at the root.
Please do not modify the simulator.cc
provided in the issued files and ensure that liblab4runtime
is linked before libswitch
during linking. Otherwise, normal testing will not be possible.
For ease of debugging, we provide a debug mode for simulator
.
You can run the debug mode by executing ./simulator 0
. In this mode, you can consider simulator
as providing a simulator that runs the controller
and switch
. You can debug using the commands listed below.
Note that the frames sent in 2-3 below are executed directly by the controller and will not be sent to the switch's buffer; when executing 4/5/6/7, ProcessFrame()
will be called. Meanwhile, 8 will immediately call ProcessFrame()
and recursively track the forwarding path of the frame.
new <num_port>
Create a new switch. Consistent with the parameter ofInitSwitch()
, thesimulator
will return the number of the created switch (positive integer). This command will create a Switch instance and callInitSwitch()
to initialize the switch.link <switch_id> <switch_id>
Connect two switches specified by the switch number returned in 1, where the port number will be selected by the controller.addhost <switch_id> <addr>
Connect a host to the corresponding switch with the MAC address<addr>
.aging
Send anAGING
control frame to all switches.n
Make all switches take a frame from their corresponding input buffer and perform one forwarding operation.ns
Continuously perform step 8 until there are no more frames to forward in the network.warmup
Send random messages between pairs of hosts in the network so that before the official test starts, each switch can learn the correct port forwarding rules.hostsend <src_addr> <dst_addr> <payload>
Send a frame from host<src_addr>
to<dst_addr>
,<payload>
is an optional field. Thesimulator
will return the length of the forwarding path for the corresponding frame in this network, along with src_addr, dst_addr, payload.exit
Exit the program.
For example, the following input sequence can serve as an example:
new 4
addhost 1 de:ad:be:ef:00:01
addhost 1 de:ad:be:ef:00:02
addhost 1 de:ad:be:ef:00:03
warmup
hostsend de:ad:be:ef:00:01 de:ad:be:ef:00:02 HelloFromHost1ToHost2
Lines 1-4 actually establish the following physical link:
Line 1 creates a switch with 4 ports (note: port 1 is reserved for the controller).
Lines 2-4 connect three hosts with MAC addresses de:ad:be:ef:00:01, de:ad:be:ef:00:02, and de:ad:be:ef:00:03, respectively, to the remaining three ports of this switch.
Line 5 uses the warmup
command to send messages between Hosts 1, 2, and 3, to preheat the forwarding table of Switch 1 so that it can learn the correct forwarding ports for each host.
Line 6 officially starts the message sending.
This lab has a total of 110 points, with some test points being released before the deadline, and all test points being tested uniformly after the deadline.
Each test point name consists of ${Category}.${Test Point Name}
All the test case links' topologies are guaranteed to be connected and will only have tree-like topologies (that is, there will be no cycles).
In all test cases, it is guaranteed that the number of switches does not exceed 20 and the number of hosts does not exceed 100.
In all test cases, it is guaranteed that there will be no illegal control frames.
All test cases will ensure that the MAC address used by the controller will not conflict with other hosts' MAC addresses.
Category | Test Point Name | Score | Released Before Deadline | Contents of Data Point |
---|---|---|---|---|
Topology1 | Forwarding | 10 | Yes | Only tests forwarding |
Topology1 | SwitchAging | 10 | Yes | Only tests aging on the premise of correct forwarding |
Topology2 | Forwarding | 10 | Yes | Only tests forwarding |
Topology2 | SwitchAging | 10 | Yes | Only tests aging on the premise of correct forwarding |
Topology3 | Forwarding | 10 | Yes | Only tests forwarding |
Topology3 | SwitchAging | 15 | Yes | Only tests aging on the premise of correct forwarding |
Topology4 | Forwarding | 10 | Yes | Only tests forwarding |
Topology4 | Mixed | 15 | Yes | Tests forwarding and switch aging simultaneously |
Topology5 | Mixed | 10 | No | Tests forwarding and switch aging simultaneously |
Topology6 | Mixed | 10 | No | Tests forwarding and switch aging simultaneously |