diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt index b372bdb..ee86fbb 100644 --- a/apps/CMakeLists.txt +++ b/apps/CMakeLists.txt @@ -5,5 +5,6 @@ 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 (tcp_ip_ethernet stream_copy) add_sponge_exec (webget) add_sponge_exec (tcp_benchmark) diff --git a/apps/tcp_ip_ethernet.cc b/apps/tcp_ip_ethernet.cc new file mode 100644 index 0000000..d19c199 --- /dev/null +++ b/apps/tcp_ip_ethernet.cc @@ -0,0 +1,142 @@ +#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 *TAP_DFLT = "tap10"; +const string LOCAL_ADDRESS_DFLT = "169.254.10.9"; +const string GATEWAY_DFLT = "169.254.10.1"; + +static void show_usage(const char *argv0, const char *msg) { + cout << "Usage: " << argv0 << " [options] \n\n" + << " Option Default\n" + << " -- --\n\n" + + << " -a Set IP source address (client mode only) " << LOCAL_ADDRESS_DFLT << "\n" + << " -s Set TCP source port (client mode only) (random)\n\n" + << " -n Set IP next-hop address " << GATEWAY_DFLT << "\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 tap " << TAP_DFLT << "\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{}; + string tapdev = TAP_DFLT; + + int curr = 1; + + string source_address = LOCAL_ADDRESS_DFLT; + string source_port = to_string(uint16_t(random_device()())); + string next_hop_address = GATEWAY_DFLT; + + while (argc - curr > 2) { + 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("-n", argv[curr], 3) == 0) { + check_argc(argc, argv, curr, "ERROR: -n requires one argument."); + next_hop_address = 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."); + tapdev = argv[curr + 1]; + 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 + c_filt.destination = {argv[curr], argv[curr + 1]}; + c_filt.source = {source_address, source_port}; + + Address next_hop{next_hop_address, "0"}; + + return make_tuple(c_fsm, c_filt, next_hop, tapdev); +} + +int main(int argc, char **argv) { + try { + if (argc < 3) { + show_usage(argv[0], "ERROR: required arguments are missing."); + return EXIT_FAILURE; + } + + // choose a random local Ethernet address (and make sure it's private, i.e. not owned by a manufacturer) + EthernetAddress local_ethernet_address; + for (auto &byte : local_ethernet_address) { + byte = random_device()(); // use a random local Ethernet address + } + local_ethernet_address.at(0) |= 0x02; // "10" in last two binary digits marks a private Ethernet address + local_ethernet_address.at(0) &= 0xfe; + + auto [c_fsm, c_filt, next_hop, tap_dev_name] = get_config(argc, argv); + + TCPOverIPv4OverEthernetSpongeSocket tcp_socket(TCPOverIPv4OverEthernetAdapter( + TCPOverIPv4OverEthernetAdapter(TapFD(tap_dev_name), local_ethernet_address, c_filt.source, next_hop))); + + 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/libsponge/network_interface.cc b/libsponge/network_interface.cc new file mode 100644 index 0000000..202b1f3 --- /dev/null +++ b/libsponge/network_interface.cc @@ -0,0 +1,46 @@ +#include "network_interface.hh" + +#include "arp_message.hh" +#include "ethernet_frame.hh" + +#include + +// Dummy implementation of a network interface +// Translates from {IP datagram, next hop address} to link-layer frame, and from link-layer frame to IP datagram + +// For Lab 5, please replace with a real implementation that passes the +// automated checks run by `make check_lab5`. + +// You will need to add private members to the class declaration in `network_interface.hh` + +template +void DUMMY_CODE(Targs &&... /* unused */) {} + +using namespace std; + +//! \param[in] ethernet_address Ethernet (what ARP calls "hardware") address of the interface +//! \param[in] ip_address IP (what ARP calls "protocol") address of the interface +NetworkInterface::NetworkInterface(const EthernetAddress ðernet_address, const Address &ip_address) + : _ethernet_address(ethernet_address), _ip_address(ip_address) { + cerr << "DEBUG: Network interface has Ethernet address " << to_string(_ethernet_address) << " and IP address " + << ip_address.ip() << "\n"; +} + +//! \param[in] dgram the IPv4 datagram to be sent +//! \param[in] next_hop the IP address of the interface to send it to (typically a router or default gateway, but may also be another host if directly connected to the same network as the destination) +//! (Note: the Address type can be converted to a uint32_t (raw 32-bit IP address) with the Address::ipv4_numeric() method.) +void NetworkInterface::send_datagram(const InternetDatagram &dgram, const Address &next_hop) { + // convert IP address of next hop to raw 32-bit representation (used in ARP header) + const uint32_t next_hop_ip = next_hop.ipv4_numeric(); + + DUMMY_CODE(dgram, next_hop, next_hop_ip); +} + +//! \param[in] frame the incoming Ethernet frame +optional NetworkInterface::recv_frame(const EthernetFrame &frame) { + DUMMY_CODE(frame); + return {}; +} + +//! \param[in] ms_since_last_tick the number of milliseconds since the last call to this method +void NetworkInterface::tick(const size_t ms_since_last_tick) { DUMMY_CODE(ms_since_last_tick); } diff --git a/libsponge/network_interface.hh b/libsponge/network_interface.hh new file mode 100644 index 0000000..87a153f --- /dev/null +++ b/libsponge/network_interface.hh @@ -0,0 +1,67 @@ +#ifndef SPONGE_LIBSPONGE_NETWORK_INTERFACE_HH +#define SPONGE_LIBSPONGE_NETWORK_INTERFACE_HH + +#include "ethernet_frame.hh" +#include "tcp_over_ip.hh" +#include "tun.hh" + +#include +#include + +//! \brief A "network interface" that connects IP (the internet layer, or network layer) +//! with Ethernet (the network access layer, or link layer). + +//! This module is the lowest layer of a TCP/IP stack +//! (connecting IP with the lower-layer network protocol, +//! e.g. Ethernet). But the same module is also used repeatedly +//! as part of a router: a router generally has many network +//! interfaces, and the router's job is to route Internet datagrams +//! between the different interfaces. + +//! The network interface translates datagrams (coming from the +//! "customer," e.g. a TCP/IP stack or router) into Ethernet +//! frames. To fill in the Ethernet destination address, it looks up +//! the Ethernet address of the next IP hop of each datagram, making +//! requests with the [Address Resolution Protocol](\ref rfc::rfc826). +//! In the opposite direction, the network interface accepts Ethernet +//! frames, checks if they are intended for it, and if so, processes +//! the the payload depending on its type. If it's an IPv4 datagram, +//! the network interface passes it up the stack. If it's an ARP +//! request or reply, the network interface processes the frame +//! and learns or replies as necessary. +class NetworkInterface { + private: + //! Ethernet (known as hardware, network-access-layer, or link-layer) address of the interface + EthernetAddress _ethernet_address; + + //! IP (known as internet-layer or network-layer) address of the interface + Address _ip_address; + + //! outbound queue of Ethernet frames that the NetworkInterface wants sent + std::queue _frames_out{}; + + public: + //! \brief Construct a network interface with given Ethernet (network-access-layer) and IP (internet-layer) addresses + NetworkInterface(const EthernetAddress ðernet_address, const Address &ip_address); + + //! \brief Access queue of Ethernet frames awaiting transmission + std::queue &frames_out() { return _frames_out; } + + //! \brief Sends an IPv4 datagram, encapsulated in an Ethernet frame (if it knows the Ethernet destination address). + + //! Will need to use [ARP](\ref rfc::rfc826) to look up the Ethernet destination address for the next hop + //! ("Sending" is accomplished by pushing the frame onto the frames_out queue.) + void send_datagram(const InternetDatagram &dgram, const Address &next_hop); + + //! \brief Receives an Ethernet frame and responds appropriately. + + //! If type is IPv4, returns the datagram. + //! If type is ARP request, learn a mapping from the "sender" fields, and send an ARP reply. + //! If type is ARP reply, learn a mapping from the "sender" fields. + std::optional recv_frame(const EthernetFrame &frame); + + //! \brief Called periodically when time elapses + void tick(const size_t ms_since_last_tick); +}; + +#endif // SPONGE_LIBSPONGE_NETWORK_INTERFACE_HH diff --git a/libsponge/tap.sh b/libsponge/tap.sh new file mode 100755 index 0000000..4e4e662 --- /dev/null +++ b/libsponge/tap.sh @@ -0,0 +1,113 @@ +#!/bin/bash + +show_usage () { + echo "Usage: $0 [tapnum ...]" + exit 1 +} + +start_tap () { + local TAPNUM="$1" TAPDEV="tap$1" LLADDR="02:B0:1D:FA:CE:"`printf "%02x" $1` + ip tuntap add mode tap user "${SUDO_USER}" name "${TAPDEV}" + ip link set "${TAPDEV}" address "${LLADDR}" + + ip addr add "${TUN_IP_PREFIX}.${TAPNUM}.1/24" dev "${TAPDEV}" + ip link set dev "${TAPDEV}" up + ip route change "${TUN_IP_PREFIX}.${TAPNUM}.0/24" dev "${TAPDEV}" rto_min 10ms + + # Apply NAT (masquerading) only to traffic from CS144's network devices + iptables -t nat -A PREROUTING -s ${TUN_IP_PREFIX}.${TAPNUM}.0/24 -j CONNMARK --set-mark ${TAPNUM} + iptables -t nat -A POSTROUTING -j MASQUERADE -m connmark --mark ${TAPNUM} + echo 1 > /proc/sys/net/ipv4/ip_forward +} + +stop_tap () { + local TAPDEV="tap$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 tap name "$TAPDEV" +} + +start_all () { + while [ ! -z "$1" ]; do + local INTF="$1"; shift + start_tap "$INTF" + done +} + +stop_all () { + while [ ! -z "$1" ]; do + local INTF="$1"; shift + stop_tap "$INTF" + done +} + +restart_all() { + stop_all "$@" + start_all "$@" +} + +check_tap () { + [ "$#" != 1 ] && { echo "bad params in check_tap"; exit 1; } + local TAPDEV="tap${1}" + # make sure tap is healthy: device is up, ip_forward is set, and iptables is configured + ip link show ${TAPDEV} &>/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 -- 10 +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_tap ${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/libsponge/tcp_helpers/arp_message.cc b/libsponge/tcp_helpers/arp_message.cc new file mode 100644 index 0000000..df49c30 --- /dev/null +++ b/libsponge/tcp_helpers/arp_message.cc @@ -0,0 +1,88 @@ +#include "arp_message.hh" + +#include +#include +#include + +using namespace std; + +ParseResult ARPMessage::parse(const Buffer buffer) { + NetParser p{buffer}; + + if (p.buffer().size() < ARPMessage::LENGTH) { + return ParseResult::PacketTooShort; + } + + hardware_type = p.u16(); + protocol_type = p.u16(); + hardware_address_size = p.u8(); + protocol_address_size = p.u8(); + opcode = p.u16(); + + if (not supported()) { + return ParseResult::Unsupported; + } + + // read sender addresses (Ethernet and IP) + for (auto &byte : sender_ethernet_address) { + byte = p.u8(); + } + sender_ip_address = p.u32(); + + // read target addresses (Ethernet and IP) + for (auto &byte : target_ethernet_address) { + byte = p.u8(); + } + target_ip_address = p.u32(); + + return p.get_error(); +} + +bool ARPMessage::supported() const { + return hardware_type == TYPE_ETHERNET and protocol_type == EthernetHeader::TYPE_IPv4 and + hardware_address_size == sizeof(EthernetHeader::src) and protocol_address_size == sizeof(IPv4Header::src) and + ((opcode == OPCODE_REQUEST) or (opcode == OPCODE_REPLY)); +} + +string ARPMessage::serialize() const { + if (not supported()) { + throw runtime_error( + "ARPMessage::serialize(): unsupported field combination (must be Ethernet/IP, and request or reply)"); + } + + string ret; + NetUnparser::u16(ret, hardware_type); + NetUnparser::u16(ret, protocol_type); + NetUnparser::u8(ret, hardware_address_size); + NetUnparser::u8(ret, protocol_address_size); + NetUnparser::u16(ret, opcode); + + /* write sender addresses */ + for (auto &byte : sender_ethernet_address) { + NetUnparser::u8(ret, byte); + } + NetUnparser::u32(ret, sender_ip_address); + + /* write target addresses */ + for (auto &byte : target_ethernet_address) { + NetUnparser::u8(ret, byte); + } + NetUnparser::u32(ret, target_ip_address); + + return ret; +} + +string ARPMessage::to_string() const { + stringstream ss{}; + string opcode_str = "(unknown type)"; + if (opcode == OPCODE_REQUEST) { + opcode_str = "REQUEST"; + } + if (opcode == OPCODE_REPLY) { + opcode_str = "REPLY"; + } + ss << "opcode=" << opcode_str << ", sender=" << ::to_string(sender_ethernet_address) << "/" + << inet_ntoa({htobe32(sender_ip_address)}) << ", target=" << ::to_string(target_ethernet_address) << "/" + << inet_ntoa({htobe32(target_ip_address)}); + return ss.str(); +} diff --git a/libsponge/tcp_helpers/arp_message.hh b/libsponge/tcp_helpers/arp_message.hh new file mode 100644 index 0000000..cc77983 --- /dev/null +++ b/libsponge/tcp_helpers/arp_message.hh @@ -0,0 +1,47 @@ +#ifndef SPONGE_LIBSPONGE_ARP_MESSAGE_HH +#define SPONGE_LIBSPONGE_ARP_MESSAGE_HH + +#include "ethernet_header.hh" +#include "ipv4_header.hh" + +using EthernetAddress = std::array; + +//! \brief [ARP](\ref rfc::rfc826) message +struct ARPMessage { + static constexpr size_t LENGTH = 28; //!< ARP message length in bytes + static constexpr uint16_t TYPE_ETHERNET = 1; //!< ARP type for Ethernet/Wi-Fi as link-layer protocol + static constexpr uint16_t OPCODE_REQUEST = 1; + static constexpr uint16_t OPCODE_REPLY = 2; + + //! \name ARPheader fields + //!@{ + uint16_t hardware_type = TYPE_ETHERNET; //!< Type of the link-layer protocol (generally Ethernet/Wi-Fi) + uint16_t protocol_type = EthernetHeader::TYPE_IPv4; //!< Type of the Internet-layer protocol (generally IPv4) + uint8_t hardware_address_size = sizeof(EthernetHeader::src); + uint8_t protocol_address_size = sizeof(IPv4Header::src); + uint16_t opcode{}; //!< Request or reply + + EthernetAddress sender_ethernet_address{}; + uint32_t sender_ip_address{}; + + EthernetAddress target_ethernet_address{}; + uint32_t target_ip_address{}; + //!@} + + //! Parse the ARP message from a string + ParseResult parse(const Buffer buffer); + + //! Serialize the ARP message to a string + std::string serialize() const; + + //! Return a string containing the ARP message in human-readable format + std::string to_string() const; + + //! Is this type of ARP message supported by the parser? + bool supported() const; +}; + +//! \struct ARPMessage +//! This struct can be used to parse an existing ARP message or to create a new one. + +#endif // SPONGE_LIBSPONGE_ETHERNET_HEADER_HH diff --git a/libsponge/tcp_helpers/ethernet_frame.cc b/libsponge/tcp_helpers/ethernet_frame.cc new file mode 100644 index 0000000..3774de7 --- /dev/null +++ b/libsponge/tcp_helpers/ethernet_frame.cc @@ -0,0 +1,24 @@ +#include "ethernet_frame.hh" + +#include "parser.hh" +#include "util.hh" + +#include +#include + +using namespace std; + +ParseResult EthernetFrame::parse(const Buffer buffer) { + NetParser p{buffer}; + _header.parse(p); + _payload = p.buffer(); + + return p.get_error(); +} + +BufferList EthernetFrame::serialize() const { + BufferList ret; + ret.append(_header.serialize()); + ret.append(_payload); + return ret; +} diff --git a/libsponge/tcp_helpers/ethernet_frame.hh b/libsponge/tcp_helpers/ethernet_frame.hh new file mode 100644 index 0000000..420adbd --- /dev/null +++ b/libsponge/tcp_helpers/ethernet_frame.hh @@ -0,0 +1,30 @@ +#ifndef SPONGE_LIBSPONGE_ETHERNET_FRAME_HH +#define SPONGE_LIBSPONGE_ETHERNET_FRAME_HH + +#include "buffer.hh" +#include "ethernet_header.hh" + +//! \brief Ethernet frame +class EthernetFrame { + private: + EthernetHeader _header{}; + BufferList _payload{}; + + public: + //! \brief Parse the frame from a string + ParseResult parse(const Buffer buffer); + + //! \brief Serialize the frame to a string + BufferList serialize() const; + + //! \name Accessors + //!@{ + const EthernetHeader &header() const { return _header; } + EthernetHeader &header() { return _header; } + + const BufferList &payload() const { return _payload; } + BufferList &payload() { return _payload; } + //!@} +}; + +#endif // SPONGE_LIBSPONGE_ETHERNET_FRAME_HH diff --git a/libsponge/tcp_helpers/ethernet_header.cc b/libsponge/tcp_helpers/ethernet_header.cc new file mode 100644 index 0000000..a0cc832 --- /dev/null +++ b/libsponge/tcp_helpers/ethernet_header.cc @@ -0,0 +1,83 @@ +#include "ethernet_header.hh" + +#include "util.hh" + +#include +#include + +using namespace std; + +ParseResult EthernetHeader::parse(NetParser &p) { + if (p.buffer().size() < EthernetHeader::LENGTH) { + return ParseResult::PacketTooShort; + } + + /* read destination address */ + for (auto &byte : dst) { + byte = p.u8(); + } + + /* read source address */ + for (auto &byte : src) { + byte = p.u8(); + } + + /* read the frame's type (e.g. IPv4, ARP, or something else) */ + type = p.u16(); + + return p.get_error(); +} + +string EthernetHeader::serialize() const { + string ret; + ret.reserve(LENGTH); + + /* write destination address */ + for (auto &byte : dst) { + NetUnparser::u8(ret, byte); + } + + /* write source address */ + for (auto &byte : src) { + NetUnparser::u8(ret, byte); + } + + /* write the frame's type (e.g. IPv4, ARP or something else) */ + NetUnparser::u16(ret, type); + + return ret; +} + +//! \returns A string with a textual representation of an Ethernet address +string to_string(const EthernetAddress address) { + stringstream ss{}; + for (auto it = address.begin(); it != address.end(); it++) { + ss.width(2); + ss << setfill('0') << hex << int(*it); + if (it != address.end() - 1) { + ss << ":"; + } + } + return ss.str(); +} + +//! \returns A string with the header's contents +string EthernetHeader::to_string() const { + stringstream ss{}; + ss << "dst=" << ::to_string(dst); + ss << ", src=" << ::to_string(src); + ss << ", type="; + switch (type) { + case TYPE_IPv4: + ss << "IPv4"; + break; + case TYPE_ARP: + ss << "ARP"; + break; + default: + ss << "[unknown type " << hex << type << "!]"; + break; + } + + return ss.str(); +} diff --git a/libsponge/tcp_helpers/ethernet_header.hh b/libsponge/tcp_helpers/ethernet_header.hh new file mode 100644 index 0000000..4e017ca --- /dev/null +++ b/libsponge/tcp_helpers/ethernet_header.hh @@ -0,0 +1,43 @@ +#ifndef SPONGE_LIBSPONGE_ETHERNET_HEADER_HH +#define SPONGE_LIBSPONGE_ETHERNET_HEADER_HH + +#include "parser.hh" + +#include + +//! Helper type for an Ethernet address (an array of six bytes) +using EthernetAddress = std::array; + +//! Ethernet broadcast address (ff:ff:ff:ff:ff:ff) +constexpr EthernetAddress ETHERNET_BROADCAST = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; + +//! Printable representation of an EthernetAddress +std::string to_string(const EthernetAddress address); + +//! \brief Ethernet frame header +struct EthernetHeader { + static constexpr size_t LENGTH = 14; //!< Ethernet header length in bytes + static constexpr uint16_t TYPE_IPv4 = 0x800; //!< Type number for [IPv4](\ref rfc::rfc791) + static constexpr uint16_t TYPE_ARP = 0x806; //!< Type number for [ARP](\ref rfc::rfc826) + + //! \name Ethernet header fields + //!@{ + EthernetAddress dst; + EthernetAddress src; + uint16_t type; + //!@} + + //! Parse the Ethernet fields from the provided NetParser + ParseResult parse(NetParser &p); + + //! Serialize the Ethernet fields to a string + std::string serialize() const; + + //! Return a string containing a header in human-readable format + std::string to_string() const; +}; + +//! \struct EthernetHeader +//! This struct can be used to parse an existing Ethernet header or to create a new one. + +#endif // SPONGE_LIBSPONGE_ETHERNET_HEADER_HH diff --git a/libsponge/tcp_helpers/tcp_sponge_socket.cc b/libsponge/tcp_helpers/tcp_sponge_socket.cc index 0e3e64d..2e0fe41 100644 --- a/libsponge/tcp_helpers/tcp_sponge_socket.cc +++ b/libsponge/tcp_helpers/tcp_sponge_socket.cc @@ -1,5 +1,6 @@ #include "tcp_sponge_socket.hh" +#include "network_interface.hh" #include "parser.hh" #include "tun.hh" #include "util.hh" @@ -211,7 +212,7 @@ void TCPSpongeSocket::connect(const TCPConfig &c_tcp, const FdAdapterCon _datagram_adapter.config_mut() = c_ad; - cerr << "DEBUG: Connecting to " << c_ad.destination.to_string() << "... "; + cerr << "DEBUG: Connecting to " << c_ad.destination.to_string() << "...\n"; _tcp->connect(); const TCPState expected_state = TCPState::State::SYN_SENT; @@ -222,7 +223,7 @@ void TCPSpongeSocket::connect(const TCPConfig &c_tcp, const FdAdapterCon } _tcp_loop([&] { return _tcp->state() == TCPState::State::SYN_SENT; }); - cerr << "done.\n"; + cerr << "Successfully connected to " << c_ad.destination.to_string() << ".\n"; _tcp_thread = thread(&TCPSpongeSocket::_tcp_main, this); } @@ -240,12 +241,12 @@ void TCPSpongeSocket::listen_and_accept(const TCPConfig &c_tcp, const Fd _datagram_adapter.config_mut() = c_ad; _datagram_adapter.set_listening(true); - cerr << "DEBUG: Listening for incoming connection... "; + cerr << "DEBUG: Listening for incoming connection...\n"; _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"; + cerr << "New connection from " << _datagram_adapter.config().destination.to_string() << ".\n"; _tcp_thread = thread(&TCPSpongeSocket::_tcp_main, this); } @@ -275,6 +276,9 @@ template class TCPSpongeSocket; //! Specialization of TCPSpongeSocket for TCPOverIPv4OverTunFdAdapter template class TCPSpongeSocket; +//! Specialization of TCPSpongeSocket for TCPOverIPv4OverEthernetAdapter +template class TCPSpongeSocket; + //! Specialization of TCPSpongeSocket for LossyTCPOverUDPSocketAdapter template class TCPSpongeSocket; @@ -293,3 +297,34 @@ void CS144TCPSocket::connect(const Address &address) { TCPOverIPv4SpongeSocket::connect(tcp_config, multiplexer_config); } + +static const string LOCAL_TAP_IP_ADDRESS = "169.254.10.9"; +static const string LOCAL_TAP_NEXT_HOP_ADDRESS = "169.254.10.1"; + +EthernetAddress random_private_ethernet_address() { + EthernetAddress addr; + for (auto &byte : addr) { + byte = random_device()(); // use a random local Ethernet address + } + addr.at(0) |= 0x02; // "10" in last two binary digits marks a private Ethernet address + addr.at(0) &= 0xfe; + + return addr; +} + +FullStackSocket::FullStackSocket() + : TCPOverIPv4OverEthernetSpongeSocket(TCPOverIPv4OverEthernetAdapter(TapFD("tap10"), + random_private_ethernet_address(), + Address(LOCAL_TAP_IP_ADDRESS, "0"), + Address(LOCAL_TAP_NEXT_HOP_ADDRESS, "0"))) {} + +void FullStackSocket::connect(const Address &address) { + TCPConfig tcp_config; + tcp_config.rt_timeout = 100; + + FdAdapterConfig multiplexer_config; + multiplexer_config.source = {LOCAL_TAP_IP_ADDRESS, to_string(uint16_t(random_device()()))}; + multiplexer_config.destination = address; + + TCPOverIPv4OverEthernetSpongeSocket::connect(tcp_config, multiplexer_config); +} diff --git a/libsponge/tcp_helpers/tcp_sponge_socket.hh b/libsponge/tcp_helpers/tcp_sponge_socket.hh index 606d04d..4a40888 100644 --- a/libsponge/tcp_helpers/tcp_sponge_socket.hh +++ b/libsponge/tcp_helpers/tcp_sponge_socket.hh @@ -5,9 +5,9 @@ #include "eventloop.hh" #include "fd_adapter.hh" #include "file_descriptor.hh" +#include "network_interface.hh" #include "tcp_config.hh" #include "tcp_connection.hh" -#include "tcp_over_ip.hh" #include "tuntap_adapter.hh" #include @@ -23,9 +23,11 @@ class TCPSpongeSocket : public LocalStreamSocket { //! Stream socket for reads and writes between owner and TCP thread LocalStreamSocket _thread_data; + protected: //! Adapter to underlying datagram socket (e.g., UDP or IP) AdaptT _datagram_adapter; + private: //! Set up the TCPConnection and the event loop void _initialize_TCP(const TCPConfig &config); @@ -96,6 +98,8 @@ class TCPSpongeSocket : public LocalStreamSocket { using TCPOverUDPSpongeSocket = TCPSpongeSocket; using TCPOverIPv4SpongeSocket = TCPSpongeSocket; +using TCPOverIPv4OverEthernetSpongeSocket = TCPSpongeSocket; + using LossyTCPOverUDPSpongeSocket = TCPSpongeSocket; using LossyTCPOverIPv4SpongeSocket = TCPSpongeSocket; @@ -126,4 +130,14 @@ class CS144TCPSocket : public TCPOverIPv4SpongeSocket { void connect(const Address &address); }; +//! Helper class that makes a TCPOverIPv4overEthernetSpongeSocket behave more like a (kernel) TCPSocket +class FullStackSocket : public TCPOverIPv4OverEthernetSpongeSocket { + public: + //! Construct a TCP (stream) socket, using the CS144 TCPConnection object, + //! that encapsulates TCP segments in IP datagrams, then encapsulates + //! those IP datagrams in Ethernet frames sent to the Ethernet address of the next hop. + FullStackSocket(); + void connect(const Address &address); +}; + #endif // SPONGE_LIBSPONGE_TCP_SPONGE_SOCKET_HH diff --git a/libsponge/tcp_helpers/tuntap_adapter.cc b/libsponge/tcp_helpers/tuntap_adapter.cc index e171b13..8a12dd1 100644 --- a/libsponge/tcp_helpers/tuntap_adapter.cc +++ b/libsponge/tcp_helpers/tuntap_adapter.cc @@ -2,5 +2,58 @@ using namespace std; +//! \param[in] tap Raw network device that will be owned by the adapter +//! \param[in] eth_address Ethernet address (local address) of the adapter +//! \param[in] ip_address IP address (local address) of the adapter +//! \param[in] next_hop IP address of the next hop (typically a router or default gateway) +TCPOverIPv4OverEthernetAdapter::TCPOverIPv4OverEthernetAdapter(TapFD &&tap, + const EthernetAddress ð_address, + const Address &ip_address, + const Address &next_hop) + : _tap(move(tap)), _interface(eth_address, ip_address), _next_hop(next_hop) { + // Linux seems to ignore the first frame sent on a TAP device, so send a dummy frame to prime the pump :-( + EthernetFrame dummy_frame; + _tap.write(dummy_frame.serialize()); +} + +optional TCPOverIPv4OverEthernetAdapter::read() { + // Read Ethernet frame from the raw device + EthernetFrame frame; + if (frame.parse(_tap.read()) != ParseResult::NoError) { + return {}; + } + + // Give the frame to the NetworkInterface. Get back an Internet datagram if frame was carrying one. + optional ip_dgram = _interface.recv_frame(frame); + + // The incoming frame may have caused the NetworkInterface to send a frame. + send_pending(); + + // Try to interpret IPv4 datagram as TCP + if (ip_dgram) { + return unwrap_tcp_in_ip(ip_dgram.value()); + } + return {}; +} + +//! \param[in] ms_since_last_tick the number of milliseconds since the last call to this method +void TCPOverIPv4OverEthernetAdapter::tick(const size_t ms_since_last_tick) { + _interface.tick(ms_since_last_tick); + send_pending(); +} + +//! \param[in] seg the TCPSegment to send +void TCPOverIPv4OverEthernetAdapter::write(TCPSegment &seg) { + _interface.send_datagram(wrap_tcp_in_ip(seg), _next_hop); + send_pending(); +} + +void TCPOverIPv4OverEthernetAdapter::send_pending() { + while (not _interface.frames_out().empty()) { + _tap.write(_interface.frames_out().front().serialize()); + _interface.frames_out().pop(); + } +} + //! Specialize LossyFdAdapter to TCPOverIPv4OverTunFdAdapter template class LossyFdAdapter; diff --git a/libsponge/tcp_helpers/tuntap_adapter.hh b/libsponge/tcp_helpers/tuntap_adapter.hh index b6506a5..aacc4bb 100644 --- a/libsponge/tcp_helpers/tuntap_adapter.hh +++ b/libsponge/tcp_helpers/tuntap_adapter.hh @@ -1,7 +1,8 @@ #ifndef SPONGE_LIBSPONGE_TUNFD_ADAPTER_HH #define SPONGE_LIBSPONGE_TUNFD_ADAPTER_HH -#include "tcp_over_ip.hh" +#include "ethernet_header.hh" +#include "network_interface.hh" #include "tun.hh" #include @@ -39,4 +40,37 @@ class TCPOverIPv4OverTunFdAdapter : public TCPOverIPv4Adapter { //! Typedef for TCPOverIPv4OverTunFdAdapter using LossyTCPOverIPv4OverTunFdAdapter = LossyFdAdapter; +//! \brief A FD adapter for IPv4 datagrams read from and written to a TAP device +class TCPOverIPv4OverEthernetAdapter : public TCPOverIPv4Adapter { + private: + TapFD _tap; //!< Raw Ethernet connection + + NetworkInterface _interface; //!< NIC abstraction + + Address _next_hop; //!< IP address of the next hop + + void send_pending(); //!< Sends any pending Ethernet frames + + public: + //! Construct from a TapFD + explicit TCPOverIPv4OverEthernetAdapter(TapFD &&tap, + const EthernetAddress ð_address, + const Address &ip_address, + const Address &next_hop); + //! Attempts to read and parse an Ethernet frame containing an IPv4 datagram that contains a TCP segment + std::optional read(); + + //! Sends a TCP segment (in an IPv4 datagram, in an Ethernet frame). + void write(TCPSegment &seg); + + //! Called periodically when time elapses + void tick(const size_t ms_since_last_tick); + + //! Access the underlying raw Ethernet connection + operator TapFD &() { return _tap; } + + //! Access the underlying raw Ethernet connection + operator const TapFD &() const { return _tap; } +}; + #endif // SPONGE_LIBSPONGE_TUNFD_ADAPTER_HH diff --git a/tap.sh b/tap.sh new file mode 100755 index 0000000..4e4e662 --- /dev/null +++ b/tap.sh @@ -0,0 +1,113 @@ +#!/bin/bash + +show_usage () { + echo "Usage: $0 [tapnum ...]" + exit 1 +} + +start_tap () { + local TAPNUM="$1" TAPDEV="tap$1" LLADDR="02:B0:1D:FA:CE:"`printf "%02x" $1` + ip tuntap add mode tap user "${SUDO_USER}" name "${TAPDEV}" + ip link set "${TAPDEV}" address "${LLADDR}" + + ip addr add "${TUN_IP_PREFIX}.${TAPNUM}.1/24" dev "${TAPDEV}" + ip link set dev "${TAPDEV}" up + ip route change "${TUN_IP_PREFIX}.${TAPNUM}.0/24" dev "${TAPDEV}" rto_min 10ms + + # Apply NAT (masquerading) only to traffic from CS144's network devices + iptables -t nat -A PREROUTING -s ${TUN_IP_PREFIX}.${TAPNUM}.0/24 -j CONNMARK --set-mark ${TAPNUM} + iptables -t nat -A POSTROUTING -j MASQUERADE -m connmark --mark ${TAPNUM} + echo 1 > /proc/sys/net/ipv4/ip_forward +} + +stop_tap () { + local TAPDEV="tap$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 tap name "$TAPDEV" +} + +start_all () { + while [ ! -z "$1" ]; do + local INTF="$1"; shift + start_tap "$INTF" + done +} + +stop_all () { + while [ ! -z "$1" ]; do + local INTF="$1"; shift + stop_tap "$INTF" + done +} + +restart_all() { + stop_all "$@" + start_all "$@" +} + +check_tap () { + [ "$#" != 1 ] && { echo "bad params in check_tap"; exit 1; } + local TAPDEV="tap${1}" + # make sure tap is healthy: device is up, ip_forward is set, and iptables is configured + ip link show ${TAPDEV} &>/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 -- 10 +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_tap ${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/tests/CMakeLists.txt b/tests/CMakeLists.txt index e6e33ba..0fff3e5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library (spongechecks STATIC send_equivalence_checker.cc tcp_fsm_test_harness.cc byte_stream_test_harness.cc) +add_library (spongechecks STATIC send_equivalence_checker.cc tcp_fsm_test_harness.cc byte_stream_test_harness.cc network_interface_test_harness.cc) macro (add_test_exec exec_name) add_executable ("${exec_name}" "${exec_name}.cc") @@ -50,3 +50,4 @@ add_test_exec (send_ack) add_test_exec (send_window) add_test_exec (send_close) add_test_exec (send_extra) +add_test_exec (net_interface) diff --git a/writeups/lab5.md b/writeups/lab5.md new file mode 100644 index 0000000..d38ccc2 --- /dev/null +++ b/writeups/lab5.md @@ -0,0 +1,29 @@ +Lab 5 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 NetworkInterface: +[] + +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]