From 9d00589b3c2556544fe0e1e32a241540f116c7e4 Mon Sep 17 00:00:00 2001 From: Keith Winstein Date: Tue, 19 Oct 2021 17:24:47 -0700 Subject: [PATCH] CS144 Lab checkpoint 4 starter code --- apps/CMakeLists.txt | 8 + apps/bidirectional_stream_copy.cc | 91 +++++++ apps/bidirectional_stream_copy.hh | 9 + apps/tcp_benchmark.cc | 116 ++++++++ apps/tcp_ipv4.cc | 162 +++++++++++ apps/tcp_native.cc | 43 +++ apps/tcp_udp.cc | 136 ++++++++++ apps/tun.cc | 57 ++++ apps/udp_tcpdump.cc | 293 ++++++++++++++++++++ libsponge/tcp_connection.cc | 49 ++++ libsponge/tcp_connection.hh | 99 +++++++ libsponge/tcp_helpers/fd_adapter.cc | 57 ++++ libsponge/tcp_helpers/fd_adapter.hh | 70 +++++ libsponge/tcp_helpers/ipv4_datagram.cc | 41 +++ libsponge/tcp_helpers/ipv4_datagram.hh | 32 +++ libsponge/tcp_helpers/ipv4_header.cc | 159 +++++++++++ libsponge/tcp_helpers/ipv4_header.hh | 71 +++++ libsponge/tcp_helpers/lossy_fd_adapter.hh | 72 +++++ libsponge/tcp_helpers/tcp_config.hh | 2 +- libsponge/tcp_helpers/tcp_over_ip.cc | 90 +++++++ libsponge/tcp_helpers/tcp_over_ip.hh | 19 ++ libsponge/tcp_helpers/tcp_sponge_socket.cc | 295 +++++++++++++++++++++ libsponge/tcp_helpers/tcp_sponge_socket.hh | 129 +++++++++ libsponge/tcp_helpers/tcp_state.cc | 77 ++++++ libsponge/tcp_helpers/tcp_state.hh | 34 +++ libsponge/tcp_helpers/tuntap_adapter.cc | 6 + libsponge/tcp_helpers/tuntap_adapter.hh | 42 +++ tests/CMakeLists.txt | 25 +- tests/ipv4_parser.data | Bin 0 -> 131234 bytes tun.sh | 111 ++++++++ txrx.sh | 238 +++++++++++++++++ writeups/lab4.md | 29 ++ 32 files changed, 2655 insertions(+), 7 deletions(-) create mode 100644 apps/bidirectional_stream_copy.cc create mode 100644 apps/bidirectional_stream_copy.hh create mode 100644 apps/tcp_benchmark.cc create mode 100644 apps/tcp_ipv4.cc create mode 100644 apps/tcp_native.cc create mode 100644 apps/tcp_udp.cc create mode 100644 apps/tun.cc create mode 100644 apps/udp_tcpdump.cc create mode 100644 libsponge/tcp_connection.cc create mode 100644 libsponge/tcp_connection.hh create mode 100644 libsponge/tcp_helpers/fd_adapter.cc create mode 100644 libsponge/tcp_helpers/fd_adapter.hh create mode 100644 libsponge/tcp_helpers/ipv4_datagram.cc create mode 100644 libsponge/tcp_helpers/ipv4_datagram.hh create mode 100644 libsponge/tcp_helpers/ipv4_header.cc create mode 100644 libsponge/tcp_helpers/ipv4_header.hh create mode 100644 libsponge/tcp_helpers/lossy_fd_adapter.hh create mode 100644 libsponge/tcp_helpers/tcp_over_ip.cc create mode 100644 libsponge/tcp_helpers/tcp_over_ip.hh create mode 100644 libsponge/tcp_helpers/tcp_sponge_socket.cc create mode 100644 libsponge/tcp_helpers/tcp_sponge_socket.hh create mode 100644 libsponge/tcp_helpers/tuntap_adapter.cc create mode 100644 libsponge/tcp_helpers/tuntap_adapter.hh create mode 100644 tests/ipv4_parser.data create mode 100755 tun.sh create mode 100755 txrx.sh create mode 100644 writeups/lab4.md 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 0000000000000000000000000000000000000000..20ac2400add583047a2606141727982bfc93700b GIT binary patch literal 131234 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%0Dly_Ii$qE}IhWag->&R_mEbQKa?aP(UGtGS}! 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 TU9$kSe2bRLV&2z>4ov+4x26U{ literal 0 HcmV?d00001 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]