diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt index e77e5fb..b372bdb 100644 --- a/apps/CMakeLists.txt +++ b/apps/CMakeLists.txt @@ -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) diff --git a/apps/bidirectional_stream_copy.cc b/apps/bidirectional_stream_copy.cc new file mode 100644 index 0000000..ee9794b --- /dev/null +++ b/apps/bidirectional_stream_copy.cc @@ -0,0 +1,91 @@ +#include "bidirectional_stream_copy.hh" + +#include "byte_stream.hh" +#include "eventloop.hh" + +#include +#include +#include + +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; + } + } +} diff --git a/apps/bidirectional_stream_copy.hh b/apps/bidirectional_stream_copy.hh new file mode 100644 index 0000000..13d8892 --- /dev/null +++ b/apps/bidirectional_stream_copy.hh @@ -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 diff --git a/apps/tcp_benchmark.cc b/apps/tcp_benchmark.cc new file mode 100644 index 0000000..b39c723 --- /dev/null +++ b/apps/tcp_benchmark.cc @@ -0,0 +1,116 @@ +#include "tcp_connection.hh" + +#include +#include +#include +#include +#include + +using namespace std; +using namespace std::chrono; + +constexpr size_t len = 100 * 1024 * 1024; + +void move_segments(TCPConnection &x, TCPConnection &y, vector &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 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(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; +} diff --git a/apps/tcp_ipv4.cc b/apps/tcp_ipv4.cc new file mode 100644 index 0000000..fc8e819 --- /dev/null +++ b/apps/tcp_ipv4.cc @@ -0,0 +1,162 @@ +#include "bidirectional_stream_copy.hh" +#include "tcp_config.hh" +#include "tcp_sponge_socket.hh" +#include "tun.hh" + +#include +#include +#include +#include +#include +#include +#include + +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] \n\n" + << " Option Default\n" + << " -- --\n\n" + + << " -l Server (listen) mode. (client mode)\n" + << " In server mode, : is the address to bind.\n\n" + + << " -a Set source address (client mode only) " << LOCAL_ADDRESS_DFLT << "\n" + << " -s Set source port (client mode only) (random)\n\n" + + << " -w Use a window of bytes " << TCPConfig::MAX_PAYLOAD_SIZE + << "\n\n" + + << " -t Set rt_timeout to tmout " << TCPConfig::TIMEOUT_DFLT << "\n\n" + + << " -d Connect to tun " << TUN_DFLT << "\n\n" + + << " -Lu Set uplink loss to (float in 0..1) (no loss)\n" + << " -Ld Set downlink loss to (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 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(static_cast(numeric_limits::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(static_cast(numeric_limits::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; +} diff --git a/apps/tcp_native.cc b/apps/tcp_native.cc new file mode 100644 index 0000000..da9024b --- /dev/null +++ b/apps/tcp_native.cc @@ -0,0 +1,43 @@ +#include "bidirectional_stream_copy.hh" + +#include +#include +#include + +using namespace std; + +void show_usage(const char *argv0) { + cerr << "Usage: " << argv0 << " [-l] \n\n" + << " -l specifies listen mode; : 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; +} diff --git a/apps/tcp_udp.cc b/apps/tcp_udp.cc new file mode 100644 index 0000000..54d7351 --- /dev/null +++ b/apps/tcp_udp.cc @@ -0,0 +1,136 @@ +#include "bidirectional_stream_copy.hh" +#include "tcp_config.hh" +#include "tcp_sponge_socket.hh" + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +constexpr uint16_t DPORT_DFLT = 1440; + +static void show_usage(const char *argv0, const char *msg) { + cout << "Usage: " << argv0 << " [options] \n\n" + + << " Option Default\n" + << " -- --\n\n" + + << " -l Server (listen) mode. (client mode)\n" + << " In server mode, : is the address to bind.\n\n" + + << " -w Use a window of bytes " << TCPConfig::MAX_PAYLOAD_SIZE + << "\n\n" + + << " -t Set rt_timeout to tmout " << TCPConfig::TIMEOUT_DFLT << "\n\n" + + << " -Lu Set uplink loss to (float in 0..1) (no loss)\n" + << " -Ld Set downlink loss to (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 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(static_cast(numeric_limits::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(static_cast(numeric_limits::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; +} diff --git a/apps/tun.cc b/apps/tun.cc new file mode 100644 index 0000000..e12e7b4 --- /dev/null +++ b/apps/tun.cc @@ -0,0 +1,57 @@ +#include "tun.hh" + +#include "ipv4_datagram.hh" +#include "parser.hh" +#include "tcp_segment.hh" +#include "util.hh" + +#include +#include +#include +#include + +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; +} diff --git a/apps/udp_tcpdump.cc b/apps/udp_tcpdump.cc new file mode 100644 index 0000000..a3bdfbb --- /dev/null +++ b/apps/udp_tcpdump.cc @@ -0,0 +1,293 @@ +#include "parser.hh" +#include "tcp_header.hh" +#include "tcp_segment.hh" +#include "util.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +static void show_usage(const char *arg0, const char *errmsg) { + cout << "Usage: " << arg0 << " [-i ] [-F ] [-h|--help] \n\n" + << " -i only capture packets from (default: all)\n\n" + + << " -F reads in a filter expression from \n" + << " is ignored if -F is supplied.\n\n" + + << " -h, --help show this message\n\n" + << " 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(data); + if (inet_ntop(AF_INET, addr, static_cast(addrbuf), 128) == nullptr) { + return "unknown"; + } + return string(static_cast(addrbuf)); +} + +static string inet6_addr(const uint8_t *data) { + char addrbuf[128]; + auto *addr = reinterpret_cast(data); + if (inet_ntop(AF_INET6, addr, static_cast(addrbuf), 128) == nullptr) { + return "unknown"; + } + return string(static_cast(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(errbuf)); + if (p_hdl == nullptr) { + cout << "\nError initiating capture: " << static_cast(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(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; +} diff --git a/libsponge/tcp_connection.cc b/libsponge/tcp_connection.cc new file mode 100644 index 0000000..aab4055 --- /dev/null +++ b/libsponge/tcp_connection.cc @@ -0,0 +1,49 @@ +#include "tcp_connection.hh" + +#include + +// 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 +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; + } +} diff --git a/libsponge/tcp_connection.hh b/libsponge/tcp_connection.hh new file mode 100644 index 0000000..b8a907d --- /dev/null +++ b/libsponge/tcp_connection.hh @@ -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 _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 &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 diff --git a/libsponge/tcp_helpers/fd_adapter.cc b/libsponge/tcp_helpers/fd_adapter.cc new file mode 100644 index 0000000..16c8d4a --- /dev/null +++ b/libsponge/tcp_helpers/fd_adapter.cc @@ -0,0 +1,57 @@ +#include "fd_adapter.hh" + +#include +#include +#include + +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 that is empty if the segment was invalid or unrelated +optional 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; diff --git a/libsponge/tcp_helpers/fd_adapter.hh b/libsponge/tcp_helpers/fd_adapter.hh new file mode 100644 index 0000000..6ee779b --- /dev/null +++ b/libsponge/tcp_helpers/fd_adapter.hh @@ -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 +#include + +//! \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 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; + +#endif // SPONGE_LIBSPONGE_FD_ADAPTER_HH diff --git a/libsponge/tcp_helpers/ipv4_datagram.cc b/libsponge/tcp_helpers/ipv4_datagram.cc new file mode 100644 index 0000000..9839485 --- /dev/null +++ b/libsponge/tcp_helpers/ipv4_datagram.cc @@ -0,0 +1,41 @@ +#include "ipv4_datagram.hh" + +#include "parser.hh" +#include "util.hh" + +#include +#include + +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; +} diff --git a/libsponge/tcp_helpers/ipv4_datagram.hh b/libsponge/tcp_helpers/ipv4_datagram.hh new file mode 100644 index 0000000..e86232f --- /dev/null +++ b/libsponge/tcp_helpers/ipv4_datagram.hh @@ -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 diff --git a/libsponge/tcp_helpers/ipv4_header.cc b/libsponge/tcp_helpers/ipv4_header.cc new file mode 100644 index 0000000..899b203 --- /dev/null +++ b/libsponge/tcp_helpers/ipv4_header.cc @@ -0,0 +1,159 @@ +#include "ipv4_header.hh" + +#include "util.hh" + +#include +#include +#include + +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(fo_val & 0x4000); // don't fragment + mf = static_cast(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(); +} diff --git a/libsponge/tcp_helpers/ipv4_header.hh b/libsponge/tcp_helpers/ipv4_header.hh new file mode 100644 index 0000000..b9eaf83 --- /dev/null +++ b/libsponge/tcp_helpers/ipv4_header.hh @@ -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 diff --git a/libsponge/tcp_helpers/lossy_fd_adapter.hh b/libsponge/tcp_helpers/lossy_fd_adapter.hh new file mode 100644 index 0000000..e76f933 --- /dev/null +++ b/libsponge/tcp_helpers/lossy_fd_adapter.hh @@ -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 +#include +#include + +//! An adapter class that adds random dropping behavior to an FD adapter +template +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 that is empty if the segment was dropped or if + //! the underlying AdapterT returned an empty value + std::optional 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 diff --git a/libsponge/tcp_helpers/tcp_config.hh b/libsponge/tcp_helpers/tcp_config.hh index 65542c7..02662d5 100644 --- a/libsponge/tcp_helpers/tcp_config.hh +++ b/libsponge/tcp_helpers/tcp_config.hh @@ -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 diff --git a/libsponge/tcp_helpers/tcp_over_ip.cc b/libsponge/tcp_helpers/tcp_over_ip.cc new file mode 100644 index 0000000..ca028c6 --- /dev/null +++ b/libsponge/tcp_helpers/tcp_over_ip.cc @@ -0,0 +1,90 @@ +#include "tcp_over_ip.hh" + +#include "ipv4_datagram.hh" +#include "ipv4_header.hh" +#include "parser.hh" + +#include +#include +#include +#include + +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 that is empty if the segment was invalid or unrelated +optional 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; +} diff --git a/libsponge/tcp_helpers/tcp_over_ip.hh b/libsponge/tcp_helpers/tcp_over_ip.hh new file mode 100644 index 0000000..2d9ab4e --- /dev/null +++ b/libsponge/tcp_helpers/tcp_over_ip.hh @@ -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 + +//! \brief A converter from TCP segments to serialized IPv4 datagrams +class TCPOverIPv4Adapter : public FdAdapterBase { + public: + std::optional unwrap_tcp_in_ip(const InternetDatagram &ip_dgram); + + InternetDatagram wrap_tcp_in_ip(TCPSegment &seg); +}; + +#endif // SPONGE_LIBSPONGE_TCP_OVER_IP_HH diff --git a/libsponge/tcp_helpers/tcp_sponge_socket.cc b/libsponge/tcp_helpers/tcp_sponge_socket.cc new file mode 100644 index 0000000..0e3e64d --- /dev/null +++ b/libsponge/tcp_helpers/tcp_sponge_socket.cc @@ -0,0 +1,295 @@ +#include "tcp_sponge_socket.hh" + +#include "parser.hh" +#include "tun.hh" +#include "util.hh" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +static constexpr size_t TCP_TICK_MS = 10; + +//! \param[in] condition is a function returning true if loop should continue +template +void TCPSpongeSocket::_tcp_loop(const function &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 +TCPSpongeSocket::TCPSpongeSocket(pair 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 +void TCPSpongeSocket::_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 socket_pair_helper(const int type) { + int fds[2]; + SystemCall("socketpair", ::socketpair(AF_UNIX, type, 0, static_cast(fds))); + return {FileDescriptor(fds[0]), FileDescriptor(fds[1])}; +} + +//! \param[in] datagram_interface is the underlying interface (e.g. to UDP, IP, or Ethernet) +template +TCPSpongeSocket::TCPSpongeSocket(AdaptT &&datagram_interface) + : TCPSpongeSocket(socket_pair_helper(SOCK_STREAM), move(datagram_interface)) {} + +template +TCPSpongeSocket::~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 +void TCPSpongeSocket::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 +void TCPSpongeSocket::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 +void TCPSpongeSocket::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 +void TCPSpongeSocket::_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; + +//! Specialization of TCPSpongeSocket for TCPOverIPv4OverTunFdAdapter +template class TCPSpongeSocket; + +//! Specialization of TCPSpongeSocket for LossyTCPOverUDPSocketAdapter +template class TCPSpongeSocket; + +//! Specialization of TCPSpongeSocket for LossyTCPOverIPv4OverTunFdAdapter +template class TCPSpongeSocket; + +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); +} diff --git a/libsponge/tcp_helpers/tcp_sponge_socket.hh b/libsponge/tcp_helpers/tcp_sponge_socket.hh new file mode 100644 index 0000000..606d04d --- /dev/null +++ b/libsponge/tcp_helpers/tcp_sponge_socket.hh @@ -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 +#include +#include +#include +#include + +//! Multithreaded wrapper around TCPConnection that approximates the Unix sockets API +template +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 _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 &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 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; +using TCPOverIPv4SpongeSocket = TCPSpongeSocket; +using LossyTCPOverUDPSpongeSocket = TCPSpongeSocket; +using LossyTCPOverIPv4SpongeSocket = TCPSpongeSocket; + +//! \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 diff --git a/libsponge/tcp_helpers/tcp_state.cc b/libsponge/tcp_helpers/tcp_state.cc index 4840695..1488fe2 100644 --- a/libsponge/tcp_helpers/tcp_state.cc +++ b/libsponge/tcp_helpers/tcp_state.cc @@ -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; diff --git a/libsponge/tcp_helpers/tcp_state.hh b/libsponge/tcp_helpers/tcp_state.hh index 4489e23..b7d37c0 100644 --- a/libsponge/tcp_helpers/tcp_state.hh +++ b/libsponge/tcp_helpers/tcp_state.hh @@ -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); diff --git a/libsponge/tcp_helpers/tuntap_adapter.cc b/libsponge/tcp_helpers/tuntap_adapter.cc new file mode 100644 index 0000000..e171b13 --- /dev/null +++ b/libsponge/tcp_helpers/tuntap_adapter.cc @@ -0,0 +1,6 @@ +#include "tuntap_adapter.hh" + +using namespace std; + +//! Specialize LossyFdAdapter to TCPOverIPv4OverTunFdAdapter +template class LossyFdAdapter; diff --git a/libsponge/tcp_helpers/tuntap_adapter.hh b/libsponge/tcp_helpers/tuntap_adapter.hh new file mode 100644 index 0000000..b6506a5 --- /dev/null +++ b/libsponge/tcp_helpers/tuntap_adapter.hh @@ -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 +#include +#include + +//! \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 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; + +#endif // SPONGE_LIBSPONGE_TUNFD_ADAPTER_HH diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0fff79e..e6e33ba 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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) diff --git a/tests/ipv4_parser.data b/tests/ipv4_parser.data new file mode 100644 index 0000000..20ac240 Binary files /dev/null and b/tests/ipv4_parser.data differ diff --git a/tun.sh b/tun.sh new file mode 100755 index 0000000..0c46eb5 --- /dev/null +++ b/tun.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +show_usage () { + echo "Usage: $0 [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" "$@" diff --git a/txrx.sh b/txrx.sh new file mode 100755 index 0000000..13e1b93 --- /dev/null +++ b/txrx.sh @@ -0,0 +1,238 @@ +#!/bin/bash + +show_usage() { + echo "Usage: $0 <-i|-u> <-c|-s> <-R|-S|-D> [-n|-o]" + echo " [-t ] [-d ] [-w ] [-l ] [-L ]" + 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 Set rtto to ms 12" + echo " -d Set total transfer size to 32" + echo " -w Set window size to 1452" + echo + echo " -l Set downlink loss to (float in 0..1) 0" + echo " -L Set uplink loss to (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 diff --git a/writeups/lab4.md b/writeups/lab4.md new file mode 100644 index 0000000..2d3d940 --- /dev/null +++ b/writeups/lab4.md @@ -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]