From 87babd9d6202f4c784fa1ae35d7493dfe81c864e Mon Sep 17 00:00:00 2001 From: Keith Winstein Date: Tue, 28 Sep 2021 16:50:45 -0700 Subject: [PATCH] CS144 Lab checkpoint 2 starter code --- libsponge/tcp_helpers/tcp_header.cc | 108 +++++++++++++++++++++++++++ libsponge/tcp_helpers/tcp_header.hh | 68 +++++++++++++++++ libsponge/tcp_helpers/tcp_segment.cc | 45 +++++++++++ libsponge/tcp_helpers/tcp_segment.hh | 36 +++++++++ libsponge/tcp_helpers/tcp_state.cc | 15 ++++ libsponge/tcp_helpers/tcp_state.hh | 35 +++++++++ libsponge/tcp_receiver.cc | 19 +++++ libsponge/tcp_receiver.hh | 66 ++++++++++++++++ libsponge/wrapping_integers.cc | 34 +++++++++ libsponge/wrapping_integers.hh | 65 ++++++++++++++++ tests/CMakeLists.txt | 11 +++ writeups/lab2.md | 29 +++++++ 12 files changed, 531 insertions(+) create mode 100644 libsponge/tcp_helpers/tcp_header.cc create mode 100644 libsponge/tcp_helpers/tcp_header.hh create mode 100644 libsponge/tcp_helpers/tcp_segment.cc create mode 100644 libsponge/tcp_helpers/tcp_segment.hh create mode 100644 libsponge/tcp_helpers/tcp_state.cc create mode 100644 libsponge/tcp_helpers/tcp_state.hh create mode 100644 libsponge/tcp_receiver.cc create mode 100644 libsponge/tcp_receiver.hh create mode 100644 libsponge/wrapping_integers.cc create mode 100644 libsponge/wrapping_integers.hh create mode 100644 writeups/lab2.md diff --git a/libsponge/tcp_helpers/tcp_header.cc b/libsponge/tcp_helpers/tcp_header.cc new file mode 100644 index 0000000..77209fc --- /dev/null +++ b/libsponge/tcp_helpers/tcp_header.cc @@ -0,0 +1,108 @@ +#include "tcp_header.hh" + +#include + +using namespace std; + +//! \param[in,out] p is a NetParser from which the TCP 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 inside the NetParser is too short to contain a header +//! - the header's `doff` field is shorter than the minimum allowed +//! - there is less data in the header than the `doff` field claims +//! - the checksum is bad +ParseResult TCPHeader::parse(NetParser &p) { + sport = p.u16(); // source port + dport = p.u16(); // destination port + seqno = WrappingInt32{p.u32()}; // sequence number + ackno = WrappingInt32{p.u32()}; // ack number + doff = p.u8() >> 4; // data offset + + const uint8_t fl_b = p.u8(); // byte including flags + urg = static_cast(fl_b & 0b0010'0000); // binary literals and ' digit separator since C++14!!! + ack = static_cast(fl_b & 0b0001'0000); + psh = static_cast(fl_b & 0b0000'1000); + rst = static_cast(fl_b & 0b0000'0100); + syn = static_cast(fl_b & 0b0000'0010); + fin = static_cast(fl_b & 0b0000'0001); + + win = p.u16(); // window size + cksum = p.u16(); // checksum + uptr = p.u16(); // urgent pointer + + if (doff < 5) { + return ParseResult::HeaderTooShort; + } + + // skip any options or anything extra in the header + p.remove_prefix(doff * 4 - TCPHeader::LENGTH); + + if (p.error()) { + return p.get_error(); + } + + return ParseResult::NoError; +} + +//! Serialize the TCPHeader to a string (does not recompute the checksum) +string TCPHeader::serialize() const { + // sanity check + if (doff < 5) { + throw runtime_error("TCP header too short"); + } + + string ret; + ret.reserve(4 * doff); + + NetUnparser::u16(ret, sport); // source port + NetUnparser::u16(ret, dport); // destination port + NetUnparser::u32(ret, seqno.raw_value()); // sequence number + NetUnparser::u32(ret, ackno.raw_value()); // ack number + NetUnparser::u8(ret, doff << 4); // data offset + + const uint8_t fl_b = (urg ? 0b0010'0000 : 0) | (ack ? 0b0001'0000 : 0) | (psh ? 0b0000'1000 : 0) | + (rst ? 0b0000'0100 : 0) | (syn ? 0b0000'0010 : 0) | (fin ? 0b0000'0001 : 0); + NetUnparser::u8(ret, fl_b); // flags + NetUnparser::u16(ret, win); // window size + + NetUnparser::u16(ret, cksum); // checksum + + NetUnparser::u16(ret, uptr); // urgent pointer + + ret.resize(4 * doff); // expand header to advertised size + + return ret; +} + +//! \returns A string with the header's contents +string TCPHeader::to_string() const { + stringstream ss{}; + ss << hex << boolalpha << "TCP source port: " << +sport << '\n' + << "TCP dest port: " << +dport << '\n' + << "TCP seqno: " << seqno << '\n' + << "TCP ackno: " << ackno << '\n' + << "TCP doff: " << +doff << '\n' + << "Flags: urg: " << urg << " ack: " << ack << " psh: " << psh << " rst: " << rst << " syn: " << syn + << " fin: " << fin << '\n' + << "TCP winsize: " << +win << '\n' + << "TCP cksum: " << +cksum << '\n' + << "TCP uptr: " << +uptr << '\n'; + return ss.str(); +} + +string TCPHeader::summary() const { + stringstream ss{}; + ss << "Header(flags=" << (syn ? "S" : "") << (ack ? "A" : "") << (rst ? "R" : "") << (fin ? "F" : "") + << ",seqno=" << seqno << ",ack=" << ackno << ",win=" << win << ")"; + return ss.str(); +} + +bool TCPHeader::operator==(const TCPHeader &other) const { + // TODO(aozdemir) more complete check (right now we omit cksum, src, dst + return seqno == other.seqno && ackno == other.ackno && doff == other.doff && urg == other.urg && ack == other.ack && + psh == other.psh && rst == other.rst && syn == other.syn && fin == other.fin && win == other.win && + uptr == other.uptr; +} diff --git a/libsponge/tcp_helpers/tcp_header.hh b/libsponge/tcp_helpers/tcp_header.hh new file mode 100644 index 0000000..6351f5c --- /dev/null +++ b/libsponge/tcp_helpers/tcp_header.hh @@ -0,0 +1,68 @@ +#ifndef SPONGE_LIBSPONGE_TCP_HEADER_HH +#define SPONGE_LIBSPONGE_TCP_HEADER_HH + +#include "parser.hh" +#include "wrapping_integers.hh" + +//! \brief [TCP](\ref rfc::rfc793) segment header +//! \note TCP options are not supported +struct TCPHeader { + static constexpr size_t LENGTH = 20; //!< [TCP](\ref rfc::rfc793) header length, not including options + + //! \struct TCPHeader + //! ~~~{.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 + //! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //! | Source Port | Destination Port | + //! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //! | Sequence Number | + //! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //! | Acknowledgment Number | + //! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //! | Data | |U|A|P|R|S|F| | + //! | Offset| Reserved |R|C|S|S|Y|I| Window | + //! | | |G|K|H|T|N|N| | + //! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //! | Checksum | Urgent Pointer | + //! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //! | Options | Padding | + //! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //! | data | + //! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + //! ~~~ + + //! \name TCP Header fields + //!@{ + uint16_t sport = 0; //!< source port + uint16_t dport = 0; //!< destination port + WrappingInt32 seqno{0}; //!< sequence number + WrappingInt32 ackno{0}; //!< ack number + uint8_t doff = LENGTH / 4; //!< data offset + bool urg = false; //!< urgent flag + bool ack = false; //!< ack flag + bool psh = false; //!< push flag + bool rst = false; //!< rst flag + bool syn = false; //!< syn flag + bool fin = false; //!< fin flag + uint16_t win = 0; //!< window size + uint16_t cksum = 0; //!< checksum + uint16_t uptr = 0; //!< urgent pointer + //!@} + + //! Parse the TCP fields from the provided NetParser + ParseResult parse(NetParser &p); + + //! Serialize the TCP fields + std::string serialize() 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; + + bool operator==(const TCPHeader &other) const; +}; + +#endif // SPONGE_LIBSPONGE_TCP_HEADER_HH diff --git a/libsponge/tcp_helpers/tcp_segment.cc b/libsponge/tcp_helpers/tcp_segment.cc new file mode 100644 index 0000000..f3bfa71 --- /dev/null +++ b/libsponge/tcp_helpers/tcp_segment.cc @@ -0,0 +1,45 @@ +#include "tcp_segment.hh" + +#include "parser.hh" +#include "util.hh" + +#include + +using namespace std; + +//! \param[in] buffer string/Buffer to be parsed +//! \param[in] datagram_layer_checksum pseudo-checksum from the lower-layer protocol +ParseResult TCPSegment::parse(const Buffer buffer, const uint32_t datagram_layer_checksum) { + InternetChecksum check(datagram_layer_checksum); + check.add(buffer); + if (check.value()) { + return ParseResult::BadChecksum; + } + + NetParser p{buffer}; + _header.parse(p); + _payload = p.buffer(); + return p.get_error(); +} + +size_t TCPSegment::length_in_sequence_space() const { + return payload().str().size() + (header().syn ? 1 : 0) + (header().fin ? 1 : 0); +} + +//! \param[in] datagram_layer_checksum pseudo-checksum from the lower-layer protocol +BufferList TCPSegment::serialize(const uint32_t datagram_layer_checksum) const { + TCPHeader header_out = _header; + header_out.cksum = 0; + + // calculate checksum -- taken over entire segment + InternetChecksum check(datagram_layer_checksum); + check.add(header_out.serialize()); + check.add(_payload); + header_out.cksum = check.value(); + + BufferList ret; + ret.append(header_out.serialize()); + ret.append(_payload); + + return ret; +} diff --git a/libsponge/tcp_helpers/tcp_segment.hh b/libsponge/tcp_helpers/tcp_segment.hh new file mode 100644 index 0000000..9bba855 --- /dev/null +++ b/libsponge/tcp_helpers/tcp_segment.hh @@ -0,0 +1,36 @@ +#ifndef SPONGE_LIBSPONGE_TCP_SEGMENT_HH +#define SPONGE_LIBSPONGE_TCP_SEGMENT_HH + +#include "buffer.hh" +#include "tcp_header.hh" + +#include + +//! \brief [TCP](\ref rfc::rfc793) segment +class TCPSegment { + private: + TCPHeader _header{}; + Buffer _payload{}; + + public: + //! \brief Parse the segment from a string + ParseResult parse(const Buffer buffer, const uint32_t datagram_layer_checksum = 0); + + //! \brief Serialize the segment to a string + BufferList serialize(const uint32_t datagram_layer_checksum = 0) const; + + //! \name Accessors + //!@{ + const TCPHeader &header() const { return _header; } + TCPHeader &header() { return _header; } + + const Buffer &payload() const { return _payload; } + Buffer &payload() { return _payload; } + //!@} + + //! \brief Segment's length in sequence space + //! \note Equal to payload length plus one byte if SYN is set, plus one byte if FIN is set + size_t length_in_sequence_space() const; +}; + +#endif // SPONGE_LIBSPONGE_TCP_SEGMENT_HH diff --git a/libsponge/tcp_helpers/tcp_state.cc b/libsponge/tcp_helpers/tcp_state.cc new file mode 100644 index 0000000..fdded50 --- /dev/null +++ b/libsponge/tcp_helpers/tcp_state.cc @@ -0,0 +1,15 @@ +#include "tcp_state.hh" + +using namespace std; + +string TCPState::state_summary(const TCPReceiver &receiver) { + if (receiver.stream_out().error()) { + return TCPReceiverStateSummary::ERROR; + } else if (not receiver.ackno().has_value()) { + return TCPReceiverStateSummary::LISTEN; + } else if (receiver.stream_out().input_ended()) { + return TCPReceiverStateSummary::FIN_RECV; + } else { + return TCPReceiverStateSummary::SYN_RECV; + } +} diff --git a/libsponge/tcp_helpers/tcp_state.hh b/libsponge/tcp_helpers/tcp_state.hh new file mode 100644 index 0000000..f6ee842 --- /dev/null +++ b/libsponge/tcp_helpers/tcp_state.hh @@ -0,0 +1,35 @@ +#ifndef SPONGE_LIBSPONGE_TCP_STATE +#define SPONGE_LIBSPONGE_TCP_STATE + +#include "tcp_receiver.hh" + +#include + +//! \brief Summary of a TCPConnection's internal state +//! +//! Most TCP implementations have a global per-connection state +//! machine, as described in the [TCP](\ref rfc::rfc793) +//! specification. Sponge is a bit different: we have factored the +//! connection into two independent parts (the sender and the +//! receiver). The TCPSender and TCPReceiver maintain their interval +//! state variables independently (e.g. next_seqno, number of bytes in +//! flight, or whether each stream has ended). There is no notion of a +//! discrete state machine or much overarching state outside the +//! sender and receiver. To test that Sponge follows the TCP spec, we +//! use this class to compare the "official" states with Sponge's +//! sender/receiver states and two variables that belong to the +//! overarching TCPConnection object. +class TCPState { + public: + //! \brief Summarize the state of a TCPReceiver in a string + static std::string state_summary(const TCPReceiver &receiver); +}; + +namespace TCPReceiverStateSummary { +const std::string ERROR = "error (connection was reset)"; +const std::string LISTEN = "waiting for SYN: ackno is empty"; +const std::string SYN_RECV = "SYN received (ackno exists), and input to stream hasn't ended"; +const std::string FIN_RECV = "input to stream has ended"; +} // namespace TCPReceiverStateSummary + +#endif // SPONGE_LIBSPONGE_TCP_STATE diff --git a/libsponge/tcp_receiver.cc b/libsponge/tcp_receiver.cc new file mode 100644 index 0000000..4f0ee81 --- /dev/null +++ b/libsponge/tcp_receiver.cc @@ -0,0 +1,19 @@ +#include "tcp_receiver.hh" + +// Dummy implementation of a TCP receiver + +// For Lab 2, please replace with a real implementation that passes the +// automated checks run by `make check_lab2`. + +template +void DUMMY_CODE(Targs &&... /* unused */) {} + +using namespace std; + +void TCPReceiver::segment_received(const TCPSegment &seg) { + DUMMY_CODE(seg); +} + +optional TCPReceiver::ackno() const { return {}; } + +size_t TCPReceiver::window_size() const { return {}; } diff --git a/libsponge/tcp_receiver.hh b/libsponge/tcp_receiver.hh new file mode 100644 index 0000000..0856a3f --- /dev/null +++ b/libsponge/tcp_receiver.hh @@ -0,0 +1,66 @@ +#ifndef SPONGE_LIBSPONGE_TCP_RECEIVER_HH +#define SPONGE_LIBSPONGE_TCP_RECEIVER_HH + +#include "byte_stream.hh" +#include "stream_reassembler.hh" +#include "tcp_segment.hh" +#include "wrapping_integers.hh" + +#include + +//! \brief The "receiver" part of a TCP implementation. + +//! Receives and reassembles segments into a ByteStream, and computes +//! the acknowledgment number and window size to advertise back to the +//! remote TCPSender. +class TCPReceiver { + //! Our data structure for re-assembling bytes. + StreamReassembler _reassembler; + + //! The maximum number of bytes we'll store. + size_t _capacity; + + public: + //! \brief Construct a TCP receiver + //! + //! \param capacity the maximum number of bytes that the receiver will + //! store in its buffers at any give time. + TCPReceiver(const size_t capacity) : _reassembler(capacity), _capacity(capacity) {} + + //! \name Accessors to provide feedback to the remote TCPSender + //!@{ + + //! \brief The ackno that should be sent to the peer + //! \returns empty if no SYN has been received + //! + //! This is the beginning of the receiver's window, or in other words, the sequence number + //! of the first byte in the stream that the receiver hasn't received. + std::optional ackno() const; + + //! \brief The window size that should be sent to the peer + //! + //! Operationally: the capacity minus the number of bytes that the + //! TCPReceiver is holding in its byte stream (those that have been + //! reassembled, but not consumed). + //! + //! Formally: the difference between (a) the sequence number of + //! the first byte that falls after the window (and will not be + //! accepted by the receiver) and (b) the sequence number of the + //! beginning of the window (the ackno). + size_t window_size() const; + //!@} + + //! \brief number of bytes stored but not yet reassembled + size_t unassembled_bytes() const { return _reassembler.unassembled_bytes(); } + + //! \brief handle an inbound segment + void segment_received(const TCPSegment &seg); + + //! \name "Output" interface for the reader + //!@{ + ByteStream &stream_out() { return _reassembler.stream_out(); } + const ByteStream &stream_out() const { return _reassembler.stream_out(); } + //!@} +}; + +#endif // SPONGE_LIBSPONGE_TCP_RECEIVER_HH diff --git a/libsponge/wrapping_integers.cc b/libsponge/wrapping_integers.cc new file mode 100644 index 0000000..7176532 --- /dev/null +++ b/libsponge/wrapping_integers.cc @@ -0,0 +1,34 @@ +#include "wrapping_integers.hh" + +// Dummy implementation of a 32-bit wrapping integer + +// For Lab 2, please replace with a real implementation that passes the +// automated checks run by `make check_lab2`. + +template +void DUMMY_CODE(Targs &&... /* unused */) {} + +using namespace std; + +//! Transform an "absolute" 64-bit sequence number (zero-indexed) into a WrappingInt32 +//! \param n The input absolute 64-bit sequence number +//! \param isn The initial sequence number +WrappingInt32 wrap(uint64_t n, WrappingInt32 isn) { + DUMMY_CODE(n, isn); + return WrappingInt32{0}; +} + +//! Transform a WrappingInt32 into an "absolute" 64-bit sequence number (zero-indexed) +//! \param n The relative sequence number +//! \param isn The initial sequence number +//! \param checkpoint A recent absolute 64-bit sequence number +//! \returns the 64-bit sequence number that wraps to `n` and is closest to `checkpoint` +//! +//! \note Each of the two streams of the TCP connection has its own ISN. One stream +//! runs from the local TCPSender to the remote TCPReceiver and has one ISN, +//! and the other stream runs from the remote TCPSender to the local TCPReceiver and +//! has a different ISN. +uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint) { + DUMMY_CODE(n, isn, checkpoint); + return {}; +} diff --git a/libsponge/wrapping_integers.hh b/libsponge/wrapping_integers.hh new file mode 100644 index 0000000..211a4e1 --- /dev/null +++ b/libsponge/wrapping_integers.hh @@ -0,0 +1,65 @@ +#ifndef SPONGE_LIBSPONGE_WRAPPING_INTEGERS_HH +#define SPONGE_LIBSPONGE_WRAPPING_INTEGERS_HH + +#include +#include + +//! \brief A 32-bit integer, expressed relative to an arbitrary initial sequence number (ISN) +//! \note This is used to express TCP sequence numbers (seqno) and acknowledgment numbers (ackno) +class WrappingInt32 { + private: + uint32_t _raw_value; //!< The raw 32-bit stored integer + + public: + //! Construct from a raw 32-bit unsigned integer + explicit WrappingInt32(uint32_t raw_value) : _raw_value(raw_value) {} + + uint32_t raw_value() const { return _raw_value; } //!< Access raw stored value +}; + +//! Transform a 64-bit absolute sequence number (zero-indexed) into a 32-bit relative sequence number +//! \param n the absolute sequence number +//! \param isn the initial sequence number +//! \returns the relative sequence number +WrappingInt32 wrap(uint64_t n, WrappingInt32 isn); + +//! Transform a 32-bit relative sequence number into a 64-bit absolute sequence number (zero-indexed) +//! \param n The relative sequence number +//! \param isn The initial sequence number +//! \param checkpoint A recent absolute sequence number +//! \returns the absolute sequence number that wraps to `n` and is closest to `checkpoint` +//! +//! \note Each of the two streams of the TCP connection has its own ISN. One stream +//! runs from the local TCPSender to the remote TCPReceiver and has one ISN, +//! and the other stream runs from the remote TCPSender to the local TCPReceiver and +//! has a different ISN. +uint64_t unwrap(WrappingInt32 n, WrappingInt32 isn, uint64_t checkpoint); + +//! \name Helper functions +//!@{ + +//! \brief The offset of `a` relative to `b` +//! \param b the starting point +//! \param a the ending point +//! \returns the number of increments needed to get from `b` to `a`, +//! negative if the number of decrements needed is less than or equal to +//! the number of increments +inline int32_t operator-(WrappingInt32 a, WrappingInt32 b) { return a.raw_value() - b.raw_value(); } + +//! \brief Whether the two integers are equal. +inline bool operator==(WrappingInt32 a, WrappingInt32 b) { return a.raw_value() == b.raw_value(); } + +//! \brief Whether the two integers are not equal. +inline bool operator!=(WrappingInt32 a, WrappingInt32 b) { return !(a == b); } + +//! \brief Serializes the wrapping integer, `a`. +inline std::ostream &operator<<(std::ostream &os, WrappingInt32 a) { return os << a.raw_value(); } + +//! \brief The point `b` steps past `a`. +inline WrappingInt32 operator+(WrappingInt32 a, uint32_t b) { return WrappingInt32{a.raw_value() + b}; } + +//! \brief The point `b` steps before `a`. +inline WrappingInt32 operator-(WrappingInt32 a, uint32_t b) { return a + -b; } +//!@} + +#endif // SPONGE_LIBSPONGE_WRAPPING_INTEGERS_HH diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e5ef3ba..477d607 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,6 +6,7 @@ macro (add_test_exec exec_name) target_link_libraries ("${exec_name}" sponge ${ARGN}) endmacro (add_test_exec) +add_test_exec (tcp_parser ${LIBPCAP}) add_test_exec (fsm_stream_reassembler_single) add_test_exec (fsm_stream_reassembler_seq) add_test_exec (fsm_stream_reassembler_dup) @@ -19,3 +20,13 @@ 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) +add_test_exec (recv_reorder) +add_test_exec (recv_close) +add_test_exec (recv_special) diff --git a/writeups/lab2.md b/writeups/lab2.md new file mode 100644 index 0000000..0519b54 --- /dev/null +++ b/writeups/lab2.md @@ -0,0 +1,29 @@ +Lab 2 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 TCPReceiver and wrap/unwrap routines: +[] + +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]