Merge remote-tracking branch 'official/lab4-startercode'

This commit is contained in:
ridethepig 2023-02-17 06:41:11 +00:00
commit 3af945edc7
32 changed files with 2655 additions and 7 deletions

View File

@ -1 +1,9 @@
add_library (stream_copy STATIC bidirectional_stream_copy.cc)
add_sponge_exec (udp_tcpdump ${LIBPCAP})
add_sponge_exec (tcp_native stream_copy)
add_sponge_exec (tun)
add_sponge_exec (tcp_udp stream_copy)
add_sponge_exec (tcp_ipv4 stream_copy)
add_sponge_exec (webget)
add_sponge_exec (tcp_benchmark)

View File

@ -0,0 +1,91 @@
#include "bidirectional_stream_copy.hh"
#include "byte_stream.hh"
#include "eventloop.hh"
#include <algorithm>
#include <iostream>
#include <unistd.h>
using namespace std;
void bidirectional_stream_copy(Socket &socket) {
constexpr size_t max_copy_length = 65536;
constexpr size_t buffer_size = 1048576;
EventLoop _eventloop{};
FileDescriptor _input{STDIN_FILENO};
FileDescriptor _output{STDOUT_FILENO};
ByteStream _outbound{buffer_size};
ByteStream _inbound{buffer_size};
bool _outbound_shutdown{false};
bool _inbound_shutdown{false};
socket.set_blocking(false);
_input.set_blocking(false);
_output.set_blocking(false);
// rule 1: read from stdin into outbound byte stream
_eventloop.add_rule(
_input,
Direction::In,
[&] {
_outbound.write(_input.read(_outbound.remaining_capacity()));
if (_input.eof()) {
_outbound.end_input();
}
},
[&] { return (not _outbound.error()) and (_outbound.remaining_capacity() > 0) and (not _inbound.error()); },
[&] { _outbound.end_input(); });
// rule 2: read from outbound byte stream into socket
_eventloop.add_rule(socket,
Direction::Out,
[&] {
const size_t bytes_to_write = min(max_copy_length, _outbound.buffer_size());
const size_t bytes_written = socket.write(_outbound.peek_output(bytes_to_write), false);
_outbound.pop_output(bytes_written);
if (_outbound.eof()) {
socket.shutdown(SHUT_WR);
_outbound_shutdown = true;
}
},
[&] { return (not _outbound.buffer_empty()) or (_outbound.eof() and not _outbound_shutdown); },
[&] { _outbound.end_input(); });
// rule 3: read from socket into inbound byte stream
_eventloop.add_rule(
socket,
Direction::In,
[&] {
_inbound.write(socket.read(_inbound.remaining_capacity()));
if (socket.eof()) {
_inbound.end_input();
}
},
[&] { return (not _inbound.error()) and (_inbound.remaining_capacity() > 0) and (not _outbound.error()); },
[&] { _inbound.end_input(); });
// rule 4: read from inbound byte stream into stdout
_eventloop.add_rule(_output,
Direction::Out,
[&] {
const size_t bytes_to_write = min(max_copy_length, _inbound.buffer_size());
const size_t bytes_written = _output.write(_inbound.peek_output(bytes_to_write), false);
_inbound.pop_output(bytes_written);
if (_inbound.eof()) {
_output.close();
_inbound_shutdown = true;
}
},
[&] { return (not _inbound.buffer_empty()) or (_inbound.eof() and not _inbound_shutdown); },
[&] { _inbound.end_input(); });
// loop until completion
while (true) {
if (EventLoop::Result::Exit == _eventloop.wait_next_event(-1)) {
return;
}
}
}

View File

@ -0,0 +1,9 @@
#ifndef SPONGE_APPS_BIDIRECTIONAL_STREAM_COPY_HH
#define SPONGE_APPS_BIDIRECTIONAL_STREAM_COPY_HH
#include "socket.hh"
//! Copy socket input/output to stdin/stdout until finished
void bidirectional_stream_copy(Socket &socket);
#endif // SPONGE_APPS_BIDIRECTIONAL_STREAM_COPY_HH

116
apps/tcp_benchmark.cc Normal file
View File

@ -0,0 +1,116 @@
#include "tcp_connection.hh"
#include <chrono>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <string>
using namespace std;
using namespace std::chrono;
constexpr size_t len = 100 * 1024 * 1024;
void move_segments(TCPConnection &x, TCPConnection &y, vector<TCPSegment> &segments, const bool reorder) {
while (not x.segments_out().empty()) {
segments.emplace_back(move(x.segments_out().front()));
x.segments_out().pop();
}
if (reorder) {
for (auto it = segments.rbegin(); it != segments.rend(); ++it) {
y.segment_received(move(*it));
}
} else {
for (auto it = segments.begin(); it != segments.end(); ++it) {
y.segment_received(move(*it));
}
}
segments.clear();
}
void main_loop(const bool reorder) {
TCPConfig config;
TCPConnection x{config}, y{config};
string string_to_send(len, 'x');
for (auto &ch : string_to_send) {
ch = rand();
}
Buffer bytes_to_send{string(string_to_send)};
x.connect();
y.end_input_stream();
bool x_closed = false;
string string_received;
string_received.reserve(len);
const auto first_time = high_resolution_clock::now();
auto loop = [&] {
// write input into x
while (bytes_to_send.size() and x.remaining_outbound_capacity()) {
const auto want = min(x.remaining_outbound_capacity(), bytes_to_send.size());
const auto written = x.write(string(bytes_to_send.str().substr(0, want)));
if (want != written) {
throw runtime_error("want = " + to_string(want) + ", written = " + to_string(written));
}
bytes_to_send.remove_prefix(written);
}
if (bytes_to_send.size() == 0 and not x_closed) {
x.end_input_stream();
x_closed = true;
}
// exchange segments between x and y but in reverse order
vector<TCPSegment> segments;
move_segments(x, y, segments, reorder);
move_segments(y, x, segments, false);
// read output from y
const auto available_output = y.inbound_stream().buffer_size();
if (available_output > 0) {
string_received.append(y.inbound_stream().read(available_output));
}
// time passes
x.tick(1000);
y.tick(1000);
};
while (not y.inbound_stream().eof()) {
loop();
}
if (string_received != string_to_send) {
throw runtime_error("strings sent vs. received don't match");
}
const auto final_time = high_resolution_clock::now();
const auto duration = duration_cast<nanoseconds>(final_time - first_time).count();
const auto gigabits_per_second = len * 8.0 / double(duration);
cout << fixed << setprecision(2);
cout << "CPU-limited throughput" << (reorder ? " with reordering: " : " : ") << gigabits_per_second
<< " Gbit/s\n";
while (x.active() or y.active()) {
loop();
}
}
int main() {
try {
main_loop(false);
main_loop(true);
} catch (const exception &e) {
cerr << e.what() << "\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

162
apps/tcp_ipv4.cc Normal file
View File

@ -0,0 +1,162 @@
#include "bidirectional_stream_copy.hh"
#include "tcp_config.hh"
#include "tcp_sponge_socket.hh"
#include "tun.hh"
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <random>
#include <string>
#include <tuple>
using namespace std;
constexpr const char *TUN_DFLT = "tun144";
const string LOCAL_ADDRESS_DFLT = "169.254.144.9";
static void show_usage(const char *argv0, const char *msg) {
cout << "Usage: " << argv0 << " [options] <host> <port>\n\n"
<< " Option Default\n"
<< " -- --\n\n"
<< " -l Server (listen) mode. (client mode)\n"
<< " In server mode, <host>:<port> is the address to bind.\n\n"
<< " -a <addr> Set source address (client mode only) " << LOCAL_ADDRESS_DFLT << "\n"
<< " -s <port> Set source port (client mode only) (random)\n\n"
<< " -w <winsz> Use a window of <winsz> bytes " << TCPConfig::MAX_PAYLOAD_SIZE
<< "\n\n"
<< " -t <tmout> Set rt_timeout to tmout " << TCPConfig::TIMEOUT_DFLT << "\n\n"
<< " -d <tundev> Connect to tun <tundev> " << TUN_DFLT << "\n\n"
<< " -Lu <loss> Set uplink loss to <rate> (float in 0..1) (no loss)\n"
<< " -Ld <loss> Set downlink loss to <rate> (float in 0..1) (no loss)\n\n"
<< " -h Show this message.\n\n";
if (msg != nullptr) {
cout << msg;
}
cout << endl;
}
static void check_argc(int argc, char **argv, int curr, const char *err) {
if (curr + 3 >= argc) {
show_usage(argv[0], err);
exit(1);
}
}
static tuple<TCPConfig, FdAdapterConfig, bool, char *> get_config(int argc, char **argv) {
TCPConfig c_fsm{};
FdAdapterConfig c_filt{};
char *tundev = nullptr;
int curr = 1;
bool listen = false;
string source_address = LOCAL_ADDRESS_DFLT;
string source_port = to_string(uint16_t(random_device()()));
while (argc - curr > 2) {
if (strncmp("-l", argv[curr], 3) == 0) {
listen = true;
curr += 1;
} else if (strncmp("-a", argv[curr], 3) == 0) {
check_argc(argc, argv, curr, "ERROR: -a requires one argument.");
source_address = argv[curr + 1];
curr += 2;
} else if (strncmp("-s", argv[curr], 3) == 0) {
check_argc(argc, argv, curr, "ERROR: -s requires one argument.");
source_port = argv[curr + 1];
curr += 2;
} else if (strncmp("-w", argv[curr], 3) == 0) {
check_argc(argc, argv, curr, "ERROR: -w requires one argument.");
c_fsm.recv_capacity = strtol(argv[curr + 1], nullptr, 0);
curr += 2;
} else if (strncmp("-t", argv[curr], 3) == 0) {
check_argc(argc, argv, curr, "ERROR: -t requires one argument.");
c_fsm.rt_timeout = strtol(argv[curr + 1], nullptr, 0);
curr += 2;
} else if (strncmp("-d", argv[curr], 3) == 0) {
check_argc(argc, argv, curr, "ERROR: -t requires one argument.");
tundev = argv[curr + 1];
curr += 2;
} else if (strncmp("-Lu", argv[curr], 3) == 0) {
check_argc(argc, argv, curr, "ERROR: -Lu requires one argument.");
float lossrate = strtof(argv[curr + 1], nullptr);
using LossRateUpT = decltype(c_filt.loss_rate_up);
c_filt.loss_rate_up =
static_cast<LossRateUpT>(static_cast<float>(numeric_limits<LossRateUpT>::max()) * lossrate);
curr += 2;
} else if (strncmp("-Ld", argv[curr], 3) == 0) {
check_argc(argc, argv, curr, "ERROR: -Lu requires one argument.");
float lossrate = strtof(argv[curr + 1], nullptr);
using LossRateDnT = decltype(c_filt.loss_rate_dn);
c_filt.loss_rate_dn =
static_cast<LossRateDnT>(static_cast<float>(numeric_limits<LossRateDnT>::max()) * lossrate);
curr += 2;
} else if (strncmp("-h", argv[curr], 3) == 0) {
show_usage(argv[0], nullptr);
exit(0);
} else {
show_usage(argv[0], string("ERROR: unrecognized option " + string(argv[curr])).c_str());
exit(1);
}
}
// parse positional command-line arguments
if (listen) {
c_filt.source = {"0", argv[curr + 1]};
if (c_filt.source.port() == 0) {
show_usage(argv[0], "ERROR: listen port cannot be zero in server mode.");
exit(1);
}
} else {
c_filt.destination = {argv[curr], argv[curr + 1]};
c_filt.source = {source_address, source_port};
}
return make_tuple(c_fsm, c_filt, listen, tundev);
}
int main(int argc, char **argv) {
try {
if (argc < 3) {
show_usage(argv[0], "ERROR: required arguments are missing.");
return EXIT_FAILURE;
}
auto [c_fsm, c_filt, listen, tun_dev_name] = get_config(argc, argv);
LossyTCPOverIPv4SpongeSocket tcp_socket(LossyTCPOverIPv4OverTunFdAdapter(
TCPOverIPv4OverTunFdAdapter(TunFD(tun_dev_name == nullptr ? TUN_DFLT : tun_dev_name))));
if (listen) {
tcp_socket.listen_and_accept(c_fsm, c_filt);
} else {
tcp_socket.connect(c_fsm, c_filt);
}
bidirectional_stream_copy(tcp_socket);
tcp_socket.wait_until_closed();
} catch (const exception &e) {
cerr << "Exception: " << e.what() << endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

43
apps/tcp_native.cc Normal file
View File

@ -0,0 +1,43 @@
#include "bidirectional_stream_copy.hh"
#include <cstdlib>
#include <cstring>
#include <iostream>
using namespace std;
void show_usage(const char *argv0) {
cerr << "Usage: " << argv0 << " [-l] <host> <port>\n\n"
<< " -l specifies listen mode; <host>:<port> is the listening address." << endl;
}
int main(int argc, char **argv) {
try {
bool server_mode = false;
if (argc < 3 || ((server_mode = (strncmp("-l", argv[1], 3) == 0)) && argc < 4)) {
show_usage(argv[0]);
return EXIT_FAILURE;
}
// in client mode, connect; in server mode, accept exactly one connection
auto socket = [&] {
if (server_mode) {
TCPSocket listening_socket; // create a TCP socket
listening_socket.set_reuseaddr(); // reuse the server's address as soon as the program quits
listening_socket.bind({argv[2], argv[3]}); // bind to specified address
listening_socket.listen(); // mark the socket as listening for incoming connections
return listening_socket.accept(); // accept exactly one connection
}
TCPSocket connecting_socket;
connecting_socket.connect({argv[1], argv[2]});
return connecting_socket;
}();
bidirectional_stream_copy(socket);
} catch (const exception &e) {
cerr << "Exception: " << e.what() << endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

136
apps/tcp_udp.cc Normal file
View File

@ -0,0 +1,136 @@
#include "bidirectional_stream_copy.hh"
#include "tcp_config.hh"
#include "tcp_sponge_socket.hh"
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <limits>
#include <random>
#include <string>
#include <tuple>
using namespace std;
constexpr uint16_t DPORT_DFLT = 1440;
static void show_usage(const char *argv0, const char *msg) {
cout << "Usage: " << argv0 << " [options] <host> <port>\n\n"
<< " Option Default\n"
<< " -- --\n\n"
<< " -l Server (listen) mode. (client mode)\n"
<< " In server mode, <host>:<port> is the address to bind.\n\n"
<< " -w <winsz> Use a window of <winsz> bytes " << TCPConfig::MAX_PAYLOAD_SIZE
<< "\n\n"
<< " -t <tmout> Set rt_timeout to tmout " << TCPConfig::TIMEOUT_DFLT << "\n\n"
<< " -Lu <loss> Set uplink loss to <rate> (float in 0..1) (no loss)\n"
<< " -Ld <loss> Set downlink loss to <rate> (float in 0..1) (no loss)\n\n"
<< " -h Show this message and quit.\n\n";
if (msg != nullptr) {
cout << msg;
}
cout << endl;
}
static void check_argc(int argc, char **argv, int curr, const char *err) {
if (curr + 3 >= argc) {
show_usage(argv[0], err);
exit(1);
}
}
static tuple<TCPConfig, FdAdapterConfig, bool> get_config(int argc, char **argv) {
TCPConfig c_fsm{};
FdAdapterConfig c_filt{};
int curr = 1;
bool listen = false;
while (argc - curr > 2) {
if (strncmp("-l", argv[curr], 3) == 0) {
listen = true;
curr += 1;
} else if (strncmp("-w", argv[curr], 3) == 0) {
check_argc(argc, argv, curr, "ERROR: -w requires one argument.");
c_fsm.recv_capacity = strtol(argv[curr + 1], nullptr, 0);
curr += 2;
} else if (strncmp("-t", argv[curr], 3) == 0) {
check_argc(argc, argv, curr, "ERROR: -t requires one argument.");
c_fsm.rt_timeout = strtol(argv[curr + 1], nullptr, 0);
curr += 2;
} else if (strncmp("-Lu", argv[curr], 3) == 0) {
check_argc(argc, argv, curr, "ERROR: -Lu requires one argument.");
float lossrate = strtof(argv[curr + 1], nullptr);
using LossRateUpT = decltype(c_filt.loss_rate_up);
c_filt.loss_rate_up =
static_cast<LossRateUpT>(static_cast<float>(numeric_limits<LossRateUpT>::max()) * lossrate);
curr += 2;
} else if (strncmp("-Ld", argv[curr], 3) == 0) {
check_argc(argc, argv, curr, "ERROR: -Lu requires one argument.");
float lossrate = strtof(argv[curr + 1], nullptr);
using LossRateDnT = decltype(c_filt.loss_rate_dn);
c_filt.loss_rate_dn =
static_cast<LossRateDnT>(static_cast<float>(numeric_limits<LossRateDnT>::max()) * lossrate);
curr += 2;
} else if (strncmp("-h", argv[curr], 3) == 0) {
show_usage(argv[0], nullptr);
exit(0);
} else {
show_usage(argv[0], std::string("ERROR: unrecognized option " + std::string(argv[curr])).c_str());
exit(1);
}
}
if (listen) {
c_filt.source = {"0", argv[argc - 1]};
} else {
c_filt.destination = {argv[argc - 2], argv[argc - 1]};
}
return make_tuple(c_fsm, c_filt, listen);
}
int main(int argc, char **argv) {
try {
if (argc < 3) {
show_usage(argv[0], "ERROR: required arguments are missing.");
exit(1);
}
// handle configuration and UDP setup from cmdline arguments
auto [c_fsm, c_filt, listen] = get_config(argc, argv);
// build a TCP FSM on top of the UDP socket
UDPSocket udp_sock;
if (listen) {
udp_sock.bind(c_filt.source);
}
LossyTCPOverUDPSpongeSocket tcp_socket(LossyTCPOverUDPSocketAdapter(TCPOverUDPSocketAdapter(move(udp_sock))));
if (listen) {
tcp_socket.listen_and_accept(c_fsm, c_filt);
} else {
tcp_socket.connect(c_fsm, c_filt);
}
bidirectional_stream_copy(tcp_socket);
tcp_socket.wait_until_closed();
} catch (const exception &e) {
cerr << "Exception: " << e.what() << endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

57
apps/tun.cc Normal file
View File

@ -0,0 +1,57 @@
#include "tun.hh"
#include "ipv4_datagram.hh"
#include "parser.hh"
#include "tcp_segment.hh"
#include "util.hh"
#include <cstdlib>
#include <exception>
#include <iostream>
#include <vector>
using namespace std;
int main() {
try {
TunFD tun("tun144");
while (true) {
auto buffer = tun.read();
cout << "\n\n***\n*** Got packet:\n***\n";
hexdump(buffer.data(), buffer.size());
IPv4Datagram ip_dgram;
cout << "attempting to parse as ipv4 datagram... ";
if (ip_dgram.parse(move(buffer)) != ParseResult::NoError) {
cout << "failed.\n";
continue;
}
cout << "success! totlen=" << ip_dgram.header().len << ", IPv4 header contents:\n";
cout << ip_dgram.header().to_string();
if (ip_dgram.header().proto != IPv4Header::PROTO_TCP) {
cout << "\nNot TCP, skipping.\n";
continue;
}
cout << "\nAttempting to parse as a TCP segment... ";
TCPSegment tcp_seg;
if (tcp_seg.parse(ip_dgram.payload(), ip_dgram.header().pseudo_cksum()) != ParseResult::NoError) {
cout << "failed.\n";
continue;
}
cout << "success! payload len=" << tcp_seg.payload().size() << ", TCP header contents:\n";
cout << tcp_seg.header().to_string() << endl;
}
} catch (const exception &e) {
cout << "Exception: " << e.what() << endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

293
apps/udp_tcpdump.cc Normal file
View File

@ -0,0 +1,293 @@
#include "parser.hh"
#include "tcp_header.hh"
#include "tcp_segment.hh"
#include "util.hh"
#include <arpa/inet.h>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <pcap/pcap.h>
#include <sstream>
#include <string>
#include <sys/socket.h>
#include <unistd.h>
#include <vector>
using namespace std;
static void show_usage(const char *arg0, const char *errmsg) {
cout << "Usage: " << arg0 << " [-i <intf>] [-F <file>] [-h|--help] <expression>\n\n"
<< " -i <intf> only capture packets from <intf> (default: all)\n\n"
<< " -F <file> reads in a filter expression from <file>\n"
<< " <expression> is ignored if -F is supplied.\n\n"
<< " -h, --help show this message\n\n"
<< " <expression> a filter expression in pcap-filter(7) syntax\n";
if (errmsg != nullptr) {
cout << '\n' << errmsg;
}
cout << endl;
}
static void check_arg(char *arg0, int argc, int curr, const char *errmsg) {
if (curr + 1 >= argc) {
show_usage(arg0, errmsg);
exit(1);
}
}
static int parse_arguments(int argc, char **argv, char **dev_ptr) {
int curr = 1;
while (curr < argc) {
if (strncmp("-i", argv[curr], 3) == 0) {
check_arg(argv[0], argc, curr, "ERROR: -i requires an argument");
*dev_ptr = argv[curr + 1];
curr += 2;
} else if ((strncmp("-h", argv[curr], 3) == 0) || (strncmp("--help", argv[curr], 7) == 0)) {
show_usage(argv[0], nullptr);
exit(0);
} else {
break;
}
}
return curr;
}
static string inet4_addr(const uint8_t *data) {
char addrbuf[128];
auto *addr = reinterpret_cast<const in_addr *>(data);
if (inet_ntop(AF_INET, addr, static_cast<char *>(addrbuf), 128) == nullptr) {
return "unknown";
}
return string(static_cast<char *>(addrbuf));
}
static string inet6_addr(const uint8_t *data) {
char addrbuf[128];
auto *addr = reinterpret_cast<const in6_addr *>(data);
if (inet_ntop(AF_INET6, addr, static_cast<char *>(addrbuf), 128) == nullptr) {
return "unknown";
}
return string(static_cast<char *>(addrbuf));
}
static int process_ipv4_ipv6(int len, const uint8_t *data, string &src_addr, string &dst_addr) {
// this is either an IPv4 or IPv6 packet, we hope
if (len < 1) {
return -1;
}
int data_offset = 0;
const uint8_t pt = data[0] & 0xf0;
if (pt == 0x40) {
// check packet length and proto
data_offset = (data[0] & 0x0f) * 4;
if (len < data_offset) {
return -1;
}
if (data[9] != 0x11) {
cerr << "Not UDP; ";
return -1;
}
src_addr = inet4_addr(data + 12);
dst_addr = inet4_addr(data + 16);
} else if (pt == 0x60) {
// check packet length
if (len < 42) {
return -1;
}
data_offset = 40;
uint8_t nxt = data[6];
while (nxt != 0x11) {
if (nxt != 0 && nxt != 43 && nxt != 60) {
cerr << "Not UDP or fragmented; ";
return -1;
}
nxt = data[data_offset];
data_offset += 8 * (1 + data[data_offset + 1]);
if (len < data_offset + 2) {
return -1;
}
}
src_addr = inet6_addr(data + 8);
dst_addr = inet6_addr(data + 24);
} else {
return -1;
}
return data_offset + 8; // skip UDP header
}
int main(int argc, char **argv) {
char *dev = nullptr;
const int exp_start = parse_arguments(argc, argv, &dev);
// create pcap handle
if (dev != nullptr) {
cout << "Capturing on interface " << dev;
} else {
cout << "Capturing on all interfaces";
}
pcap_t *p_hdl = nullptr;
const int dl_type = [&] {
char errbuf[PCAP_ERRBUF_SIZE] = {
0,
};
p_hdl = pcap_open_live(dev, 65535, 0, 100, static_cast<char *>(errbuf));
if (p_hdl == nullptr) {
cout << "\nError initiating capture: " << static_cast<char *>(errbuf) << endl;
exit(1);
}
int dlt = pcap_datalink(p_hdl);
// need to handle: DLT_RAW, DLT_NULL, DLT_EN10MB, DLT_LINUX_SLL
if (dlt != DLT_RAW && dlt != DLT_NULL && dlt != DLT_EN10MB && dlt != DLT_LINUX_SLL
#ifdef DLT_LINUX_SLL2
&& dlt != DLT_LINUX_SLL2
#endif
) {
cout << "\nError: unsupported datalink type " << pcap_datalink_val_to_description(dlt) << endl;
exit(1);
}
cout << " (type: " << pcap_datalink_val_to_description(dlt) << ")\n";
return dlt;
}();
// compile and set filter
{
struct bpf_program p_flt {};
stringstream f_stream;
for (int i = exp_start; i < argc; ++i) {
f_stream << argv[i] << ' ';
}
string filter_expression = f_stream.str();
cout << "Using filter expression: " << filter_expression << "\n";
if (pcap_compile(p_hdl, &p_flt, filter_expression.c_str(), 1, PCAP_NETMASK_UNKNOWN) != 0) {
cout << "Error compiling filter expression: " << pcap_geterr(p_hdl) << endl;
return EXIT_FAILURE;
}
if (pcap_setfilter(p_hdl, &p_flt) != 0) {
cout << "Error configuring packet filter: " << pcap_geterr(p_hdl) << endl;
return EXIT_FAILURE;
}
pcap_freecode(&p_flt);
}
int next_ret = 0;
struct pcap_pkthdr *pkt_hdr = nullptr;
const uint8_t *pkt_data = nullptr;
cout << setfill('0');
while ((next_ret = pcap_next_ex(p_hdl, &pkt_hdr, &pkt_data)) >= 0) {
if (next_ret == 0) {
// timeout; just listen again
continue;
}
size_t hdr_off = 0;
int start_off = 0;
// figure out where in the datagram to look based on link type
if (dl_type == DLT_NULL) {
hdr_off = 4;
if (pkt_hdr->caplen < hdr_off) {
cerr << "[INFO] Skipping malformed packet.\n";
continue;
}
const uint8_t pt = pkt_data[3];
if (pt != 2 && pt != 24 && pt != 28 && pt != 30) {
cerr << "[INFO] Skipping non-IP packet.\n";
continue;
}
} else if (dl_type == DLT_EN10MB) {
hdr_off = 14;
if (pkt_hdr->caplen < hdr_off) {
cerr << "[INFO] Skipping malformed packet.\n";
continue;
}
const uint16_t pt = (pkt_data[12] << 8) | pkt_data[13];
if (pt != 0x0800 && pt != 0x86dd) {
cerr << "[INFO] Skipping non-IP packet.\n";
continue;
}
} else if (dl_type == DLT_LINUX_SLL) {
hdr_off = 16;
if (pkt_hdr->caplen < hdr_off) {
cerr << "[INFO] Skipping malformed packet.\n";
continue;
}
const uint16_t pt = (pkt_data[14] << 8) | pkt_data[15];
if (pt != 0x0800 && pt != 0x86dd) {
cerr << "[INFO] Skipping non-IP packet.\n";
continue;
}
#ifdef DLT_LINUX_SLL2
} else if (dl_type == DLT_LINUX_SLL2) {
if (pkt_hdr->caplen < 20) {
cerr << "[INFO] Skipping malformed packet.\n";
continue;
}
const uint16_t pt = (pkt_data[0] << 8) | pkt_data[1];
hdr_off = 20;
if (pt != 0x0800 && pt != 0x86dd) {
cerr << "[INFO] Skipping non-IP packet.\n";
continue;
}
#endif
} else if (dl_type != DLT_RAW) {
cerr << "Mysterious datalink type. Giving up.";
return EXIT_FAILURE;
}
// now actually parse the packet
string src{}, dst{};
if ((start_off = process_ipv4_ipv6(pkt_hdr->caplen - hdr_off, pkt_data + hdr_off, src, dst)) < 0) {
cerr << "Error parsing IPv4/IPv6 packet. Skipping.\n";
continue;
}
// hdr_off + start_off is now the start of the UDP payload
const size_t payload_off = hdr_off + start_off;
const size_t payload_len = pkt_hdr->caplen - payload_off;
string_view payload{reinterpret_cast<const char *>(pkt_data) + payload_off, payload_len};
// try to parse UDP payload as TCP packet
auto seg = TCPSegment{};
if (const auto res = seg.parse(string(payload), 0); res > ParseResult::BadChecksum) {
cout << "(did not recognize TCP header) src: " << src << " dst: " << dst << '\n';
} else {
const TCPHeader &tcp_hdr = seg.header();
uint32_t seqlen = seg.length_in_sequence_space();
cout << src << ':' << tcp_hdr.sport << " > " << dst << ':' << tcp_hdr.dport << "\n Flags ["
<< (tcp_hdr.urg ? "U" : "") << (tcp_hdr.psh ? "P" : "") << (tcp_hdr.rst ? "R" : "")
<< (tcp_hdr.syn ? "S" : "") << (tcp_hdr.fin ? "F" : "") << (tcp_hdr.ack ? "." : "")
<< "] cksum 0x" << hex << setw(4) << tcp_hdr.cksum << dec
<< (res == ParseResult::NoError ? " (correct)" : " (incorrect!)")
<< " seq " << tcp_hdr.seqno;
if (seqlen > 0) {
cout << ':' << (tcp_hdr.seqno + seqlen);
}
cout << " ack " << tcp_hdr.ackno << " win " << tcp_hdr.win << " length " << payload_len << endl;
}
hexdump(payload.data(), payload.size(), 8);
}
pcap_close(p_hdl);
if (next_ret == -1) {
cout << "Error listening for packet: " << pcap_geterr(p_hdl) << endl;
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

View File

@ -0,0 +1,49 @@
#include "tcp_connection.hh"
#include <iostream>
// Dummy implementation of a TCP connection
// For Lab 4, please replace with a real implementation that passes the
// automated checks run by `make check`.
template <typename... Targs>
void DUMMY_CODE(Targs &&... /* unused */) {}
using namespace std;
size_t TCPConnection::remaining_outbound_capacity() const { return {}; }
size_t TCPConnection::bytes_in_flight() const { return {}; }
size_t TCPConnection::unassembled_bytes() const { return {}; }
size_t TCPConnection::time_since_last_segment_received() const { return {}; }
void TCPConnection::segment_received(const TCPSegment &seg) { DUMMY_CODE(seg); }
bool TCPConnection::active() const { return {}; }
size_t TCPConnection::write(const string &data) {
DUMMY_CODE(data);
return {};
}
//! \param[in] ms_since_last_tick number of milliseconds since the last call to this method
void TCPConnection::tick(const size_t ms_since_last_tick) { DUMMY_CODE(ms_since_last_tick); }
void TCPConnection::end_input_stream() {}
void TCPConnection::connect() {}
TCPConnection::~TCPConnection() {
try {
if (active()) {
cerr << "Warning: Unclean shutdown of TCPConnection\n";
// Your code here: need to send a RST segment to the peer
}
} catch (const exception &e) {
std::cerr << "Exception destructing TCP FSM: " << e.what() << std::endl;
}
}

View File

@ -0,0 +1,99 @@
#ifndef SPONGE_LIBSPONGE_TCP_FACTORED_HH
#define SPONGE_LIBSPONGE_TCP_FACTORED_HH
#include "tcp_config.hh"
#include "tcp_receiver.hh"
#include "tcp_sender.hh"
#include "tcp_state.hh"
//! \brief A complete endpoint of a TCP connection
class TCPConnection {
private:
TCPConfig _cfg;
TCPReceiver _receiver{_cfg.recv_capacity};
TCPSender _sender{_cfg.send_capacity, _cfg.rt_timeout, _cfg.fixed_isn};
//! outbound queue of segments that the TCPConnection wants sent
std::queue<TCPSegment> _segments_out{};
//! Should the TCPConnection stay active (and keep ACKing)
//! for 10 * _cfg.rt_timeout milliseconds after both streams have ended,
//! in case the remote TCPConnection doesn't know we've received its whole stream?
bool _linger_after_streams_finish{true};
public:
//! \name "Input" interface for the writer
//!@{
//! \brief Initiate a connection by sending a SYN segment
void connect();
//! \brief Write data to the outbound byte stream, and send it over TCP if possible
//! \returns the number of bytes from `data` that were actually written.
size_t write(const std::string &data);
//! \returns the number of `bytes` that can be written right now.
size_t remaining_outbound_capacity() const;
//! \brief Shut down the outbound byte stream (still allows reading incoming data)
void end_input_stream();
//!@}
//! \name "Output" interface for the reader
//!@{
//! \brief The inbound byte stream received from the peer
ByteStream &inbound_stream() { return _receiver.stream_out(); }
//!@}
//! \name Accessors used for testing
//!@{
//! \brief number of bytes sent and not yet acknowledged, counting SYN/FIN each as one byte
size_t bytes_in_flight() const;
//! \brief number of bytes not yet reassembled
size_t unassembled_bytes() const;
//! \brief Number of milliseconds since the last segment was received
size_t time_since_last_segment_received() const;
//!< \brief summarize the state of the sender, receiver, and the connection
TCPState state() const { return {_sender, _receiver, active(), _linger_after_streams_finish}; };
//!@}
//! \name Methods for the owner or operating system to call
//!@{
//! Called when a new segment has been received from the network
void segment_received(const TCPSegment &seg);
//! Called periodically when time elapses
void tick(const size_t ms_since_last_tick);
//! \brief TCPSegments that the TCPConnection has enqueued for transmission.
//! \note The owner or operating system will dequeue these and
//! put each one into the payload of a lower-layer datagram (usually Internet datagrams (IP),
//! but could also be user datagrams (UDP) or any other kind).
std::queue<TCPSegment> &segments_out() { return _segments_out; }
//! \brief Is the connection still alive in any way?
//! \returns `true` if either stream is still running or if the TCPConnection is lingering
//! after both streams have finished (e.g. to ACK retransmissions from the peer)
bool active() const;
//!@}
//! Construct a new connection from a configuration
explicit TCPConnection(const TCPConfig &cfg) : _cfg{cfg} {}
//! \name construction and destruction
//! moving is allowed; copying is disallowed; default construction not possible
//!@{
~TCPConnection(); //!< destructor sends a RST if the connection is still open
TCPConnection() = delete;
TCPConnection(TCPConnection &&other) = default;
TCPConnection &operator=(TCPConnection &&other) = default;
TCPConnection(const TCPConnection &other) = delete;
TCPConnection &operator=(const TCPConnection &other) = delete;
//!@}
};
#endif // SPONGE_LIBSPONGE_TCP_FACTORED_HH

View File

@ -0,0 +1,57 @@
#include "fd_adapter.hh"
#include <iostream>
#include <stdexcept>
#include <utility>
using namespace std;
//! \details This function first attempts to parse a TCP segment from the next UDP
//! payload recv()d from the socket.
//!
//! If this succeeds, it then checks that the received segment is related to the
//! current connection. When a TCP connection has been established, this means
//! checking that the source and destination ports in the TCP header are correct.
//!
//! If the TCP FSM is listening (i.e., TCPOverUDPSocketAdapter::_listen is `true`)
//! and the TCP segment read from the wire includes a SYN, this function clears the
//! `_listen` flag and calls calls connect() on the underlying UDP socket, with
//! the result that future outgoing segments go to the sender of the SYN segment.
//! \returns a std::optional<TCPSegment> that is empty if the segment was invalid or unrelated
optional<TCPSegment> TCPOverUDPSocketAdapter::read() {
auto datagram = _sock.recv();
// is it for us?
if (not listening() and (datagram.source_address != config().destination)) {
return {};
}
// is the payload a valid TCP segment?
TCPSegment seg;
if (ParseResult::NoError != seg.parse(move(datagram.payload), 0)) {
return {};
}
// should we target this source in all future replies?
if (listening()) {
if (seg.header().syn and not seg.header().rst) {
config_mutable().destination = datagram.source_address;
set_listening(false);
} else {
return {};
}
}
return seg;
}
//! Serialize a TCP segment and send it as the payload of a UDP datagram.
//! \param[in] seg is the TCP segment to write
void TCPOverUDPSocketAdapter::write(TCPSegment &seg) {
seg.header().sport = config().source.port();
seg.header().dport = config().destination.port();
_sock.sendto(config().destination, seg.serialize(0));
}
//! Specialize LossyFdAdapter to TCPOverUDPSocketAdapter
template class LossyFdAdapter<TCPOverUDPSocketAdapter>;

View File

@ -0,0 +1,70 @@
#ifndef SPONGE_LIBSPONGE_FD_ADAPTER_HH
#define SPONGE_LIBSPONGE_FD_ADAPTER_HH
#include "file_descriptor.hh"
#include "lossy_fd_adapter.hh"
#include "socket.hh"
#include "tcp_config.hh"
#include "tcp_header.hh"
#include "tcp_segment.hh"
#include <optional>
#include <utility>
//! \brief Basic functionality for file descriptor adaptors
//! \details See TCPOverUDPSocketAdapter and TCPOverIPv4OverTunFdAdapter for more information.
class FdAdapterBase {
private:
FdAdapterConfig _cfg{}; //!< Configuration values
bool _listen = false; //!< Is the connected TCP FSM in listen state?
protected:
FdAdapterConfig &config_mutable() { return _cfg; }
public:
//! \brief Set the listening flag
//! \param[in] l is the new value for the flag
void set_listening(const bool l) { _listen = l; }
//! \brief Get the listening flag
//! \returns whether the FdAdapter is listening for a new connection
bool listening() const { return _listen; }
//! \brief Get the current configuration
//! \returns a const reference
const FdAdapterConfig &config() const { return _cfg; }
//! \brief Get the current configuration (mutable)
//! \returns a mutable reference
FdAdapterConfig &config_mut() { return _cfg; }
//! Called periodically when time elapses
void tick(const size_t) {}
};
//! \brief A FD adaptor that reads and writes TCP segments in UDP payloads
class TCPOverUDPSocketAdapter : public FdAdapterBase {
private:
UDPSocket _sock;
public:
//! Construct from a UDPSocket sliced into a FileDescriptor
explicit TCPOverUDPSocketAdapter(UDPSocket &&sock) : _sock(std::move(sock)) {}
//! Attempts to read and return a TCP segment related to the current connection from a UDP payload
std::optional<TCPSegment> read();
//! Writes a TCP segment into a UDP payload
void write(TCPSegment &seg);
//! Access the underlying UDP socket
operator UDPSocket &() { return _sock; }
//! Access the underlying UDP socket
operator const UDPSocket &() const { return _sock; }
};
//! Typedef for TCPOverUDPSocketAdapter
using LossyTCPOverUDPSocketAdapter = LossyFdAdapter<TCPOverUDPSocketAdapter>;
#endif // SPONGE_LIBSPONGE_FD_ADAPTER_HH

View File

@ -0,0 +1,41 @@
#include "ipv4_datagram.hh"
#include "parser.hh"
#include "util.hh"
#include <stdexcept>
#include <string>
using namespace std;
ParseResult IPv4Datagram::parse(const Buffer buffer) {
NetParser p{buffer};
_header.parse(p);
_payload = p.buffer();
if (_payload.size() != _header.payload_length()) {
return ParseResult::PacketTooShort;
}
return p.get_error();
}
BufferList IPv4Datagram::serialize() const {
if (_payload.size() != _header.payload_length()) {
throw runtime_error("IPv4Datagram::serialize: payload is wrong size");
}
IPv4Header header_out = _header;
header_out.cksum = 0;
const string header_zero_checksum = header_out.serialize();
// calculate checksum -- taken over header only
InternetChecksum check;
check.add(header_zero_checksum);
header_out.cksum = check.value();
BufferList ret;
ret.append(header_out.serialize());
ret.append(_payload);
return ret;
}

View File

@ -0,0 +1,32 @@
#ifndef SPONGE_LIBSPONGE_IPV4_DATAGRAM_HH
#define SPONGE_LIBSPONGE_IPV4_DATAGRAM_HH
#include "buffer.hh"
#include "ipv4_header.hh"
//! \brief [IPv4](\ref rfc::rfc791) Internet datagram
class IPv4Datagram {
private:
IPv4Header _header{};
BufferList _payload{};
public:
//! \brief Parse the segment from a string
ParseResult parse(const Buffer buffer);
//! \brief Serialize the segment to a string
BufferList serialize() const;
//! \name Accessors
//!@{
const IPv4Header &header() const { return _header; }
IPv4Header &header() { return _header; }
const BufferList &payload() const { return _payload; }
BufferList &payload() { return _payload; }
//!@}
};
using InternetDatagram = IPv4Datagram;
#endif // SPONGE_LIBSPONGE_IPV4_DATAGRAM_HH

View File

@ -0,0 +1,159 @@
#include "ipv4_header.hh"
#include "util.hh"
#include <arpa/inet.h>
#include <iomanip>
#include <sstream>
using namespace std;
//! \param[in,out] p is a NetParser from which the IP fields will be extracted
//! \returns a ParseResult indicating success or the reason for failure
//! \details It is important to check for (at least) the following potential errors
//! (but note that NetParser inherently checks for certain errors;
//! use that fact to your advantage!):
//!
//! - data stream is too short to contain a header
//! - wrong IP version number
//! - the header's `hlen` field is shorter than the minimum allowed
//! - there is less data in the header than the `doff` field claims
//! - there is less data in the full datagram than the `len` field claims
//! - the checksum is bad
ParseResult IPv4Header::parse(NetParser &p) {
Buffer original_serialized_version = p.buffer();
const size_t data_size = p.buffer().size();
if (data_size < IPv4Header::LENGTH) {
return ParseResult::PacketTooShort;
}
const uint8_t first_byte = p.u8();
ver = first_byte >> 4; // version
hlen = first_byte & 0x0f; // header length
tos = p.u8(); // type of service
len = p.u16(); // length
id = p.u16(); // id
const uint16_t fo_val = p.u16();
df = static_cast<bool>(fo_val & 0x4000); // don't fragment
mf = static_cast<bool>(fo_val & 0x2000); // more fragments
offset = fo_val & 0x1fff; // offset
ttl = p.u8(); // ttl
proto = p.u8(); // proto
cksum = p.u16(); // checksum
src = p.u32(); // source address
dst = p.u32(); // destination address
if (data_size < 4 * hlen) {
return ParseResult::PacketTooShort;
}
if (ver != 4) {
return ParseResult::WrongIPVersion;
}
if (hlen < 5) {
return ParseResult::HeaderTooShort;
}
if (data_size != len) {
return ParseResult::TruncatedPacket;
}
p.remove_prefix(hlen * 4 - IPv4Header::LENGTH);
if (p.error()) {
return p.get_error();
}
InternetChecksum check;
check.add({original_serialized_version.str().data(), size_t(4 * hlen)});
if (check.value()) {
return ParseResult::BadChecksum;
}
return ParseResult::NoError;
}
//! Serialize the IPv4Header to a string (does not recompute the checksum)
string IPv4Header::serialize() const {
// sanity checks
if (ver != 4) {
throw runtime_error("wrong IP version");
}
if (4 * hlen < IPv4Header::LENGTH) {
throw runtime_error("IP header too short");
}
string ret;
ret.reserve(4 * hlen);
const uint8_t first_byte = (ver << 4) | (hlen & 0xf);
NetUnparser::u8(ret, first_byte); // version and header length
NetUnparser::u8(ret, tos); // type of service
NetUnparser::u16(ret, len); // length
NetUnparser::u16(ret, id); // id
const uint16_t fo_val = (df ? 0x4000 : 0) | (mf ? 0x2000 : 0) | (offset & 0x1fff);
NetUnparser::u16(ret, fo_val); // flags and offset
NetUnparser::u8(ret, ttl); // time to live
NetUnparser::u8(ret, proto); // protocol number
NetUnparser::u16(ret, cksum); // checksum
NetUnparser::u32(ret, src); // src address
NetUnparser::u32(ret, dst); // dst address
ret.resize(4 * hlen); // expand header to advertised size
return ret;
}
uint16_t IPv4Header::payload_length() const { return len - 4 * hlen; }
//! \details This value is needed when computing the checksum of an encapsulated TCP segment.
//! ~~~{.txt}
//! 0 7 8 15 16 23 24 31
//! +--------+--------+--------+--------+
//! | source address |
//! +--------+--------+--------+--------+
//! | destination address |
//! +--------+--------+--------+--------+
//! | zero |protocol| payload length |
//! +--------+--------+--------+--------+
//! ~~~
uint32_t IPv4Header::pseudo_cksum() const {
uint32_t pcksum = (src >> 16) + (src & 0xffff); // source addr
pcksum += (dst >> 16) + (dst & 0xffff); // dest addr
pcksum += proto; // protocol
pcksum += payload_length(); // payload length
return pcksum;
}
//! \returns A string with the header's contents
std::string IPv4Header::to_string() const {
stringstream ss{};
ss << hex << boolalpha << "IP version: " << +ver << '\n'
<< "IP hdr len: " << +hlen << '\n'
<< "IP tos: " << +tos << '\n'
<< "IP dgram len: " << +len << '\n'
<< "IP id: " << +id << '\n'
<< "Flags: df: " << df << " mf: " << mf << '\n'
<< "Offset: " << +offset << '\n'
<< "TTL: " << +ttl << '\n'
<< "Protocol: " << +proto << '\n'
<< "Checksum: " << +cksum << '\n'
<< "Src addr: " << +src << '\n'
<< "Dst addr: " << +dst << '\n';
return ss.str();
}
std::string IPv4Header::summary() const {
stringstream ss{};
ss << hex << boolalpha << "IPv" << +ver << ", "
<< "len=" << +len << ", "
<< "protocol=" << +proto << ", " << (ttl >= 10 ? "" : "ttl=" + ::to_string(ttl) + ", ")
<< "src=" << inet_ntoa({htobe32(src)}) << ", "
<< "dst=" << inet_ntoa({htobe32(dst)});
return ss.str();
}

View File

@ -0,0 +1,71 @@
#ifndef SPONGE_LIBSPONGE_IPV4_HEADER_HH
#define SPONGE_LIBSPONGE_IPV4_HEADER_HH
#include "parser.hh"
//! \brief [IPv4](\ref rfc::rfc791) Internet datagram header
//! \note IP options are not supported
struct IPv4Header {
static constexpr size_t LENGTH = 20; //!< [IPv4](\ref rfc::rfc791) header length, not including options
static constexpr uint8_t DEFAULT_TTL = 128; //!< A reasonable default TTL value
static constexpr uint8_t PROTO_TCP = 6; //!< Protocol number for [tcp](\ref rfc::rfc793)
//! \struct IPv4Header
//! ~~~{.txt}
//! 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 |
//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//! ~~~
//! \name IPv4 Header fields
//!@{
uint8_t ver = 4; //!< IP version
uint8_t hlen = LENGTH / 4; //!< header length (multiples of 32 bits)
uint8_t tos = 0; //!< type of service
uint16_t len = 0; //!< total length of packet
uint16_t id = 0; //!< identification number
bool df = true; //!< don't fragment flag
bool mf = false; //!< more fragments flag
uint16_t offset = 0; //!< fragment offset field
uint8_t ttl = DEFAULT_TTL; //!< time to live field
uint8_t proto = PROTO_TCP; //!< protocol field
uint16_t cksum = 0; //!< checksum field
uint32_t src = 0; //!< src address
uint32_t dst = 0; //!< dst address
//!@}
//! Parse the IP fields from the provided NetParser
ParseResult parse(NetParser &p);
//! Serialize the IP fields
std::string serialize() const;
//! Length of the payload
uint16_t payload_length() const;
//! [pseudo-header's](\ref rfc::rfc793) contribution to the TCP checksum
uint32_t pseudo_cksum() const;
//! Return a string containing a header in human-readable format
std::string to_string() const;
//! Return a string containing a human-readable summary of the header
std::string summary() const;
};
//! \struct IPv4Header
//! This struct can be used to parse an existing IP header or to create a new one.
#endif // SPONGE_LIBSPONGE_IPV4_HEADER_HH

View File

@ -0,0 +1,72 @@
#ifndef SPONGE_LIBSPONGE_LOSSY_FD_ADAPTER_HH
#define SPONGE_LIBSPONGE_LOSSY_FD_ADAPTER_HH
#include "file_descriptor.hh"
#include "tcp_config.hh"
#include "tcp_segment.hh"
#include "util.hh"
#include <optional>
#include <random>
#include <utility>
//! An adapter class that adds random dropping behavior to an FD adapter
template <typename AdapterT>
class LossyFdAdapter {
private:
//! Fast RNG used by _should_drop()
std::mt19937 _rand{get_random_generator()};
//! The underlying FD adapter
AdapterT _adapter;
//! \brief Determine whether or not to drop a given read or write
//! \param[in] uplink is `true` to use the uplink loss probability, else use the downlink loss probability
//! \returns `true` if the segment should be dropped
bool _should_drop(bool uplink) {
const auto &cfg = _adapter.config();
const uint16_t loss = uplink ? cfg.loss_rate_up : cfg.loss_rate_dn;
return loss != 0 && uint16_t(_rand()) < loss;
}
public:
//! Conversion to a FileDescriptor by returning the underlying AdapterT
operator const FileDescriptor &() const { return _adapter; }
//! Construct from a FileDescriptor appropriate to the AdapterT constructor
explicit LossyFdAdapter(AdapterT &&adapter) : _adapter(std::move(adapter)) {}
//! \brief Read from the underlying AdapterT instance, potentially dropping the read datagram
//! \returns std::optional<TCPSegment> that is empty if the segment was dropped or if
//! the underlying AdapterT returned an empty value
std::optional<TCPSegment> read() {
auto ret = _adapter.read();
if (_should_drop(false)) {
return {};
}
return ret;
}
//! \brief Write to the underlying AdapterT instance, potentially dropping the datagram to be written
//! \param[in] seg is the packet to either write or drop
void write(TCPSegment &seg) {
if (_should_drop(true)) {
return;
}
return _adapter.write(seg);
}
//! \name
//! Passthrough functions to the underlying AdapterT instance
//!@{
void set_listening(const bool l) { _adapter.set_listening(l); } //!< FdAdapterBase::set_listening passthrough
const FdAdapterConfig &config() const { return _adapter.config(); } //!< FdAdapterBase::config passthrough
FdAdapterConfig &config_mut() { return _adapter.config_mut(); } //!< FdAdapterBase::config_mut passthrough
void tick(const size_t ms_since_last_tick) {
_adapter.tick(ms_since_last_tick);
} //!< FdAdapterBase::tick passthrough
//!@}
};
#endif // SPONGE_LIBSPONGE_LOSSY_FD_ADAPTER_HH

View File

@ -12,7 +12,7 @@
class TCPConfig {
public:
static constexpr size_t DEFAULT_CAPACITY = 64000; //!< Default capacity
static constexpr size_t MAX_PAYLOAD_SIZE = 1452; //!< Max TCP payload that fits in either IPv4 or UDP datagram
static constexpr size_t MAX_PAYLOAD_SIZE = 1000; //!< Conservative max payload size for real Internet
static constexpr uint16_t TIMEOUT_DFLT = 1000; //!< Default re-transmit timeout is 1 second
static constexpr unsigned MAX_RETX_ATTEMPTS = 8; //!< Maximum re-transmit attempts before giving up

View File

@ -0,0 +1,90 @@
#include "tcp_over_ip.hh"
#include "ipv4_datagram.hh"
#include "ipv4_header.hh"
#include "parser.hh"
#include <arpa/inet.h>
#include <stdexcept>
#include <unistd.h>
#include <utility>
using namespace std;
//! \details This function attempts to parse a TCP segment from
//! the IP datagram's payload.
//!
//! If this succeeds, it then checks that the received segment is related to the
//! current connection. When a TCP connection has been established, this means
//! checking that the source and destination ports in the TCP header are correct.
//!
//! If the TCP connection is listening (i.e., TCPOverIPv4OverTunFdAdapter::_listen is `true`)
//! and the TCP segment read from the wire includes a SYN, this function clears the
//! `_listen` flag and records the source and destination addresses and port numbers
//! from the TCP header; it uses this information to filter future reads.
//! \returns a std::optional<TCPSegment> that is empty if the segment was invalid or unrelated
optional<TCPSegment> TCPOverIPv4Adapter::unwrap_tcp_in_ip(const InternetDatagram &ip_dgram) {
// is the IPv4 datagram for us?
// Note: it's valid to bind to address "0" (INADDR_ANY) and reply from actual address contacted
if (not listening() and (ip_dgram.header().dst != config().source.ipv4_numeric())) {
return {};
}
// is the IPv4 datagram from our peer?
if (not listening() and (ip_dgram.header().src != config().destination.ipv4_numeric())) {
return {};
}
// does the IPv4 datagram claim that its payload is a TCP segment?
if (ip_dgram.header().proto != IPv4Header::PROTO_TCP) {
return {};
}
// is the payload a valid TCP segment?
TCPSegment tcp_seg;
if (ParseResult::NoError != tcp_seg.parse(ip_dgram.payload(), ip_dgram.header().pseudo_cksum())) {
return {};
}
// is the TCP segment for us?
if (tcp_seg.header().dport != config().source.port()) {
return {};
}
// should we target this source addr/port (and use its destination addr as our source) in reply?
if (listening()) {
if (tcp_seg.header().syn and not tcp_seg.header().rst) {
config_mutable().source = {inet_ntoa({htobe32(ip_dgram.header().dst)}), config().source.port()};
config_mutable().destination = {inet_ntoa({htobe32(ip_dgram.header().src)}), tcp_seg.header().sport};
set_listening(false);
} else {
return {};
}
}
// is the TCP segment from our peer?
if (tcp_seg.header().sport != config().destination.port()) {
return {};
}
return tcp_seg;
}
//! Takes a TCP segment, sets port numbers as necessary, and wraps it in an IPv4 datagram
//! \param[in] seg is the TCP segment to convert
InternetDatagram TCPOverIPv4Adapter::wrap_tcp_in_ip(TCPSegment &seg) {
// set the port numbers in the TCP segment
seg.header().sport = config().source.port();
seg.header().dport = config().destination.port();
// create an Internet Datagram and set its addresses and length
InternetDatagram ip_dgram;
ip_dgram.header().src = config().source.ipv4_numeric();
ip_dgram.header().dst = config().destination.ipv4_numeric();
ip_dgram.header().len = ip_dgram.header().hlen * 4 + seg.header().doff * 4 + seg.payload().size();
// set payload, calculating TCP checksum using information from IP header
ip_dgram.payload() = seg.serialize(ip_dgram.header().pseudo_cksum());
return ip_dgram;
}

View File

@ -0,0 +1,19 @@
#ifndef SPONGE_LIBSPONGE_TCP_OVER_IP_HH
#define SPONGE_LIBSPONGE_TCP_OVER_IP_HH
#include "buffer.hh"
#include "fd_adapter.hh"
#include "ipv4_datagram.hh"
#include "tcp_segment.hh"
#include <optional>
//! \brief A converter from TCP segments to serialized IPv4 datagrams
class TCPOverIPv4Adapter : public FdAdapterBase {
public:
std::optional<TCPSegment> unwrap_tcp_in_ip(const InternetDatagram &ip_dgram);
InternetDatagram wrap_tcp_in_ip(TCPSegment &seg);
};
#endif // SPONGE_LIBSPONGE_TCP_OVER_IP_HH

View File

@ -0,0 +1,295 @@
#include "tcp_sponge_socket.hh"
#include "parser.hh"
#include "tun.hh"
#include "util.hh"
#include <cstddef>
#include <exception>
#include <iostream>
#include <stdexcept>
#include <string>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
#include <utility>
using namespace std;
static constexpr size_t TCP_TICK_MS = 10;
//! \param[in] condition is a function returning true if loop should continue
template <typename AdaptT>
void TCPSpongeSocket<AdaptT>::_tcp_loop(const function<bool()> &condition) {
auto base_time = timestamp_ms();
while (condition()) {
auto ret = _eventloop.wait_next_event(TCP_TICK_MS);
if (ret == EventLoop::Result::Exit or _abort) {
break;
}
if (_tcp.value().active()) {
const auto next_time = timestamp_ms();
_tcp.value().tick(next_time - base_time);
_datagram_adapter.tick(next_time - base_time);
base_time = next_time;
}
}
}
//! \param[in] data_socket_pair is a pair of connected AF_UNIX SOCK_STREAM sockets
//! \param[in] datagram_interface is the interface for reading and writing datagrams
template <typename AdaptT>
TCPSpongeSocket<AdaptT>::TCPSpongeSocket(pair<FileDescriptor, FileDescriptor> data_socket_pair,
AdaptT &&datagram_interface)
: LocalStreamSocket(move(data_socket_pair.first))
, _thread_data(move(data_socket_pair.second))
, _datagram_adapter(move(datagram_interface)) {
_thread_data.set_blocking(false);
}
template <typename AdaptT>
void TCPSpongeSocket<AdaptT>::_initialize_TCP(const TCPConfig &config) {
_tcp.emplace(config);
// Set up the event loop
// There are four possible events to handle:
//
// 1) Incoming datagram received (needs to be given to
// TCPConnection::segment_received method)
//
// 2) Outbound bytes received from local application via a write()
// call (needs to be read from the local stream socket and
// given to TCPConnection::data_written method)
//
// 3) Incoming bytes reassembled by the TCPConnection
// (needs to be read from the inbound_stream and written
// to the local stream socket back to the application)
//
// 4) Outbound segment generated by TCP (needs to be
// given to underlying datagram socket)
// rule 1: read from filtered packet stream and dump into TCPConnection
_eventloop.add_rule(_datagram_adapter,
Direction::In,
[&] {
auto seg = _datagram_adapter.read();
if (seg) {
_tcp->segment_received(move(seg.value()));
}
// debugging output:
if (_thread_data.eof() and _tcp.value().bytes_in_flight() == 0 and not _fully_acked) {
cerr << "DEBUG: Outbound stream to "
<< _datagram_adapter.config().destination.to_string()
<< " has been fully acknowledged.\n";
_fully_acked = true;
}
},
[&] { return _tcp->active(); });
// rule 2: read from pipe into outbound buffer
_eventloop.add_rule(
_thread_data,
Direction::In,
[&] {
const auto data = _thread_data.read(_tcp->remaining_outbound_capacity());
const auto len = data.size();
const auto amount_written = _tcp->write(move(data));
if (amount_written != len) {
throw runtime_error("TCPConnection::write() accepted less than advertised length");
}
if (_thread_data.eof()) {
_tcp->end_input_stream();
_outbound_shutdown = true;
// debugging output:
cerr << "DEBUG: Outbound stream to " << _datagram_adapter.config().destination.to_string()
<< " finished (" << _tcp.value().bytes_in_flight() << " byte"
<< (_tcp.value().bytes_in_flight() == 1 ? "" : "s") << " still in flight).\n";
}
},
[&] { return (_tcp->active()) and (not _outbound_shutdown) and (_tcp->remaining_outbound_capacity() > 0); },
[&] {
_tcp->end_input_stream();
_outbound_shutdown = true;
});
// rule 3: read from inbound buffer into pipe
_eventloop.add_rule(
_thread_data,
Direction::Out,
[&] {
ByteStream &inbound = _tcp->inbound_stream();
// Write from the inbound_stream into
// the pipe, handling the possibility of a partial
// write (i.e., only pop what was actually written).
const size_t amount_to_write = min(size_t(65536), inbound.buffer_size());
const std::string buffer = inbound.peek_output(amount_to_write);
const auto bytes_written = _thread_data.write(move(buffer), false);
inbound.pop_output(bytes_written);
if (inbound.eof() or inbound.error()) {
_thread_data.shutdown(SHUT_WR);
_inbound_shutdown = true;
// debugging output:
cerr << "DEBUG: Inbound stream from " << _datagram_adapter.config().destination.to_string()
<< " finished " << (inbound.error() ? "with an error/reset.\n" : "cleanly.\n");
if (_tcp.value().state() == TCPState::State::TIME_WAIT) {
cerr << "DEBUG: Waiting for lingering segments (e.g. retransmissions of FIN) from peer...\n";
}
}
},
[&] {
return (not _tcp->inbound_stream().buffer_empty()) or
((_tcp->inbound_stream().eof() or _tcp->inbound_stream().error()) and not _inbound_shutdown);
});
// rule 4: read outbound segments from TCPConnection and send as datagrams
_eventloop.add_rule(_datagram_adapter,
Direction::Out,
[&] {
while (not _tcp->segments_out().empty()) {
_datagram_adapter.write(_tcp->segments_out().front());
_tcp->segments_out().pop();
}
},
[&] { return not _tcp->segments_out().empty(); });
}
//! \brief Call [socketpair](\ref man2::socketpair) and return connected Unix-domain sockets of specified type
//! \param[in] type is the type of AF_UNIX sockets to create (e.g., SOCK_SEQPACKET)
//! \returns a std::pair of connected sockets
static inline pair<FileDescriptor, FileDescriptor> socket_pair_helper(const int type) {
int fds[2];
SystemCall("socketpair", ::socketpair(AF_UNIX, type, 0, static_cast<int *>(fds)));
return {FileDescriptor(fds[0]), FileDescriptor(fds[1])};
}
//! \param[in] datagram_interface is the underlying interface (e.g. to UDP, IP, or Ethernet)
template <typename AdaptT>
TCPSpongeSocket<AdaptT>::TCPSpongeSocket(AdaptT &&datagram_interface)
: TCPSpongeSocket(socket_pair_helper(SOCK_STREAM), move(datagram_interface)) {}
template <typename AdaptT>
TCPSpongeSocket<AdaptT>::~TCPSpongeSocket() {
try {
if (_tcp_thread.joinable()) {
cerr << "Warning: unclean shutdown of TCPSpongeSocket\n";
// force the other side to exit
_abort.store(true);
_tcp_thread.join();
}
} catch (const exception &e) {
cerr << "Exception destructing TCPSpongeSocket: " << e.what() << endl;
}
}
template <typename AdaptT>
void TCPSpongeSocket<AdaptT>::wait_until_closed() {
shutdown(SHUT_RDWR);
if (_tcp_thread.joinable()) {
cerr << "DEBUG: Waiting for clean shutdown... ";
_tcp_thread.join();
cerr << "done.\n";
}
}
//! \param[in] c_tcp is the TCPConfig for the TCPConnection
//! \param[in] c_ad is the FdAdapterConfig for the FdAdapter
template <typename AdaptT>
void TCPSpongeSocket<AdaptT>::connect(const TCPConfig &c_tcp, const FdAdapterConfig &c_ad) {
if (_tcp) {
throw runtime_error("connect() with TCPConnection already initialized");
}
_initialize_TCP(c_tcp);
_datagram_adapter.config_mut() = c_ad;
cerr << "DEBUG: Connecting to " << c_ad.destination.to_string() << "... ";
_tcp->connect();
const TCPState expected_state = TCPState::State::SYN_SENT;
if (_tcp->state() != expected_state) {
throw runtime_error("After TCPConnection::connect(), state was " + _tcp->state().name() + " but expected " +
expected_state.name());
}
_tcp_loop([&] { return _tcp->state() == TCPState::State::SYN_SENT; });
cerr << "done.\n";
_tcp_thread = thread(&TCPSpongeSocket::_tcp_main, this);
}
//! \param[in] c_tcp is the TCPConfig for the TCPConnection
//! \param[in] c_ad is the FdAdapterConfig for the FdAdapter
template <typename AdaptT>
void TCPSpongeSocket<AdaptT>::listen_and_accept(const TCPConfig &c_tcp, const FdAdapterConfig &c_ad) {
if (_tcp) {
throw runtime_error("listen_and_accept() with TCPConnection already initialized");
}
_initialize_TCP(c_tcp);
_datagram_adapter.config_mut() = c_ad;
_datagram_adapter.set_listening(true);
cerr << "DEBUG: Listening for incoming connection... ";
_tcp_loop([&] {
const auto s = _tcp->state();
return (s == TCPState::State::LISTEN or s == TCPState::State::SYN_RCVD or s == TCPState::State::SYN_SENT);
});
cerr << "new connection from " << _datagram_adapter.config().destination.to_string() << ".\n";
_tcp_thread = thread(&TCPSpongeSocket::_tcp_main, this);
}
template <typename AdaptT>
void TCPSpongeSocket<AdaptT>::_tcp_main() {
try {
if (not _tcp.has_value()) {
throw runtime_error("no TCP");
}
_tcp_loop([] { return true; });
shutdown(SHUT_RDWR);
if (not _tcp.value().active()) {
cerr << "DEBUG: TCP connection finished "
<< (_tcp.value().state() == TCPState::State::RESET ? "uncleanly" : "cleanly.\n");
}
_tcp.reset();
} catch (const exception &e) {
cerr << "Exception in TCPConnection runner thread: " << e.what() << "\n";
throw e;
}
}
//! Specialization of TCPSpongeSocket for TCPOverUDPSocketAdapter
template class TCPSpongeSocket<TCPOverUDPSocketAdapter>;
//! Specialization of TCPSpongeSocket for TCPOverIPv4OverTunFdAdapter
template class TCPSpongeSocket<TCPOverIPv4OverTunFdAdapter>;
//! Specialization of TCPSpongeSocket for LossyTCPOverUDPSocketAdapter
template class TCPSpongeSocket<LossyTCPOverUDPSocketAdapter>;
//! Specialization of TCPSpongeSocket for LossyTCPOverIPv4OverTunFdAdapter
template class TCPSpongeSocket<LossyTCPOverIPv4OverTunFdAdapter>;
CS144TCPSocket::CS144TCPSocket() : TCPOverIPv4SpongeSocket(TCPOverIPv4OverTunFdAdapter(TunFD("tun144"))) {}
void CS144TCPSocket::connect(const Address &address) {
TCPConfig tcp_config;
tcp_config.rt_timeout = 100;
FdAdapterConfig multiplexer_config;
multiplexer_config.source = {"169.254.144.9", to_string(uint16_t(random_device()()))};
multiplexer_config.destination = address;
TCPOverIPv4SpongeSocket::connect(tcp_config, multiplexer_config);
}

View File

@ -0,0 +1,129 @@
#ifndef SPONGE_LIBSPONGE_TCP_SPONGE_SOCKET_HH
#define SPONGE_LIBSPONGE_TCP_SPONGE_SOCKET_HH
#include "byte_stream.hh"
#include "eventloop.hh"
#include "fd_adapter.hh"
#include "file_descriptor.hh"
#include "tcp_config.hh"
#include "tcp_connection.hh"
#include "tcp_over_ip.hh"
#include "tuntap_adapter.hh"
#include <atomic>
#include <cstdint>
#include <optional>
#include <thread>
#include <vector>
//! Multithreaded wrapper around TCPConnection that approximates the Unix sockets API
template <typename AdaptT>
class TCPSpongeSocket : public LocalStreamSocket {
private:
//! Stream socket for reads and writes between owner and TCP thread
LocalStreamSocket _thread_data;
//! Adapter to underlying datagram socket (e.g., UDP or IP)
AdaptT _datagram_adapter;
//! Set up the TCPConnection and the event loop
void _initialize_TCP(const TCPConfig &config);
//! TCP state machine
std::optional<TCPConnection> _tcp{};
//! eventloop that handles all the events (new inbound datagram, new outbound bytes, new inbound bytes)
EventLoop _eventloop{};
//! Process events while specified condition is true
void _tcp_loop(const std::function<bool()> &condition);
//! Main loop of TCPConnection thread
void _tcp_main();
//! Handle to the TCPConnection thread; owner thread calls join() in the destructor
std::thread _tcp_thread{};
//! Construct LocalStreamSocket fds from socket pair, initialize eventloop
TCPSpongeSocket(std::pair<FileDescriptor, FileDescriptor> data_socket_pair, AdaptT &&datagram_interface);
std::atomic_bool _abort{false}; //!< Flag used by the owner to force the TCPConnection thread to shut down
bool _inbound_shutdown{false}; //!< Has TCPSpongeSocket shut down the incoming data to the owner?
bool _outbound_shutdown{false}; //!< Has the owner shut down the outbound data to the TCP connection?
bool _fully_acked{false}; //!< Has the outbound data been fully acknowledged by the peer?
public:
//! Construct from the interface that the TCPConnection thread will use to read and write datagrams
explicit TCPSpongeSocket(AdaptT &&datagram_interface);
//! Close socket, and wait for TCPConnection to finish
//! \note Calling this function is only advisable if the socket has reached EOF,
//! or else may wait foreever for remote peer to close the TCP connection.
void wait_until_closed();
//! Connect using the specified configurations; blocks until connect succeeds or fails
void connect(const TCPConfig &c_tcp, const FdAdapterConfig &c_ad);
//! Listen and accept using the specified configurations; blocks until accept succeeds or fails
void listen_and_accept(const TCPConfig &c_tcp, const FdAdapterConfig &c_ad);
//! When a connected socket is destructed, it will send a RST
~TCPSpongeSocket();
//! \name
//! This object cannot be safely moved or copied, since it is in use by two threads simultaneously
//!@{
TCPSpongeSocket(const TCPSpongeSocket &) = delete;
TCPSpongeSocket(TCPSpongeSocket &&) = delete;
TCPSpongeSocket &operator=(const TCPSpongeSocket &) = delete;
TCPSpongeSocket &operator=(TCPSpongeSocket &&) = delete;
//!@}
//! \name
//! Some methods of the parent Socket wouldn't work as expected on the TCP socket, so delete them
//!@{
void bind(const Address &address) = delete;
Address local_address() const = delete;
Address peer_address() const = delete;
void set_reuseaddr() = delete;
//!@}
};
using TCPOverUDPSpongeSocket = TCPSpongeSocket<TCPOverUDPSocketAdapter>;
using TCPOverIPv4SpongeSocket = TCPSpongeSocket<TCPOverIPv4OverTunFdAdapter>;
using LossyTCPOverUDPSpongeSocket = TCPSpongeSocket<LossyTCPOverUDPSocketAdapter>;
using LossyTCPOverIPv4SpongeSocket = TCPSpongeSocket<LossyTCPOverIPv4OverTunFdAdapter>;
//! \class TCPSpongeSocket
//! This class involves the simultaneous operation of two threads.
//!
//! One, the "owner" or foreground thread, interacts with this class in much the
//! same way as one would interact with a TCPSocket: it connects or listens, writes to
//! and reads from a reliable data stream, etc. Only the owner thread calls public
//! methods of this class.
//!
//! The other, the "TCPConnection" thread, takes care of the back-end tasks that the kernel would
//! perform for a TCPSocket: reading and parsing datagrams from the wire, filtering out
//! segments unrelated to the connection, etc.
//!
//! There are a few notable differences between the TCPSpongeSocket and TCPSocket interfaces:
//!
//! - a TCPSpongeSocket can only accept a single connection
//! - listen_and_accept() is a blocking function call that acts as both [listen(2)](\ref man2::listen)
//! and [accept(2)](\ref man2::accept)
//! - if TCPSpongeSocket is destructed while a TCP connection is open, the connection is
//! immediately terminated with a RST (call `wait_until_closed` to avoid this)
//! Helper class that makes a TCPOverIPv4SpongeSocket behave more like a (kernel) TCPSocket
class CS144TCPSocket : public TCPOverIPv4SpongeSocket {
public:
CS144TCPSocket();
void connect(const Address &address);
};
#endif // SPONGE_LIBSPONGE_TCP_SPONGE_SOCKET_HH

View File

@ -2,6 +2,83 @@
using namespace std;
bool TCPState::operator==(const TCPState &other) const {
return _active == other._active and _linger_after_streams_finish == other._linger_after_streams_finish and
_sender == other._sender and _receiver == other._receiver;
}
bool TCPState::operator!=(const TCPState &other) const { return not operator==(other); }
string TCPState::name() const {
return "sender=`" + _sender + "`, receiver=`" + _receiver + "`, active=" + to_string(_active) +
", linger_after_streams_finish=" + to_string(_linger_after_streams_finish);
}
TCPState::TCPState(const TCPState::State state) {
switch (state) {
case TCPState::State::LISTEN:
_receiver = TCPReceiverStateSummary::LISTEN;
_sender = TCPSenderStateSummary::CLOSED;
break;
case TCPState::State::SYN_RCVD:
_receiver = TCPReceiverStateSummary::SYN_RECV;
_sender = TCPSenderStateSummary::SYN_SENT;
break;
case TCPState::State::SYN_SENT:
_receiver = TCPReceiverStateSummary::LISTEN;
_sender = TCPSenderStateSummary::SYN_SENT;
break;
case TCPState::State::ESTABLISHED:
_receiver = TCPReceiverStateSummary::SYN_RECV;
_sender = TCPSenderStateSummary::SYN_ACKED;
break;
case TCPState::State::CLOSE_WAIT:
_receiver = TCPReceiverStateSummary::FIN_RECV;
_sender = TCPSenderStateSummary::SYN_ACKED;
_linger_after_streams_finish = false;
break;
case TCPState::State::LAST_ACK:
_receiver = TCPReceiverStateSummary::FIN_RECV;
_sender = TCPSenderStateSummary::FIN_SENT;
_linger_after_streams_finish = false;
break;
case TCPState::State::CLOSING:
_receiver = TCPReceiverStateSummary::FIN_RECV;
_sender = TCPSenderStateSummary::FIN_SENT;
break;
case TCPState::State::FIN_WAIT_1:
_receiver = TCPReceiverStateSummary::SYN_RECV;
_sender = TCPSenderStateSummary::FIN_SENT;
break;
case TCPState::State::FIN_WAIT_2:
_receiver = TCPReceiverStateSummary::SYN_RECV;
_sender = TCPSenderStateSummary::FIN_ACKED;
break;
case TCPState::State::TIME_WAIT:
_receiver = TCPReceiverStateSummary::FIN_RECV;
_sender = TCPSenderStateSummary::FIN_ACKED;
break;
case TCPState::State::RESET:
_receiver = TCPReceiverStateSummary::ERROR;
_sender = TCPSenderStateSummary::ERROR;
_linger_after_streams_finish = false;
_active = false;
break;
case TCPState::State::CLOSED:
_receiver = TCPReceiverStateSummary::FIN_RECV;
_sender = TCPSenderStateSummary::FIN_ACKED;
_linger_after_streams_finish = false;
_active = false;
break;
}
}
TCPState::TCPState(const TCPSender &sender, const TCPReceiver &receiver, const bool active, const bool linger)
: _sender(state_summary(sender))
, _receiver(state_summary(receiver))
, _active(active)
, _linger_after_streams_finish(active ? linger : false) {}
string TCPState::state_summary(const TCPReceiver &receiver) {
if (receiver.stream_out().error()) {
return TCPReceiverStateSummary::ERROR;

View File

@ -21,7 +21,41 @@
//! sender/receiver states and two variables that belong to the
//! overarching TCPConnection object.
class TCPState {
private:
std::string _sender{};
std::string _receiver{};
bool _active{true};
bool _linger_after_streams_finish{true};
public:
bool operator==(const TCPState &other) const;
bool operator!=(const TCPState &other) const;
//! \brief Official state names from the [TCP](\ref rfc::rfc793) specification
enum class State {
LISTEN = 0, //!< Listening for a peer to connect
SYN_RCVD, //!< Got the peer's SYN
SYN_SENT, //!< Sent a SYN to initiate a connection
ESTABLISHED, //!< Three-way handshake complete
CLOSE_WAIT, //!< Remote side has sent a FIN, connection is half-open
LAST_ACK, //!< Local side sent a FIN from CLOSE_WAIT, waiting for ACK
FIN_WAIT_1, //!< Sent a FIN to the remote side, not yet ACK'd
FIN_WAIT_2, //!< Received an ACK for previously-sent FIN
CLOSING, //!< Received a FIN just after we sent one
TIME_WAIT, //!< Both sides have sent FIN and ACK'd, waiting for 2 MSL
CLOSED, //!< A connection that has terminated normally
RESET, //!< A connection that terminated abnormally
};
//! \brief Summarize the TCPState in a string
std::string name() const;
//! \brief Construct a TCPState given a sender, a receiver, and the TCPConnection's active and linger bits
TCPState(const TCPSender &sender, const TCPReceiver &receiver, const bool active, const bool linger);
//! \brief Construct a TCPState that corresponds to one of the "official" TCP state names
TCPState(const TCPState::State state);
//! \brief Summarize the state of a TCPReceiver in a string
static std::string state_summary(const TCPReceiver &receiver);

View File

@ -0,0 +1,6 @@
#include "tuntap_adapter.hh"
using namespace std;
//! Specialize LossyFdAdapter to TCPOverIPv4OverTunFdAdapter
template class LossyFdAdapter<TCPOverIPv4OverTunFdAdapter>;

View File

@ -0,0 +1,42 @@
#ifndef SPONGE_LIBSPONGE_TUNFD_ADAPTER_HH
#define SPONGE_LIBSPONGE_TUNFD_ADAPTER_HH
#include "tcp_over_ip.hh"
#include "tun.hh"
#include <optional>
#include <unordered_map>
#include <utility>
//! \brief A FD adapter for IPv4 datagrams read from and written to a TUN device
class TCPOverIPv4OverTunFdAdapter : public TCPOverIPv4Adapter {
private:
TunFD _tun;
public:
//! Construct from a TunFD
explicit TCPOverIPv4OverTunFdAdapter(TunFD &&tun) : _tun(std::move(tun)) {}
//! Attempts to read and parse an IPv4 datagram containing a TCP segment related to the current connection
std::optional<TCPSegment> read() {
InternetDatagram ip_dgram;
if (ip_dgram.parse(_tun.read()) != ParseResult::NoError) {
return {};
}
return unwrap_tcp_in_ip(ip_dgram);
}
//! Creates an IPv4 datagram from a TCP segment and writes it to the TUN device
void write(TCPSegment &seg) { _tun.write(wrap_tcp_in_ip(seg).serialize()); }
//! Access the underlying TUN device
operator TunFD &() { return _tun; }
//! Access the underlying TUN device
operator const TunFD &() const { return _tun; }
};
//! Typedef for TCPOverIPv4OverTunFdAdapter
using LossyTCPOverIPv4OverTunFdAdapter = LossyFdAdapter<TCPOverIPv4OverTunFdAdapter>;
#endif // SPONGE_LIBSPONGE_TUNFD_ADAPTER_HH

View File

@ -1,4 +1,4 @@
add_library (spongechecks STATIC byte_stream_test_harness.cc)
add_library (spongechecks STATIC send_equivalence_checker.cc tcp_fsm_test_harness.cc byte_stream_test_harness.cc)
macro (add_test_exec exec_name)
add_executable ("${exec_name}" "${exec_name}.cc")
@ -7,6 +7,12 @@ macro (add_test_exec exec_name)
endmacro (add_test_exec)
add_test_exec (tcp_parser ${LIBPCAP})
add_test_exec (ipv4_parser ${LIBPCAP})
add_test_exec (fsm_active_close)
add_test_exec (fsm_passive_close)
add_test_exec (fsm_ack_rst_relaxed)
add_test_exec (fsm_ack_rst_win_relaxed)
add_test_exec (fsm_stream_reassembler_cap)
add_test_exec (fsm_stream_reassembler_single)
add_test_exec (fsm_stream_reassembler_seq)
add_test_exec (fsm_stream_reassembler_dup)
@ -14,16 +20,23 @@ add_test_exec (fsm_stream_reassembler_holes)
add_test_exec (fsm_stream_reassembler_many)
add_test_exec (fsm_stream_reassembler_overlapping)
add_test_exec (fsm_stream_reassembler_win)
add_test_exec (fsm_stream_reassembler_cap)
add_test_exec (fsm_connect_relaxed)
add_test_exec (fsm_listen_relaxed)
add_test_exec (fsm_reorder)
add_test_exec (fsm_loopback)
add_test_exec (fsm_loopback_win)
add_test_exec (fsm_retx_relaxed)
add_test_exec (fsm_retx_win)
add_test_exec (fsm_winsize)
add_test_exec (wrapping_integers_cmp)
add_test_exec (wrapping_integers_unwrap)
add_test_exec (wrapping_integers_wrap)
add_test_exec (wrapping_integers_roundtrip)
add_test_exec (byte_stream_construction)
add_test_exec (byte_stream_one_write)
add_test_exec (byte_stream_two_writes)
add_test_exec (byte_stream_capacity)
add_test_exec (byte_stream_many_writes)
add_test_exec (wrapping_integers_cmp)
add_test_exec (wrapping_integers_unwrap)
add_test_exec (wrapping_integers_wrap)
add_test_exec (wrapping_integers_roundtrip)
add_test_exec (recv_connect)
add_test_exec (recv_transmit)
add_test_exec (recv_window)

BIN
tests/ipv4_parser.data Normal file

Binary file not shown.

111
tun.sh Executable file
View File

@ -0,0 +1,111 @@
#!/bin/bash
show_usage () {
echo "Usage: $0 <start | stop | restart | check> [tunnum ...]"
exit 1
}
start_tun () {
local TUNNUM="$1" TUNDEV="tun$1"
ip tuntap add mode tun user "${SUDO_USER}" name "${TUNDEV}"
ip addr add "${TUN_IP_PREFIX}.${TUNNUM}.1/24" dev "${TUNDEV}"
ip link set dev "${TUNDEV}" up
ip route change "${TUN_IP_PREFIX}.${TUNNUM}.0/24" dev "${TUNDEV}" rto_min 10ms
# Apply NAT (masquerading) only to traffic from CS144's network devices
iptables -t nat -A PREROUTING -s ${TUN_IP_PREFIX}.${TUNNUM}.0/24 -j CONNMARK --set-mark ${TUNNUM}
iptables -t nat -A POSTROUTING -j MASQUERADE -m connmark --mark ${TUNNUM}
echo 1 > /proc/sys/net/ipv4/ip_forward
}
stop_tun () {
local TUNDEV="tun$1"
iptables -t nat -D PREROUTING -s ${TUN_IP_PREFIX}.${1}.0/24 -j CONNMARK --set-mark ${1}
iptables -t nat -D POSTROUTING -j MASQUERADE -m connmark --mark ${1}
ip tuntap del mode tun name "$TUNDEV"
}
start_all () {
while [ ! -z "$1" ]; do
local INTF="$1"; shift
start_tun "$INTF"
done
}
stop_all () {
while [ ! -z "$1" ]; do
local INTF="$1"; shift
stop_tun "$INTF"
done
}
restart_all() {
stop_all "$@"
start_all "$@"
}
check_tun () {
[ "$#" != 1 ] && { echo "bad params in check_tun"; exit 1; }
local TUNDEV="tun${1}"
# make sure tun is healthy: device is up, ip_forward is set, and iptables is configured
ip link show ${TUNDEV} &>/dev/null || return 1
[ "$(cat /proc/sys/net/ipv4/ip_forward)" = "1" ] || return 2
}
check_sudo () {
if [ "$SUDO_USER" = "root" ]; then
echo "please execute this script as a regular user, not as root"
exit 1
fi
if [ -z "$SUDO_USER" ]; then
# if the user didn't call us with sudo, re-execute
exec sudo $0 "$MODE" "$@"
fi
}
# check arguments
if [ -z "$1" ] || ([ "$1" != "start" ] && [ "$1" != "stop" ] && [ "$1" != "restart" ] && [ "$1" != "check" ]); then
show_usage
fi
MODE=$1; shift
# set default argument
if [ "$#" = "0" ]; then
set -- 144 145
fi
# execute 'check' before trying to sudo
# - like start, but exit successfully if everything is OK
if [ "$MODE" = "check" ]; then
declare -a INTFS
MODE="start"
while [ ! -z "$1" ]; do
INTF="$1"; shift
check_tun ${INTF}
RET=$?
if [ "$RET" = "0" ]; then
continue
fi
if [ "$((RET > 1))" = "1" ]; then
MODE="restart"
fi
INTFS+=($INTF)
done
# address only the interfaces that need it
set -- "${INTFS[@]}"
if [ "$#" = "0" ]; then
exit 0
fi
echo -e "[$0] Bringing up tunnels ${INTFS[@]}:"
fi
# sudo if necessary
check_sudo "$@"
# get configuration
. "$(dirname "$0")"/etc/tunconfig
# start, stop, or restart all intfs
eval "${MODE}_all" "$@"

238
txrx.sh Executable file
View File

@ -0,0 +1,238 @@
#!/bin/bash
show_usage() {
echo "Usage: $0 <-i|-u> <-c|-s> <-R|-S|-D> [-n|-o]"
echo " [-t <rtto>] [-d <size>] [-w <size>] [-l <rate>] [-L <rate>]"
echo
echo " Option Default"
echo " -- --"
echo " -i or -u Select IP or UDP mode (must specify)"
echo " -c or -s Select client or server mode (must specify)"
echo " -R, -S, -D Receive test, Send test, or Duplex test (must specify)"
echo
echo " -t <rtto> Set rtto to <rtto> ms 12"
echo " -d <size> Set total transfer size to <size> 32"
echo " -w <size> Set window size to <size> 1452"
echo
echo " -l <rate> Set downlink loss to <rate> (float in 0..1) 0"
echo " -L <rate> Set uplink loss to <rate> (float in 0..1) 0"
echo
echo " -n In IP mode, use tcp_native rather tcp_ipv4_ref False"
echo " -o In IP mode, use socat rather than tcp_ipv4_ref False"
[ ! -z "$1" ] && { echo; echo ERROR: "$1"; }
exit 1
}
get_cmdline_options () {
# prepare to use getopts
local OPT= OPTIND=1 OPTARG=
CSMODE= RSDMODE= DATASIZE=32 WINSIZE= IUMODE= USE_IPV4= RTTO="-t 12" LOSS_UP= LOSS_DN=
while getopts "t:oniucsRSDd:w:p:l:L:" OPT; do
case "$OPT" in
i|u)
[ ! -z "$IUMODE" ] && show_usage "Only one of -i and -u is allowed."
IUMODE=$OPT
;;
c|s)
[ ! -z "$CSMODE" ] && show_usage "Only one of -c and -s is allowed."
CSMODE=$OPT
;;
R|S|D)
[ ! -z "$RSDMODE" ] && show_usage "Only one of -R, -S, and -D is allowed."
RSDMODE=$OPT
;;
d)
DATASIZE="$OPTARG"
;;
w)
expand_num "$OPTARG" || show_usage "Bad numeric arg \"$OPTARG\" to -w."
WINSIZE="-w ${NUM_EXPANDED}"
;;
l)
LOSS_DN="$OPTARG"
;;
L)
LOSS_UP="$OPTARG"
;;
n|o)
[ ! -z "$USE_IPV4" ] && show_usage "Only one of -n and -o is allowed."
USE_IPV4=$OPT
;;
t)
expand_num "$OPTARG" || show_usage "Bad numeric arg \"$OPTARG\" to -t."
RTTO="-t ${NUM_EXPANDED}"
;;
*)
show_usage "Unknown option $OPT"
;;
esac
done
if [ "$OPTIND" != $(($# + 1)) ]; then
show_usage "Extraneous arguments detected."
fi
if [ -z "$CSMODE" ] || [ -z "$RSDMODE" ] || [ -z "$IUMODE" ]; then
show_usage "You must specify either -i or -u, either -c or -s, and one of -R, -S, and -D."
fi
if [ ! -z "$USE_IPV4" ] && [ "$IUMODE" != "i" ]; then
show_usage "-n and -o may only be specified in IP mode (-i)."
fi
# loss param args depend on whether we're applying to ref or test program (test uplink == loss downlink)
{ [ ! -z "$USE_IPV4" ] && local LD_SWITCH="-Ld" LU_SWITCH="-Lu"; } || local LD_SWITCH="-Lu" LU_SWITCH="-Ld"
[ ! -z "$LOSS_DN" ] && LOSS_DN="${LD_SWITCH} ${LOSS_DN}"
[ ! -z "$LOSS_UP" ] && LOSS_UP="${LU_SWITCH} ${LOSS_UP}"
}
expand_num () {
[ "$#" != "1" ] && { echo "bad args"; exit 1; }
NUM_EXPANDED=$(numfmt --from=iec "$1" 2>/dev/null)
return $?
}
_socat_listen () {
coproc socat tcp4-listen:${SERVER_PORT},reuseaddr,reuseport,linger=2 stdio >"$1" <"$2" && sleep 0.1
set +u
[ -z "$COPROC_PID" ] && { echo "Error in _socat_listen"; exit 1; }
set -u
}
_socat_connect () {
socat tcp4-connect:${TEST_HOST}:${SERVER_PORT},reuseaddr,reuseport,linger=2 stdio >"$1" <"$2" ||
{ echo "Error in _socat_connect"; exit 1; }
}
_rt_listen () {
coproc $3 -l $4 ${SERVER_PORT} >"$1" <"$2" && sleep 0.1
set +u
[ -z "$COPROC_PID" ] && { echo "Error in _rt_listen"; exit 1; }
set -u
}
_rt_connect () {
$3 $4 ${SERVER_PORT} >"$1" <"$2" || { echo "Error in _rt_connect"; exit 1; }
}
test_listen () {
[ "$#" != 2 ] && { echo "bad args"; exit 1; }
_rt_listen "$1" "$2" "${TEST_PROG}" "${TEST_HOST}"
}
test_connect () {
[ "$#" != 2 ] && { echo "bad args"; exit 1; }
_rt_connect "$1" "$2" "${TEST_PROG}" "${REF_HOST}"
}
ref_listen () {
[ "$#" != 2 ] && { echo "bad args"; exit 1; }
if [ "$IUMODE" = "u" ] || [ -z "$USE_IPV4" ] || [ "$USE_IPV4" = "n" ]; then
_rt_listen "$1" "$2" "${REF_PROG}" "${REF_HOST}"
else
_socat_listen "$1" "$2"
fi
}
ref_connect () {
[ "$#" != 2 ] && { echo "bad args"; exit 1; }
if [ "$IUMODE" = "u" ] || [ -z "$USE_IPV4" ] || [ "$USE_IPV4" = "n" ]; then
_rt_connect "$1" "$2" "${REF_PROG}" "${TEST_HOST}"
else
_socat_connect "$1" "$2"
fi
}
hash_file () {
[ "$#" != "1" ] && { echo "bad args"; exit 1; }
sha256sum "$1" | cut -d \ -f 1
}
make_test_file () {
if expand_num "$2"; then
dd status=none if=/dev/urandom of="$1" bs="${NUM_EXPANDED}" count=1 || { echo "Failed to make test file."; exit 1; }
else
# can't interpret as a number, so interpret as literal data to send
echo -en "$2" >"$1"
fi
}
exit_cleanup () {
set +u
rm -f "${TEST_IN_FILE}" "${TEST_OUT_FILE}" "${TEST_OUT2_FILE}"
[ ! -z "$COPROC_PID" ] && kill ${COPROC_PID}
}
# make sure tun device is running
ip link show tun144 &>/dev/null || { echo "please enable tun144 and re-run"; exit 1; }
ip link show tun145 &>/dev/null || { echo "please enable tun145 and re-run"; exit 1; }
set -u
trap exit_cleanup EXIT
get_cmdline_options "$@"
. "$(dirname "$0")"/etc/tunconfig
REF_HOST=${TUN_IP_PREFIX}.144.1
TEST_HOST=${TUN_IP_PREFIX}.144.1
SERVER_PORT=$(($((RANDOM % 50000)) + 1025))
if [ "$IUMODE" = "i" ]; then
# IPv4 mode
TEST_HOST=${TUN_IP_PREFIX}.144.9
if [ -z "$USE_IPV4" ]; then
REF_HOST=${TUN_IP_PREFIX}.145.9
REF_PROG="./apps/tcp_ipv4 ${RTTO} ${WINSIZE} ${LOSS_UP} ${LOSS_DN} -d tun145 -a ${REF_HOST}"
TEST_PROG="./apps/tcp_ipv4 ${RTTO} ${WINSIZE} -d tun144 -a ${TEST_HOST}"
else
REF_PROG="./apps/tcp_native"
TEST_PROG="./apps/tcp_ipv4 ${RTTO} ${WINSIZE} ${LOSS_UP} ${LOSS_DN} -d tun144 -a ${TEST_HOST}"
fi
else
# UDP mode
REF_PROG="./apps/tcp_udp ${RTTO} ${WINSIZE} ${LOSS_UP} ${LOSS_DN}"
TEST_PROG="./apps/tcp_udp ${RTTO} ${WINSIZE}"
fi
TEST_OUT_FILE=$(mktemp)
TEST_IN_FILE=$(mktemp)
make_test_file "${TEST_IN_FILE}" "${DATASIZE}"
HASH_IN=$(sha256sum ${TEST_IN_FILE} | cut -d \ -f 1)
HASH_OUT2=
case "$RSDMODE" in
S) # test sending
if [ "$CSMODE" = "c" ]; then
ref_listen "${TEST_OUT_FILE}" /dev/null
test_connect /dev/null "${TEST_IN_FILE}"
else
test_listen /dev/null "${TEST_IN_FILE}"
ref_connect "${TEST_OUT_FILE}" /dev/null
fi
;;
R) # test receiving
if [ "$CSMODE" = "c" ]; then
ref_listen /dev/null "${TEST_IN_FILE}"
test_connect "${TEST_OUT_FILE}" /dev/null
else
test_listen "${TEST_OUT_FILE}" /dev/null
ref_connect /dev/null "${TEST_IN_FILE}"
fi
;;
D) # test full-duplex
TEST_OUT2_FILE=$(mktemp)
if [ "$CSMODE" = "c" ]; then
ref_listen "${TEST_OUT_FILE}" "${TEST_IN_FILE}"
test_connect "${TEST_OUT2_FILE}" "${TEST_IN_FILE}"
else
test_listen "${TEST_OUT_FILE}" "${TEST_IN_FILE}"
ref_connect "${TEST_OUT2_FILE}" "${TEST_IN_FILE}"
fi
HASH_OUT2=$(hash_file "${TEST_OUT2_FILE}")
;;
esac
if ! wait; then
echo ERROR: subprocess failed
exit 1
fi
HASH_OUT=$(hash_file ${TEST_OUT_FILE})
if [ ! -z "${HASH_OUT2}" ] && [ "${HASH_OUT}" != "${HASH_OUT2}" ] || [ "${HASH_IN}" != "${HASH_OUT}" ]; then
echo ERROR: "$HASH_IN" neq "$HASH_OUT" or "$HASH_OUT2"
exit 1
fi
exit 0

29
writeups/lab4.md Normal file
View File

@ -0,0 +1,29 @@
Lab 4 Writeup
=============
My name: [your name here]
My SUNet ID: [your sunetid here]
I collaborated with: [list sunetids here]
I would like to thank/reward these classmates for their help: [list sunetids here]
This lab took me about [n] hours to do. I [did/did not] attend the lab session.
Program Structure and Design of the TCPConnection:
[]
Implementation Challenges:
[]
Remaining Bugs:
[]
- Optional: I had unexpected difficulty with: [describe]
- Optional: I think you could make this lab better by: [describe]
- Optional: I was surprised by: [describe]
- Optional: I'm not sure about: [describe]