diff --git a/CMakeLists.txt b/CMakeLists.txt index 3daa119f..403fd5e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,8 @@ add_executable(newserv src/Shell.cc src/ServerShell.cc src/ProxyShell.cc + src/IPFrameInfo.cc + src/IPStackSimulator.cc src/Main.cc ) target_include_directories(newserv PUBLIC ${LIBEVENT_INCLUDE_DIR}) diff --git a/README.md b/README.md index e7e0a7e6..cf0c9ae8 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ This project is primarily for my own nostalgia. Feel free to peruse if you'd lik Currently this code should build on macOS and Ubuntu. It might build on other Linux flavors, but don't expect it to work on Windows at all. So, you've read all of the above and you want to try it out? Here's what you do: -- Make sure you have CMake and libevent installed (use Homebrew in macOS, or install libevent-dev in Linux). +- Make sure you have CMake and libevent installed. - Build and install phosg (https://github.com/fuzziqersoftware/phosg). - Run `cmake . && make`. - Edit system/config.json to your liking. @@ -36,17 +36,21 @@ So, you've read all of the above and you want to try it out? Here's what you do: ### Connecting local clients -If you're running PSO on a real GameCube, you can make PSO connect to newserv by changing its default gateway and DNS server addresses to newserv's address. +If you're running PSO on a real GameCube, you can make it connect to newserv by setting its default gateway and DNS server addresses to newserv's address. -If you're emulating PSO GC using Dolphin on Mac OS (like I am), you can make it connect to newserv by doing this: +If you're emulating PSO GC using Dolphin on macOS (like I am), you can make it connect to a newserv instance running on the same machine by doing this: - Use a build of Dolphin that has tapserver support. -- Install tapserver (https://github.com/fuzziqersoftware/tapserver). -- In PSO, manually configure your network settings as follows: IP address = `192.168.0.200`, subnet mask = `255.255.255.0`, default gateway = `192.168.0.5`, DNS server address 1 = `192.168.0.5`. -- Start Dolphin and newserv. -- Run tapserver according to its instructions. -- Start an online game, and it will connect to your local instance of newserv! +- Enable the IP stack simulator according to the comments in config.json, and start newserv. +- In PSO, you have to configure the network settings manually, but the actual values don't matter as long as they're valid IP addresses. Example values: + - IP address: `10.0.1.5` + - Subnet mask: `255.255.255.0` + - Default gateway: `10.0.1.1` + - DNS server address 1: `10.0.1.1` +- Start an online game. -If you want to play online on remote servers, newserv also includes a PSO proxy server. Run newserv like `./newserv --proxy-destination=1.1.1.1` (replace the IP address appropriately for the server you want to connect to). +This setup works for all PSO versions, including Plus and Episode III. + +If you want to play online on remote servers, newserv also includes a PSO proxy server. Run newserv like `./newserv --proxy-destination=1.1.1.1` (replace the IP address appropriately for the server you want to connect to). Currently this works with PSO PC and GC, but not with BB. ### Connecting external clients diff --git a/src/Client.cc b/src/Client.cc index 164f24a2..3c917bc4 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -15,17 +15,36 @@ using namespace std; -Client::Client(struct bufferevent* bev, GameVersion version, - ServerBehavior server_behavior) : version(version), - flags(flags_for_version(version, 0)), bev(bev), - server_behavior(server_behavior), should_disconnect(false), - play_time_begin(now()), last_recv_time(this->play_time_begin), - last_send_time(0), area(0), lobby_id(0), lobby_client_id(0), - lobby_arrow_color(0), next_exp_value(0), infinite_hp(false), - infinite_tp(false), can_chat(true) { +Client::Client( + struct bufferevent* bev, + GameVersion version, + ServerBehavior server_behavior) + : version(version), + flags(flags_for_version(this->version, 0)), + bev(bev), + server_behavior(server_behavior), + should_disconnect(false), + play_time_begin(now()), + last_recv_time(this->play_time_begin), + last_send_time(0), + area(0), + lobby_id(0), + lobby_client_id(0), + lobby_arrow_color(0), + next_exp_value(0), + infinite_hp(false), + infinite_tp(false), + can_chat(true) { int fd = bufferevent_getfd(this->bev); - get_socket_addresses(fd, &this->local_addr, &this->remote_addr); + if (fd < 0) { + this->is_virtual_connection = true; + memset(&this->local_addr, 0, sizeof(this->local_addr)); + memset(&this->remote_addr, 0, sizeof(this->remote_addr)); + } else { + this->is_virtual_connection = false; + get_socket_addresses(fd, &this->local_addr, &this->remote_addr); + } memset(this->name, 0, sizeof(this->name)); memset(&this->next_connection_addr, 0, sizeof(this->next_connection_addr)); } diff --git a/src/Client.hh b/src/Client.hh index c13e065c..f4dcd21d 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -33,23 +33,24 @@ struct ClientConfigBB { }; struct Client { - // license & account + // License & account std::shared_ptr license; char16_t name[0x20]; ClientConfigBB config; GameVersion version; uint16_t flags; - // encryption + // Encryption std::unique_ptr crypt_in; std::unique_ptr crypt_out; - // network + // Network struct sockaddr_storage local_addr; struct sockaddr_storage remote_addr; struct bufferevent* bev; struct sockaddr_storage next_connection_addr; ServerBehavior server_behavior; + bool is_virtual_connection; bool should_disconnect; std::string recv_buffer; diff --git a/src/DNSServer.cc b/src/DNSServer.cc index fbdd0064..7dd98ba2 100644 --- a/src/DNSServer.cc +++ b/src/DNSServer.cc @@ -54,6 +54,31 @@ void DNSServer::dispatch_on_receive_message(evutil_socket_t fd, reinterpret_cast(ctx)->on_receive_message(fd, events); } +string DNSServer::response_for_query( + const void* vdata, size_t size, uint32_t resolved_address) { + if (size < 0x0C) { + throw invalid_argument("query too small"); + } + + const char* data = reinterpret_cast(vdata); + size_t name_len = strlen(&data[12]) + 1; + + be_uint32_t be_resolved_address = resolved_address; + + string response; + response.append(data, 2); + response.append("\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00", 10); + response.append(&data[12], name_len); + response.append("\x00\x01\x00\x01\xC0\x0C\x00\x01\x00\x01\x00\x00\x00\x3C\x00\x04", 16); + response.append(reinterpret_cast(&be_resolved_address), 4); + return response; +} + +string DNSServer::response_for_query( + const string& query, uint32_t resolved_address) { + return DNSServer::response_for_query(query.data(), query.size(), resolved_address); +} + void DNSServer::on_receive_message(int fd, short) { for (;;) { sockaddr_in remote; @@ -74,34 +99,19 @@ void DNSServer::on_receive_message(int fd, short) { } else if (bytes == 0) { break; - } else { // bytes > 0 + } else if (bytes < 0x0C) { + log(WARNING, "[DNSServer] input query too small"); + print_data(stderr, input); + + } else { input.resize(bytes); - uint32_t remote_address = bswap32(remote.sin_addr.s_addr); - uint32_t connect_address; - if (is_local_address(remote_address)) { - connect_address = this->local_connect_address; - } else { - connect_address = this->external_connect_address; - } - - if (input.size() >= 0x0C) { - string response; - size_t name_len = strlen(input.data() + 0x0C) + 1; - - uint32_t connect_address_be = bswap32(connect_address); - response.append(input.substr(0, 2)); - response.append("\x81\x80\x00\x01\x00\x01\x00\x00\x00\x00", 10); - response.append(input.substr(12, name_len)); - response.append("\x00\x01\x00\x01\xC0\x0C\x00\x01\x00\x01\x00\x00\x00\x3C\x00\x04", 16); - response.append(reinterpret_cast(&connect_address_be), 4); - - sendto(fd, response.data(), response.size(), 0, - reinterpret_cast(&remote), remote_size); - } else { - log(WARNING, "[DNSServer] input query too small"); - print_data(stderr, input); - } + uint32_t connect_address = is_local_address(remote_address) + ? this->local_connect_address + : this->external_connect_address; + string response = this->response_for_query(input, connect_address); + sendto(fd, response.data(), response.size(), 0, + reinterpret_cast(&remote), remote_size); } } } diff --git a/src/DNSServer.hh b/src/DNSServer.hh index d0489681..d9f79ea9 100644 --- a/src/DNSServer.hh +++ b/src/DNSServer.hh @@ -21,6 +21,11 @@ public: void listen(int port); void add_socket(int fd); + static std::string response_for_query( + const void* vdata, size_t size, uint32_t resolved_address); + static std::string response_for_query( + const std::string& query, uint32_t resolved_address); + private: std::shared_ptr base; std::unordered_map> fd_to_receive_event; diff --git a/src/IPFrameInfo.cc b/src/IPFrameInfo.cc new file mode 100644 index 00000000..535936e6 --- /dev/null +++ b/src/IPFrameInfo.cc @@ -0,0 +1,293 @@ +#include "IPFrameInfo.hh" + +#include + +#include + +using namespace std; + + + +static inline uint16_t collapse_checksum(uint32_t sum) { + // It's impossible for this to be necessary more than twice: the first + // addition can carry out at most a single bit. + sum = (sum & 0xFFFF) + (sum >> 16); + return (sum & 0xFFFF) + (sum >> 16); +} + + + +FrameInfo::FrameInfo() + : ether(nullptr), + ether_protocol(0), + ipv4(nullptr), + arp(nullptr), + udp(nullptr), + tcp(nullptr), + header_start(nullptr), + payload(nullptr), + total_size(0), + tcp_options_size(0), + payload_size(0) { } + +FrameInfo::FrameInfo(const string& data) : FrameInfo(data.data(), data.size()) { } + +FrameInfo::FrameInfo(const void* header_start, size_t size) + : ether(nullptr), + ether_protocol(0), + ipv4(nullptr), + arp(nullptr), + udp(nullptr), + tcp(nullptr), + header_start(header_start), + payload(nullptr), + total_size(size), + tcp_options_size(0), + payload_size(size) { + + // Parse ethernet header + if (this->payload_size < sizeof(EthernetHeader)) { + throw invalid_argument("frame is too small for ethernet"); + } + this->payload_size -= sizeof(EthernetHeader); + this->ether = reinterpret_cast(header_start); + this->ether_protocol = this->ether->protocol; + + // Figure out the protocol + const be_uint16_t* u16data = reinterpret_cast(this->ether + 1); + while ((this->ether_protocol == 0x8100) || (this->ether_protocol == 0x88A8)) { + if (this->payload_size < 4) { + throw invalid_argument("VLAN tags exceed frame size"); + } + this->ether_protocol = u16data[1]; + u16data += 2; + this->payload_size -= 4; + } + + // TODO: Some less-common protocols that we might want to support: + // 0x8035 = RARP + // 0x809B = AppleTalk + // 0x80F3 = AppleTalk ARP + // 0x8137 = IPX + // 0x9000 = loopback + + // Parse protocol headers if possible + if (this->ether_protocol == 0x0800) { // IPv4 + if (this->payload_size < sizeof(IPv4Header)) { + throw invalid_argument("frame is too small for ipv4 header"); + } + this->ipv4 = reinterpret_cast(u16data); + if (this->payload_size < this->ipv4->size) { + throw invalid_argument("ipv4 header specifies size larger than frame"); + } + this->payload_size = this->ipv4->size - sizeof(IPv4Header); + + if (this->ipv4->protocol == 0x06) { + if (this->payload_size < sizeof(TCPHeader)) { + throw invalid_argument("frame is too small for tcp4 header"); + } + this->tcp = reinterpret_cast(this->ipv4 + 1); + size_t tcp_header_size = (this->tcp->flags >> 12) * 4; + if (tcp_header_size < sizeof(TCPHeader) || tcp_header_size > this->payload_size) { + throw invalid_argument("frame is too small for tcp4 header with options"); + } + this->tcp_options_size = tcp_header_size - sizeof(TCPHeader); + this->payload_size -= tcp_header_size; + this->payload = reinterpret_cast(this->tcp) + tcp_header_size; + + } else if (this->ipv4->protocol == 0x11) { + if (this->payload_size < sizeof(UDPHeader)) { + throw invalid_argument("frame is too small for udp4 header"); + } + this->payload_size -= sizeof(UDPHeader); + this->udp = reinterpret_cast(this->ipv4 + 1); + this->payload = this->udp + 1; + + } else { + this->payload = this->ipv4 + 1; + } + + } else if (this->ether_protocol == 0x0806) { // ARP + if (this->payload_size < sizeof(const ARPHeader)) { + throw invalid_argument("frame is too small for arp header"); + } + this->payload_size -= sizeof(ARPHeader); + this->arp = reinterpret_cast(u16data); + this->payload = this->arp + 1; + + } else { + throw runtime_error("unknown protocol"); + } +} + +string FrameInfo::header_str() const { + if (!this->ether) { + return ""; + } + + string ret = string_printf("%02hhX%02hhX%02hhX%02hhX%02hhX%02hhX->%02hhX%02hhX%02hhX%02hhX%02hhX%02hhX", + this->ether->src_mac[0], this->ether->src_mac[1], this->ether->src_mac[2], + this->ether->src_mac[3], this->ether->src_mac[4], this->ether->src_mac[5], + this->ether->dest_mac[0], this->ether->dest_mac[1], this->ether->dest_mac[2], + this->ether->dest_mac[3], this->ether->dest_mac[4], this->ether->dest_mac[5]); + + if (this->arp) { + ret += string_printf(",ARP,hw_type=%04hX,proto_type=%04hX,hw_addr_len=%02hhX,proto_addr_len=%02hhX,op=%04hX", + this->arp->hardware_type.load(), this->arp->protocol_type.load(), this->arp->hwaddr_len, this->arp->paddr_len, this->arp->operation.load()); + + } else if (this->ipv4) { + ret += string_printf(",IPv4,size=%04hX,src=%08" PRIX32 ",dest=%08" PRIX32, + this->ipv4->size.load(), this->ipv4->src_addr.load(), this->ipv4->dest_addr.load()); + + if (this->udp) { + ret += string_printf(",UDP,src_port=%04hX,dest_port=%04hX,size=%04hX", + this->udp->src_port.load(), this->udp->dest_port.load(), this->udp->size.load()); + + } else if (this->tcp) { + ret += string_printf(",TCP,src_port=%04hX,dest_port=%04hX,seq=%08" PRIX32 ",ack=%08" PRIX32 ",flags=%04hX(", + this->tcp->src_port.load(), this->tcp->dest_port.load(), this->tcp->seq_num.load(), this->tcp->ack_num.load(), this->tcp->flags.load()); + if (this->tcp->flags & TCPHeader::Flag::FIN) { + ret += "FIN,"; + } + if (this->tcp->flags & TCPHeader::Flag::SYN) { + ret += "SYN,"; + } + if (this->tcp->flags & TCPHeader::Flag::RST) { + ret += "RST,"; + } + if (this->tcp->flags & TCPHeader::Flag::PSH) { + ret += "PSH,"; + } + if (this->tcp->flags & TCPHeader::Flag::ACK) { + ret += "ACK"; + } + ret += ')'; + + } else { + ret += string_printf(",proto=%02hhX", this->ipv4->protocol); + } + + } else { + ret += string_printf(",proto=%04hX", this->ether->protocol.load()); + } + + return ret; +} + +void FrameInfo::truncate(size_t new_total_size) { + if (new_total_size > this->total_size) { + throw logic_error("truncate call expands frame size"); + } + if (new_total_size < this->payload_size) { + throw logic_error("truncate call destroys part of header"); + } + size_t delta_bytes = this->total_size - new_total_size; + this->total_size -= delta_bytes; + this->payload_size -= delta_bytes; +} + +size_t FrameInfo::size_from_header() const { + if (this->ipv4) { + return this->ipv4->size; + } else if (this->arp) { + return sizeof(ARPHeader) + 2 * (this->arp->hwaddr_len + this->arp->paddr_len); + } else { + return 0; + } +} + +uint16_t FrameInfo::computed_ipv4_header_checksum(const IPv4Header& ipv4) { + return ~collapse_checksum( + ((ipv4.version_ihl << 8) | ipv4.tos) + + ipv4.size + + ipv4.id + + ipv4.frag_offset + + ((ipv4.ttl << 8) | ipv4.protocol) + + (ipv4.src_addr >> 16) + + (ipv4.src_addr & 0xFFFF) + + (ipv4.dest_addr >> 16) + + (ipv4.dest_addr & 0xFFFF)); +} + +uint16_t FrameInfo::computed_ipv4_header_checksum() const { + if (!this->ipv4) { + throw logic_error("cannot compute ipv4 header checksum for non-ipv4 frame"); + } + return this->computed_ipv4_header_checksum(*this->ipv4); +} + +uint16_t FrameInfo::computed_udp4_checksum( + const IPv4Header& ipv4, const UDPHeader& udp, const void* data, size_t size) { + uint32_t sum = + (ipv4.src_addr >> 16) + + (ipv4.src_addr & 0xFFFF) + + (ipv4.dest_addr >> 16) + + (ipv4.dest_addr & 0xFFFF) + + ipv4.protocol + + udp.size + + udp.src_port + + udp.dest_port + + udp.size; + + const uint8_t* u8_data = reinterpret_cast(data); + for (size_t offset = 0; offset + 2 <= size; offset += 2) { + sum += *reinterpret_cast(u8_data + offset); + } + if (size & 1) { + sum += u8_data[size - 1] << 8; + } + return ~collapse_checksum(sum); +} + +uint16_t FrameInfo::computed_udp4_checksum() const { + if (!this->ipv4) { + throw logic_error("cannot compute udp header checksum for non-ipv4 frame"); + } + if (!this->udp) { + throw logic_error("cannot compute udp header checksum for non-udp frame"); + } + return this->computed_udp4_checksum( + *this->ipv4, *this->udp, this->payload, this->payload_size); +} + +uint16_t FrameInfo::computed_tcp4_checksum( + const IPv4Header& ipv4, const TCPHeader& tcp, const void* data, size_t size) { + uint16_t tcp_size = ipv4.size - sizeof(IPv4Header); + uint32_t sum = + (ipv4.src_addr >> 16) + + (ipv4.src_addr & 0xFFFF) + + (ipv4.dest_addr >> 16) + + (ipv4.dest_addr & 0xFFFF) + + ipv4.protocol + + tcp_size + + tcp.src_port + + tcp.dest_port + + (tcp.seq_num >> 16) + + (tcp.seq_num & 0xFFFF) + + (tcp.ack_num >> 16) + + (tcp.ack_num & 0xFFFF) + + tcp.flags + + tcp.window + + tcp.urgent_ptr; + + const uint8_t* u8_data = reinterpret_cast(data); + for (size_t offset = 0; offset + 2 <= size; offset += 2) { + sum += *reinterpret_cast(u8_data + offset); + } + if (size & 1) { + sum += u8_data[size - 1] << 8; + } + return ~collapse_checksum(sum); +} + +uint16_t FrameInfo::computed_tcp4_checksum() const { + if (!this->ipv4) { + throw logic_error("cannot compute tcp header checksum for non-ipv4 frame"); + } + if (!this->tcp) { + throw logic_error("cannot compute tcp header checksum for non-tcp frame"); + } + return this->computed_tcp4_checksum( + *this->ipv4, *this->tcp, this->tcp + 1, + this->payload_size + this->tcp_options_size); +} diff --git a/src/IPFrameInfo.hh b/src/IPFrameInfo.hh new file mode 100644 index 00000000..c76f3f32 --- /dev/null +++ b/src/IPFrameInfo.hh @@ -0,0 +1,105 @@ +#pragma once + +#include + +#include + + + +struct EthernetHeader { + uint8_t dest_mac[6]; + uint8_t src_mac[6]; + be_uint16_t protocol; +} __attribute__((packed)); + +struct ARPHeader { + be_uint16_t hardware_type; + be_uint16_t protocol_type; // same as EthernetHeader::protocol + uint8_t hwaddr_len; + uint8_t paddr_len; + be_uint16_t operation; +} __attribute__((packed)); + +struct IPv4Header { + uint8_t version_ihl; + uint8_t tos; + be_uint16_t size; + be_uint16_t id; + be_uint16_t frag_offset; + uint8_t ttl; + uint8_t protocol; + be_uint16_t checksum; + be_uint32_t src_addr; + be_uint32_t dest_addr; +} __attribute__((packed)); + +struct UDPHeader { + be_uint16_t src_port; + be_uint16_t dest_port; + be_uint16_t size; + be_uint16_t checksum; +} __attribute__((packed)); + +struct TCPHeader { + enum Flag { + NS = 0x0100, + CWR = 0x0080, // congestion window reduced + ECE = 0x0040, // ECN capable / congestion experienced + URG = 0x0020, // urgent pointer used + ACK = 0x0010, // ack_num is valid + PSH = 0x0008, // sending data + RST = 0x0004, // reset (hard disconnect) + SYN = 0x0002, // synchronize sequence numbers (open connection) + FIN = 0x0001, // close (normal disconnect) + }; + + be_uint16_t src_port; + be_uint16_t dest_port; + be_uint32_t seq_num; + be_uint32_t ack_num; + be_uint16_t flags; + be_uint16_t window; + be_uint16_t checksum; + be_uint16_t urgent_ptr; +} __attribute__((packed)); + + + +struct FrameInfo { + // This is always valid + const EthernetHeader* ether; + uint16_t ether_protocol; + + // At most one of these is not null + const IPv4Header* ipv4; + const ARPHeader* arp; + + // One of these may be not null if this->ipv4 is not null + const UDPHeader* udp; + const TCPHeader* tcp; + + const void* header_start; + const void* payload; + size_t total_size; + size_t tcp_options_size; + size_t payload_size; + + FrameInfo(); + FrameInfo(const std::string& data); + FrameInfo(const void* data, size_t size); + + std::string header_str() const; + + void truncate(size_t new_total_size); + + size_t size_from_header() const; + + static uint16_t computed_ipv4_header_checksum(const IPv4Header& ipv4); + uint16_t computed_ipv4_header_checksum() const; + static uint16_t computed_udp4_checksum( + const IPv4Header& ipv4, const UDPHeader& udp, const void* data, size_t size); + uint16_t computed_udp4_checksum() const; + static uint16_t computed_tcp4_checksum( + const IPv4Header& ip, const TCPHeader& tcp, const void* data, size_t size); + uint16_t computed_tcp4_checksum() const; +}; diff --git a/src/IPStackSimulator.cc b/src/IPStackSimulator.cc new file mode 100644 index 00000000..603be58e --- /dev/null +++ b/src/IPStackSimulator.cc @@ -0,0 +1,933 @@ +#include "IPStackSimulator.hh" + +#include +#include +#include +#include + +#ifndef PHOSG_WINDOWS +#include +#else +#include +#include +#endif + +#include +#include +#include +#include +#include +#include + +#include "IPFrameInfo.hh" +#include "DNSServer.hh" + +using namespace std; + + + +static const size_t DEFAULT_RESEND_PUSH_USECS = 200000; // 200ms + + + +// Note: these functions exist because seq nums are allowed to wrap around the +// 32-bit integer space by design. We have to do the subtraction before the +// comparison to allow integer overflow to occur if needed. + +static inline bool seq_num_less(uint32_t a, uint32_t b) { + return (a - b) & 0x80000000; +} + +static inline bool seq_num_less_or_equal(uint32_t a, uint32_t b) { + return (a == b) || seq_num_less(a, b); +} + +static inline bool seq_num_greater(uint32_t a, uint32_t b) { + return (b - a) & 0x80000000; +} + +static __attribute__((unused)) inline bool seq_num_greater_or_equal(uint32_t a, uint32_t b) { + return (a == b) || seq_num_greater(a, b); +} + + + +string IPStackSimulator::str_for_ipv4_netloc(uint32_t addr, uint16_t port) { + be_uint32_t be_addr = addr; + char addr_str[INET_ADDRSTRLEN]; + if (!inet_ntop(AF_INET, &be_addr, addr_str, INET_ADDRSTRLEN)) { + return string_printf(":%hu", port); + } else { + return string_printf("%s:%hu", addr_str, port); + } +} + +string IPStackSimulator::str_for_tcp_connection(shared_ptr c, + const IPClient::TCPConnection& conn) { + uint64_t key = IPStackSimulator::tcp_conn_key_for_connection(conn); + string server_netloc_str = str_for_ipv4_netloc(conn.server_addr, conn.server_port); + string client_netloc_str = str_for_ipv4_netloc(c->ipv4_addr, conn.client_port); + int fd = bufferevent_getfd(c->bev.get()); + return string_printf("%d+%016" PRIX64 " (%s -> %s)", + fd, key, client_netloc_str.c_str(), server_netloc_str.c_str()); +} + + + +IPStackSimulator::IPStackSimulator( + std::shared_ptr base, + std::shared_ptr game_server, + std::shared_ptr proxy_server, + std::shared_ptr state) + : base(base), + game_server(game_server), + proxy_server(proxy_server), + state(state), + pcap_text_log_file(state->ip_stack_debug ? fopen("IPStackSimulator-Log.txt", "wt") : nullptr) { + memset(this->host_mac_address_bytes, 0x90, 6); + memset(this->broadcast_mac_address_bytes, 0xFF, 6); +} + +IPStackSimulator::~IPStackSimulator() { + if (this->pcap_text_log_file) { + fclose(this->pcap_text_log_file); + } +} + + + +void IPStackSimulator::listen(const std::string& socket_path) { + this->add_socket(::listen(socket_path, 0, SOMAXCONN)); +} + +void IPStackSimulator::listen(const std::string& addr, int port) { + this->add_socket(::listen(addr, port, SOMAXCONN)); +} + +void IPStackSimulator::listen(int port) { + this->add_socket(::listen("", port, SOMAXCONN)); +} + +void IPStackSimulator::add_socket(int fd) { + this->listeners.emplace( + evconnlistener_new( + this->base.get(), + IPStackSimulator::dispatch_on_listen_accept, + this, + LEV_OPT_REUSEABLE, + 0, + fd), + evconnlistener_free); +} + + + +uint32_t IPStackSimulator::connect_address_for_remote_address(uint32_t remote_addr) { + // Use and address not on the same subnet as the client, so that PSO Plus and + // Episode III will think they're talking to a remote network and won't reject + // the connection. + if ((remote_addr & 0xFF000000) != 0x23000000) { + return 0x23232323; + } else { + return 0x24242424; + } +} + + + +IPStackSimulator::IPClient::IPClient(struct bufferevent* bev) + : bev(bev, bufferevent_free), ipv4_addr(0) { + memset(this->mac_addr, 0, 6); +} + + + +static void flush_and_free_bufferevent(struct bufferevent* bev) { + bufferevent_flush(bev, EV_READ | EV_WRITE, BEV_FINISHED); + bufferevent_free(bev); +} + +IPStackSimulator::IPClient::TCPConnection::TCPConnection() + : server_bev(nullptr, flush_and_free_bufferevent), + pending_data(evbuffer_new(), evbuffer_free), + resend_push_event(nullptr, event_free) { } + + + +void IPStackSimulator::dispatch_on_listen_accept( + struct evconnlistener* listener, evutil_socket_t fd, + struct sockaddr *address, int socklen, void* ctx) { + reinterpret_cast(ctx)->on_listen_accept( + listener, fd, address, socklen); +} + +void IPStackSimulator::on_listen_accept(struct evconnlistener* listener, + evutil_socket_t fd, struct sockaddr*, int) { + int listen_fd = evconnlistener_get_fd(listener); + log(INFO, "[IPStackSimulator] Client fd %d connected via fd %d", + fd, listen_fd); + + struct bufferevent *bev = bufferevent_socket_new(this->base.get(), fd, + BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); + shared_ptr c(new IPClient(bev)); + c->sim = this; + this->bev_to_client.emplace(make_pair(bev, c)); + + bufferevent_setcb(bev, &IPStackSimulator::dispatch_on_client_input, NULL, + &IPStackSimulator::dispatch_on_client_error, this); + bufferevent_enable(bev, EV_READ | EV_WRITE); +} + +void IPStackSimulator::dispatch_on_listen_error( + struct evconnlistener* listener, void* ctx) { + reinterpret_cast(ctx)->on_listen_error(listener); +} + +void IPStackSimulator::on_listen_error(struct evconnlistener* listener) { + int err = EVUTIL_SOCKET_ERROR(); + log(ERROR, "[IPStackSimulator] Failure on listening socket %d: %d (%s)", + evconnlistener_get_fd(listener), err, evutil_socket_error_to_string(err)); + event_base_loopexit(this->base.get(), NULL); +} + + + +void IPStackSimulator::dispatch_on_client_input( + struct bufferevent* bev, void* ctx) { + reinterpret_cast(ctx)->on_client_input(bev); +} + +void IPStackSimulator::on_client_input(struct bufferevent* bev) { + struct evbuffer* buf = bufferevent_get_input(bev); + + shared_ptr c; + try { + c = this->bev_to_client.at(bev); + } catch (const out_of_range&) { + size_t bytes = evbuffer_get_length(buf); + log(ERROR, "[IPStackSimulator] Ignoring data received from unregistered client (0x%zX bytes)", + bytes); + evbuffer_drain(buf, bytes); + return; + } + + while (evbuffer_get_length(buf) >= 2) { + uint16_t frame_size; + evbuffer_copyout(buf, &frame_size, 2); + if (evbuffer_get_length(buf) < frame_size + 2) { + break; // No complete frame available; done for now + } + + evbuffer_drain(buf, 2); + string frame(frame_size, '\0'); + evbuffer_remove(buf, frame.data(), frame.size()); + + try { + this->on_client_frame(c, frame); + } catch (const exception& e) { + log(WARNING, "[IPStackSimulator] Failed to process client frame: %s", e.what()); + print_data(stderr, frame); + } + } +} + +void IPStackSimulator::dispatch_on_client_error( + struct bufferevent* bev, short events, void* ctx) { + reinterpret_cast(ctx)->on_client_error(bev, events); +} +void IPStackSimulator::on_client_error(struct bufferevent* bev, + short events) { + if (events & BEV_EVENT_ERROR) { + int err = EVUTIL_SOCKET_ERROR(); + log(WARNING, "[IPStackSimulator] Client caused error %d (%s)", err, + evutil_socket_error_to_string(err)); + } + if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { + log(INFO, "[IPStackSimulator] Client fd %d disconnected", + bufferevent_getfd(bev)); + + this->bev_to_client.erase(bev); + } +} + + + +void IPStackSimulator::on_client_frame( + shared_ptr c, const string& frame) { + if (this->state->ip_stack_debug) { + fputc('\n', stderr); + log(INFO, "[IPStackSimulator] Client sent frame"); + print_data(stderr, frame); + } + this->log_frame(frame); + + FrameInfo fi(frame); + if (this->state->ip_stack_debug) { + string fi_header = fi.header_str(); + log(INFO, "[IPStackSimulator] Frame header: %s", fi_header.c_str()); + } + + if (fi.arp) { + this->on_client_arp_frame(c, fi); + + } else if (fi.ipv4) { + uint16_t expected_ipv4_checksum = fi.computed_ipv4_header_checksum(); + if (fi.ipv4->checksum != expected_ipv4_checksum) { + throw runtime_error(string_printf( + "IPv4 header checksum is incorrect (%04hX expected, %04hX received)", + expected_ipv4_checksum, fi.ipv4->checksum.load())); + } + if (memcmp(fi.ether->src_mac, c->mac_addr, 6)) { + throw runtime_error("client sent IPv4 packet from different MAC address"); + } + if (fi.ipv4->src_addr != c->ipv4_addr) { + throw runtime_error("client sent IPv4 packet from different IPv4 address"); + } + + if (fi.udp) { + uint16_t expected_udp_checksum = fi.computed_udp4_checksum(); + if (fi.udp->checksum != expected_udp_checksum) { + throw runtime_error(string_printf( + "UDP checksum is incorrect (%04hX expected, %04hX received)", + expected_udp_checksum, fi.udp->checksum.load())); + } + this->on_client_udp_frame(c, fi); + + } else if (fi.tcp) { + uint16_t expected_tcp_checksum = fi.computed_tcp4_checksum(); + if (fi.tcp->checksum != expected_tcp_checksum) { + throw runtime_error(string_printf( + "TCP checksum is incorrect (%04hX expected, %04hX received)", + expected_tcp_checksum, fi.tcp->checksum.load())); + } + this->on_client_tcp_frame(c, fi); + + } else { + throw runtime_error("frame uses unsupported IPv4 protocol"); + } + + } else { + throw runtime_error("frame is not IPv4"); + } +} + + + +void IPStackSimulator::on_client_arp_frame( + shared_ptr c, const FrameInfo& fi) { + if (fi.arp->hwaddr_len != 6 || + fi.arp->paddr_len != 4 || + fi.arp->hardware_type != 0x0001 || + fi.arp->protocol_type != 0x0800) { + throw runtime_error("unsupported ARP parameters"); + } + if (fi.payload_size < 20) { + throw runtime_error("ARP payload too small"); + } + + // Populate the client's addresses if needed + if (!memcmp(c->mac_addr, "\0\0\0\0\0\0", 6)) { + memcpy(c->mac_addr, fi.ether->src_mac, 6); + } + if (c->ipv4_addr == 0) { + c->ipv4_addr = *reinterpret_cast( + reinterpret_cast(fi.payload) + 6); + } + + EthernetHeader r_ether; + memcpy(r_ether.dest_mac, fi.ether->src_mac, 6); + memcpy(r_ether.src_mac, this->host_mac_address_bytes, 6); + r_ether.protocol = fi.ether->protocol; + + ARPHeader r_arp; + r_arp.hardware_type = fi.arp->hardware_type; + r_arp.protocol_type = fi.arp->protocol_type; + r_arp.hwaddr_len = 6; + r_arp.paddr_len = 4; + r_arp.operation = 0x0002; + + // The incoming payload is: + // uint8_t src_mac[6]; // MAC address of client + // uint8_t src_ip[4]; // IP address of client + // uint8_t dest_mac[6]; // MAC address of host (all zeroes) + // uint8_t dest_ip[4]; // IP address of host + // The outgoing payload is: + // uint8_t dest_mac[6]; // MAC address of host (from configuration) + // uint8_t dest_ip[4]; // IP address of host + // uint8_t src_mac[6]; // MAC address of client + // uint8_t src_ip[4]; // IP address of client + + const char* payload_bytes = reinterpret_cast(fi.payload); + + uint8_t r_payload[20]; + memcpy(&r_payload[0], this->host_mac_address_bytes, 6); + memcpy(&r_payload[6], payload_bytes + 16, 4); + memcpy(&r_payload[10], payload_bytes, 10); + + struct evbuffer* out_buf = bufferevent_get_output(c->bev.get()); + + uint16_t frame_size = sizeof(r_ether) + sizeof(r_arp) + sizeof(r_payload); + evbuffer_add(out_buf, &frame_size, 2); + evbuffer_add(out_buf, &r_ether, sizeof(r_ether)); + evbuffer_add(out_buf, &r_arp, sizeof(r_arp)); + evbuffer_add(out_buf, r_payload, sizeof(r_payload)); + + if (this->state->ip_stack_debug) { + log(INFO, "[IPStackSimulator] Sending ARP response"); + } + + if (this->pcap_text_log_file) { + StringWriter w; + w.write(&r_ether, sizeof(r_ether)); + w.write(&r_arp, sizeof(r_arp)); + w.write(r_payload, sizeof(r_payload)); + this->log_frame(w.str()); + } +} + + + +void IPStackSimulator::on_client_udp_frame( + shared_ptr c, const FrameInfo& fi) { + // We only implement the DNS server here + if (fi.udp->dest_port != 53) { + throw runtime_error("UDP packet is not DNS"); + } + if (fi.payload_size < 0x0C) { + throw runtime_error("DNS payload too small"); + } + + EthernetHeader r_ether; + memcpy(r_ether.dest_mac, fi.ether->src_mac, 6); + memcpy(r_ether.src_mac, this->host_mac_address_bytes, 6); + r_ether.protocol = fi.ether->protocol; + + IPv4Header r_ipv4; + r_ipv4.version_ihl = 0x45; + r_ipv4.tos = 0; + // r_ipv4.size filled in later + r_ipv4.id = 0; + r_ipv4.frag_offset = 0; + r_ipv4.ttl = 20; // TODO: Does this value actually matter? Looks like it just has to be nonzero + r_ipv4.protocol = 17; // UDP + // r_ipv4.checksum filled in later + r_ipv4.src_addr = fi.ipv4->dest_addr; + r_ipv4.dest_addr = fi.ipv4->src_addr; + + UDPHeader r_udp; + r_udp.src_port = fi.udp->dest_port; + r_udp.dest_port = fi.udp->src_port; + // r_udp.size filled in later + // r_udp.checksum filled in later + + uint32_t resolved_address = this->connect_address_for_remote_address(c->ipv4_addr); + + string r_data = DNSServer::response_for_query( + fi.payload, fi.payload_size, resolved_address); + + r_ipv4.size = sizeof(IPv4Header) + sizeof(UDPHeader) + r_data.size(); + r_udp.size = sizeof(UDPHeader) + r_data.size(); + r_ipv4.checksum = FrameInfo::computed_ipv4_header_checksum(r_ipv4); + r_udp.checksum = FrameInfo::computed_udp4_checksum( + r_ipv4, r_udp, r_data.data(), r_data.size()); + + struct evbuffer* out_buf = bufferevent_get_output(c->bev.get()); + + if (this->state->ip_stack_debug) { + string remote_str = this->str_for_ipv4_netloc(fi.ipv4->src_addr, fi.udp->src_port); + log(INFO, "[IPStackSimulator] Sending DNS response to %s", remote_str.c_str()); + } + + uint16_t frame_size = sizeof(r_ether) + sizeof(r_ipv4) + sizeof(r_udp) + r_data.size(); + evbuffer_add(out_buf, &frame_size, 2); + evbuffer_add(out_buf, &r_ether, sizeof(r_ether)); + evbuffer_add(out_buf, &r_ipv4, sizeof(r_ipv4)); + evbuffer_add(out_buf, &r_udp, sizeof(r_udp)); + evbuffer_add(out_buf, r_data.data(), r_data.size()); + + if (this->pcap_text_log_file) { + StringWriter w; + w.write(&r_ether, sizeof(r_ether)); + w.write(&r_ipv4, sizeof(r_ipv4)); + w.write(&r_udp, sizeof(r_udp)); + w.write(r_data.data(), r_data.size()); + this->log_frame(w.str()); + } +} + + + +uint64_t IPStackSimulator::tcp_conn_key_for_connection( + const IPClient::TCPConnection& conn) { + return (static_cast(conn.server_addr) << 32) | + (static_cast(conn.server_port) << 16) | + static_cast(conn.client_port); +} + +uint64_t IPStackSimulator::tcp_conn_key_for_client_frame( + const IPv4Header& ipv4, const TCPHeader& tcp) { + return (static_cast(ipv4.dest_addr) << 32) | + (static_cast(tcp.dest_port) << 16) | + static_cast(tcp.src_port); +} + +uint64_t IPStackSimulator::tcp_conn_key_for_client_frame(const FrameInfo& fi) { + if (!fi.ipv4 || !fi.tcp) { + throw logic_error("tcp_conn_key_for_frame called on non-TCP frame"); + } + return IPStackSimulator::tcp_conn_key_for_client_frame(*fi.ipv4, *fi.tcp); +} + + +void IPStackSimulator::on_client_tcp_frame( + shared_ptr c, const FrameInfo& fi) { + if (this->state->ip_stack_debug) { + log(INFO, "[IPStackSimulator] Client sent TCP frame (seq=%08" PRIX32 ", ack=%08" PRIX32 ")", + fi.tcp->seq_num.load(), fi.tcp->ack_num.load()); + } + + if (fi.tcp->flags & (TCPHeader::Flag::NS | TCPHeader::Flag::CWR | + TCPHeader::Flag::ECE | TCPHeader::Flag::URG)) { + throw runtime_error("unsupported flag in TCP packet"); + } + + if (fi.tcp->flags & TCPHeader::Flag::SYN) { + // We never make connections back to the client, so we should never receive + // a SYN+ACK. Essentially, no other flags should be set in any received SYN. + if ((fi.tcp->flags & 0x0FFF) != TCPHeader::Flag::SYN) { + throw runtime_error("TCP SYN contains extra flags"); + } + + uint64_t key = this->tcp_conn_key_for_client_frame(fi); + if (c->tcp_connections.count(key)) { + throw runtime_error("TCP SYN received for already-open connection"); + } + + StringReader options_r(fi.tcp + 1, fi.tcp_options_size); + size_t max_frame_size = 1400; + while (!options_r.eof()) { + uint8_t option = options_r.get_u8(); + uint8_t option_size = (option < 2) ? 1 : options_r.get_u8(); + switch (option) { + case 0: // End of options list + options_r.go(options_r.size()); + break; + case 1: // No option (padding) + break; + case 2: // Max segment size + if (option_size != 4) { + throw runtime_error("incorrect size for TCP max frame size option"); + } + max_frame_size = options_r.get_u16r(); + break; + case 3: // Window scale (ignored) + if (option_size != 3) { + throw runtime_error("incorrect size for TCP window scale option"); + } + options_r.skip(option_size); + break; + case 4: // Selective ACK supported (ignored) + if (option_size != 2) { + throw runtime_error("incorrect size for TCP selective ACK supported option"); + } + break; + case 5: // Selective ACK (ignored) + options_r.skip(option_size - 2); + break; + case 8: // Timestamps (ignored) + if (option_size != 10) { + throw runtime_error("incorrect size for TCP timestamps option"); + } + options_r.skip(8); + break; + default: + throw runtime_error("invalid TCP option"); + } + } + + // Set up the IPStackSimulator end of the virtual connection + auto& conn = c->tcp_connections.emplace(key, IPClient::TCPConnection()).first->second; + conn.client = c; + conn.resend_push_event.reset(event_new(this->base.get(), -1, EV_TIMEOUT, + &IPStackSimulator::dispatch_on_resend_push, &conn)); + conn.server_addr = fi.ipv4->dest_addr; + conn.server_port = fi.tcp->dest_port; + conn.client_port = fi.tcp->src_port; + conn.next_client_seq = fi.tcp->seq_num + 1; + conn.acked_server_seq = random_object(); + conn.resend_push_usecs = DEFAULT_RESEND_PUSH_USECS; + conn.awaiting_first_ack = true; + conn.max_frame_size = max_frame_size; + + string conn_str = this->state->ip_stack_debug ? this->str_for_tcp_connection(c, conn) : ""; + if (this->state->ip_stack_debug) { + log(INFO, "[IPStackSimulator] Client opened TCP connection %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ")", + conn_str.c_str(), conn.acked_server_seq, conn.next_client_seq); + } + + // Send a SYN+ACK (send_tcp_frame always adds the ACK flag) + this->send_tcp_frame(c, conn, TCPHeader::Flag::SYN); + if (this->state->ip_stack_debug) { + log(INFO, "[IPStackSimulator] Sent SYN+ACK on %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ")", + conn_str.c_str(), conn.acked_server_seq, conn.next_client_seq); + } + + } else { + // This frame isn't a SYN, so a connection object should already exist + uint64_t key = this->tcp_conn_key_for_client_frame(fi); + IPClient::TCPConnection* conn; + try { + conn = &c->tcp_connections.at(key); + } catch (const out_of_range&) { + throw runtime_error("non-SYN frame does not correspond to any open TCP connection"); + } + bool conn_valid = true; + + if (fi.tcp->flags & TCPHeader::Flag::ACK) { + if (this->state->ip_stack_debug) { + log(INFO, "[IPStackSimulator] Client sent ACK %08" PRIX32, fi.tcp->ack_num.load()); + } + if (conn->awaiting_first_ack) { + if (fi.tcp->ack_num != conn->acked_server_seq + 1) { + throw runtime_error("first ack_num was not acked_server_seq + 1"); + } + conn->acked_server_seq++; + conn->awaiting_first_ack = false; + + } else { + if (seq_num_greater(fi.tcp->ack_num, conn->acked_server_seq)) { + if (this->state->ip_stack_debug) { + log(INFO, "[IPStackSimulator] Advancing acked_server_seq from %08" PRIX32, conn->acked_server_seq); + } + uint32_t ack_delta = fi.tcp->ack_num - conn->acked_server_seq; + size_t pending_bytes = evbuffer_get_length(conn->pending_data.get()); + if (pending_bytes < ack_delta) { + throw runtime_error("client acknowledged beyond end of sent data"); + } + + evbuffer_drain(conn->pending_data.get(), ack_delta); + conn->acked_server_seq += ack_delta; + conn->resend_push_usecs = DEFAULT_RESEND_PUSH_USECS; + + if (this->state->ip_stack_debug) { + log(INFO, "[IPStackSimulator] Removed %08" PRIX32 " bytes from pending buffer and advanced acked_server_seq to %08" PRIX32, + ack_delta, conn->acked_server_seq); + } + + } else if (seq_num_less(fi.tcp->ack_num, conn->acked_server_seq)) { + throw runtime_error("client sent lower ack num than previous frame"); + } + } + + if (!conn->server_bev.get()) { + this->open_server_connection(c, *conn); + } + } + + if (fi.tcp->flags & (TCPHeader::Flag::RST | TCPHeader::Flag::FIN)) { + bool is_rst = (fi.tcp->flags & TCPHeader::Flag::RST); + if (is_rst && (fi.tcp->flags & TCPHeader::Flag::FIN)) { + throw runtime_error("client sent TCP FIN+RST"); + } + + if (this->state->ip_stack_debug) { + string conn_str = this->str_for_tcp_connection(c, *conn); + log(INFO, "[IPStackSimulator] Client closed TCP connection %s", conn_str.c_str()); + } + + // TODO: Are we supposed to send a response to an RST? Here we do, and the + // client probably just ignores it anyway + this->send_tcp_frame(c, *conn, fi.tcp->flags & (TCPHeader::Flag::RST | TCPHeader::Flag::FIN)); + + // Delete the connection object. The unique_ptr destructor flushes the + // bufferevent, and thereby sends an EOF to the server's end. + c->tcp_connections.erase(key); + conn_valid = false; + + // Note: The PSH flag isn't required to be set on all packets that contain + // data. The PSH flag just means "tell the application that data is + // available", so some senders only set the PSH flag on the last frame of a + // large segment of data, since the application wouldn't be able to process + // the segment until all of it is available. newserv can handle incomplete + // commands, so we just ignore the PSH flag and forward any data to the + // server immediately. + } else if (fi.payload_size != 0) { + + string conn_str = this->state->ip_stack_debug ? this->str_for_tcp_connection(c, *conn) : ""; + + size_t payload_skip_bytes; + if (fi.tcp->seq_num == conn->next_client_seq) { + payload_skip_bytes = 0; + + } else if (seq_num_less(fi.tcp->seq_num, conn->next_client_seq)) { + // If the frame overlaps an existing boundary, we'll accept some of the + // data; otherwise we'll ignore it entirely (but still send an ACK) + uint32_t end_seq = fi.tcp->seq_num + fi.payload_size; + if (seq_num_less_or_equal(end_seq, conn->next_client_seq)) { // Fully "in the past" + payload_skip_bytes = fi.payload_size; + } else { // Partially "in the past" + payload_skip_bytes = fi.payload_size - (end_seq - conn->next_client_seq); + } + + } else { + // Payload is in the future - we must have missed a data frame. We'll + // ignore it (but warn) and send an ACK later, and the client should + // retransmit the lost data + if (this->state->ip_stack_debug) { + log(WARNING, + "[IPStackSimulator] Client sent out-of-order sequence number (expected %08" PRIX32 ", received %08" PRIX32 ", 0x%zX data bytes)", + conn->next_client_seq, fi.tcp->seq_num.load(), fi.payload_size); + } + payload_skip_bytes = fi.payload_size; + } + + if (payload_skip_bytes > fi.payload_size) { + throw logic_error("payload skip bytes too large"); + } + + if (payload_skip_bytes < fi.payload_size) { + const void* payload = reinterpret_cast(fi.payload) + payload_skip_bytes; + size_t payload_size = fi.payload_size - payload_skip_bytes; + + if (this->state->ip_stack_debug) { + if (payload_skip_bytes) { + log(INFO, "[IPStackSimulator] Client sent data on TCP connection %s, overlapping existing ack'ed data (0x%zX bytes ignored)", + conn_str.c_str(), payload_skip_bytes); + } else { + log(INFO, "[IPStackSimulator] Client sent data on TCP connection %s", + conn_str.c_str()); + } + print_data(stderr, payload, payload_size); + } + + // Send the new data to the server + struct evbuffer* server_out_buf = bufferevent_get_output( + conn->server_bev.get()); + evbuffer_add(server_out_buf, payload, payload_size); + + // Update the sequence number and stats + conn->next_client_seq += payload_size; + conn->bytes_received += payload_size; + } + + // Send an ACK + this->send_tcp_frame(c, *conn); + if (this->state->ip_stack_debug) { + log(INFO, "[IPStackSimulator] Sent PSH ACK on %s (acked_server_seq=%08" PRIX32 ", next_client_seq=%08" PRIX32 ", bytes_received=0x%zX)", + conn_str.c_str(), conn->acked_server_seq, conn->next_client_seq, conn->bytes_received); + } + } + + if (conn_valid) { + // Try to send some more data if the client is waiting on it + this->send_pending_push_frame(c, *conn); + } + } +} + +void IPStackSimulator::open_server_connection( + shared_ptr c, IPClient::TCPConnection& conn) { + if (conn.server_bev.get()) { + throw logic_error("server connection is already open"); + } + + const PortConfiguration* port_config; + try { + port_config = &this->state->numbered_port_configuration.at(conn.server_port); + } catch (const out_of_range&) { + throw logic_error("client connected to port missing from configuration"); + } + + struct bufferevent* bevs[2]; + bufferevent_pair_new(this->base.get(), 0, bevs); + + // Set up the IPStackSimulator end of the virtual connection + bufferevent_setcb(bevs[0], &IPStackSimulator::dispatch_on_server_input, NULL, + &IPStackSimulator::dispatch_on_server_error, &conn); + bufferevent_enable(bevs[0], EV_READ | EV_WRITE); + conn.server_bev.reset(bevs[0]); + + // Link the client to the server - the server sees this as a normal TCP + // connection and treats it as if the client connected to one of its listening + // sockets + if (this->game_server.get()) { + this->game_server->connect_client(bevs[1], c->ipv4_addr, conn.client_port, + port_config->version, port_config->behavior); + } else if (this->proxy_server.get()) { + this->proxy_server->connect_client(bevs[1]); + } + + string conn_str = this->str_for_tcp_connection(c, conn); + log(INFO, "[IPStackSimulator] Connected TCP connection %s to game server", + conn_str.c_str()); +} + +void IPStackSimulator::send_pending_push_frame( + shared_ptr c, IPClient::TCPConnection& conn) { + size_t pending_bytes = evbuffer_get_length(conn.pending_data.get()); + if (!pending_bytes) { + return; + } + + size_t bytes_to_send = min(pending_bytes, conn.max_frame_size); + + if (this->state->ip_stack_debug) { + log(INFO, "[IPStackSimulator] Sending PSH frame with seq_num %08" PRIX32 ", 0x%zX/0x%zX data bytes", + conn.acked_server_seq, bytes_to_send, pending_bytes); + } + + this->send_tcp_frame(c, conn, TCPHeader::Flag::PSH, conn.pending_data.get(), + bytes_to_send); + struct timeval resend_push_timeout = usecs_to_timeval(conn.resend_push_usecs); + event_add(conn.resend_push_event.get(), &resend_push_timeout); + + // If the client isn't responding to our PSHes, back off exponentially up to + // a limit of 5 seconds between PSH frames. This window is reset when + // acked_server_seq changes (that is, when the client has acknowledged any new + // data) + conn.resend_push_usecs *= 2; + if (conn.resend_push_usecs > 5000000) { + conn.resend_push_usecs = 5000000; + } +} + +void IPStackSimulator::send_tcp_frame( + shared_ptr c, + IPClient::TCPConnection& conn, + uint16_t flags, + struct evbuffer* src_buf, + size_t src_bytes) { + if (!src_bytes != !(flags & TCPHeader::Flag::PSH)) { + throw logic_error("data should be given if and only if PSH is given"); + } + + EthernetHeader ether; + memcpy(ether.dest_mac, c->mac_addr, 6); + memcpy(ether.src_mac, this->host_mac_address_bytes, 6); + ether.protocol = 0x0800; // IPv4 + + IPv4Header ipv4; + ipv4.version_ihl = 0x45; + ipv4.tos = 0; + // ipv4.size filled in later + ipv4.id = 0; + ipv4.frag_offset = 0; + ipv4.ttl = 20; + ipv4.protocol = 6; // TCP + // ipv4.checksum filled in later + ipv4.src_addr = conn.server_addr; + ipv4.dest_addr = c->ipv4_addr; + + TCPHeader tcp; + tcp.src_port = conn.server_port; + tcp.dest_port = conn.client_port; + tcp.seq_num = conn.acked_server_seq; + tcp.ack_num = conn.next_client_seq; + tcp.flags = (5 << 12) | TCPHeader::Flag::ACK | flags; + tcp.window = 0x1000; + tcp.urgent_ptr = 0; + // tcp.checksum filled in later + + ipv4.size = sizeof(IPv4Header) + sizeof(TCPHeader) + src_bytes; + ipv4.checksum = FrameInfo::computed_ipv4_header_checksum(ipv4); + + const void* linear_data = src_bytes ? evbuffer_pullup(src_buf, src_bytes) : nullptr; + tcp.checksum = FrameInfo::computed_tcp4_checksum( + ipv4, tcp, linear_data, src_bytes); + + struct evbuffer* out_buf = bufferevent_get_output(c->bev.get()); + + uint16_t frame_size = sizeof(ether) + sizeof(ipv4) + sizeof(tcp) + src_bytes; + evbuffer_add(out_buf, &frame_size, 2); + evbuffer_add(out_buf, ðer, sizeof(ether)); + evbuffer_add(out_buf, &ipv4, sizeof(ipv4)); + evbuffer_add(out_buf, &tcp, sizeof(tcp)); + if (src_bytes) { + evbuffer_add(out_buf, linear_data, src_bytes); + } + + if (this->pcap_text_log_file) { + StringWriter w; + w.write(ðer, sizeof(ether)); + w.write(&ipv4, sizeof(ipv4)); + w.write(&tcp, sizeof(tcp)); + w.write(linear_data, src_bytes); + this->log_frame(w.str()); + } +} + +void IPStackSimulator::dispatch_on_resend_push(evutil_socket_t, short, void* ctx) { + auto* conn = reinterpret_cast(ctx); + auto c = conn->client.lock(); + if (!c.get()) { + log(WARNING, "[IPStackSimulator] Resend push event triggered for deleted client; ignoring"); + } else { + c->sim->on_resend_push(c, *conn); + } +} + +void IPStackSimulator::on_resend_push(shared_ptr c, IPClient::TCPConnection& conn) { + this->send_pending_push_frame(c, conn); +} + +void IPStackSimulator::dispatch_on_server_input(struct bufferevent*, void* ctx) { + auto* conn = reinterpret_cast(ctx); + auto c = conn->client.lock(); + if (!c.get()) { + log(WARNING, "[IPStackSimulator] Server input event triggered for deleted client; ignoring"); + } else { + c->sim->on_server_input(c, *conn); + } +} + +void IPStackSimulator::on_server_input(shared_ptr c, IPClient::TCPConnection& conn) { + struct evbuffer* buf = bufferevent_get_input(conn.server_bev.get()); + if (this->state->ip_stack_debug) { + log(INFO, "[IPStackSimulator] Server input event: 0x%zX bytes to read", + evbuffer_get_length(buf)); + } + + evbuffer_add_buffer(conn.pending_data.get(), buf); + this->send_pending_push_frame(c, conn); +} + +void IPStackSimulator::dispatch_on_server_error( + struct bufferevent*, short events, void* ctx) { + auto* conn = reinterpret_cast(ctx); + auto c = conn->client.lock(); + if (!c.get()) { + log(WARNING, "[IPStackSimulator] Server error event triggered for deleted client; ignoring"); + } else { + c->sim->on_server_error(c, *conn, events); + } +} + +void IPStackSimulator::on_server_error( + shared_ptr c, IPClient::TCPConnection& conn, short events) { + if (events & BEV_EVENT_ERROR) { + int err = EVUTIL_SOCKET_ERROR(); + log(WARNING, "[IPStackSimulator] Received error %d from virtual connection (%s)", err, + evutil_socket_error_to_string(err)); + } + if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { + // Send an RST to the client. Kind of rude (we really should use FIN) but + // the PSO network stack always sends an RST to us when disconnecting, so + // whatever + this->send_tcp_frame(c, conn, TCPHeader::Flag::RST); + + // Delete the connection object (this also flushes and frees the server + // virtual connection bufferevent) + c->tcp_connections.erase(this->tcp_conn_key_for_connection(conn)); + } +} + + + +void IPStackSimulator::log_frame(const string& data) const { + if (this->pcap_text_log_file) { + print_data(this->pcap_text_log_file, data, 0, nullptr, + PrintDataFlags::SkipSeparator); + fputc('\n', this->pcap_text_log_file); + fflush(this->pcap_text_log_file); + } +} diff --git a/src/IPStackSimulator.hh b/src/IPStackSimulator.hh new file mode 100644 index 00000000..f19a3783 --- /dev/null +++ b/src/IPStackSimulator.hh @@ -0,0 +1,144 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "IPFrameInfo.hh" +#include "Server.hh" +#include "ProxyServer.hh" +#include "ServerState.hh" + + + +class IPStackSimulator { +public: + IPStackSimulator( + std::shared_ptr base, + std::shared_ptr game_server, + std::shared_ptr proxy_server, + std::shared_ptr state); + ~IPStackSimulator(); + + void listen(const std::string& socket_path); + void listen(const std::string& addr, int port); + void listen(int port); + void add_socket(int fd); + + static uint32_t connect_address_for_remote_address(uint32_t remote_addr); + +private: + std::shared_ptr base; + std::shared_ptr game_server; + std::shared_ptr proxy_server; + std::shared_ptr state; + + using unique_listener = std::unique_ptr; + using unique_bufferevent = std::unique_ptr; + using unique_evbuffer = std::unique_ptr; + using unique_event = std::unique_ptr; + + struct IPClient { + IPStackSimulator* sim; + + unique_bufferevent bev; + uint8_t mac_addr[6]; + uint32_t ipv4_addr; + + struct TCPConnection { + std::weak_ptr client; + + // The PSO protocol begins with the server sending a command, but we + // shouldn't send a PSH immediately after the SYN+ACK, so the connection + // isn't handed to the Server object until after the 3-way handshake + // (receive SYN, send SYN+ACK, receive ACK). This means server_bev is null + // during the first part of the connection phase. + unique_bufferevent server_bev; + // TODO: Get rid of pending_data and just use server_bev's input buffer in + // its place + unique_evbuffer pending_data; + unique_event resend_push_event; + + bool awaiting_first_ack; + + uint32_t server_addr; + uint16_t server_port; + uint16_t client_port; + uint32_t next_client_seq; + uint32_t acked_server_seq; + size_t resend_push_usecs; + size_t max_frame_size; + size_t bytes_received; + size_t bytes_sent; + + TCPConnection(); + }; + std::unordered_map tcp_connections; + + IPClient(struct bufferevent* bev); + }; + + std::unordered_set listeners; + std::unordered_map> bev_to_client; + + uint8_t host_mac_address_bytes[6]; + uint8_t broadcast_mac_address_bytes[6]; + + FILE* pcap_text_log_file; + + static uint64_t tcp_conn_key_for_connection( + const IPClient::TCPConnection& conn); + static uint64_t tcp_conn_key_for_client_frame( + const IPv4Header& ipv4, const TCPHeader& tcp); + static uint64_t tcp_conn_key_for_client_frame(const FrameInfo& fi); + + static std::string str_for_ipv4_netloc(uint32_t addr, uint16_t port); + static std::string str_for_tcp_connection(std::shared_ptr c, + const IPClient::TCPConnection& conn); + + static void dispatch_on_listen_accept(struct evconnlistener* listener, + evutil_socket_t fd, struct sockaddr *address, int socklen, void* ctx); + void on_listen_accept(struct evconnlistener* listener, evutil_socket_t fd, + struct sockaddr *address, int socklen); + static void dispatch_on_listen_error(struct evconnlistener* listener, void* ctx); + void on_listen_error(struct evconnlistener* listener); + + static void dispatch_on_client_input(struct bufferevent* bev, void* ctx); + void on_client_input(struct bufferevent* bev); + static void dispatch_on_client_error(struct bufferevent* bev, short events, + void* ctx); + void on_client_error(struct bufferevent* bev, short events); + + void on_client_frame(std::shared_ptr c, const std::string& frame); + void on_client_arp_frame(std::shared_ptr c, const FrameInfo& fi); + void on_client_udp_frame(std::shared_ptr c, const FrameInfo& fi); + void on_client_tcp_frame(std::shared_ptr c, const FrameInfo& fi); + + static void dispatch_on_resend_push(evutil_socket_t fd, short events, + void* ctx); + void on_resend_push(std::shared_ptr c, IPClient::TCPConnection& conn); + + static void dispatch_on_server_input(struct bufferevent* bev, void* ctx); + void on_server_input(std::shared_ptr c, IPClient::TCPConnection& conn); + static void dispatch_on_server_error(struct bufferevent* bev, short events, + void* ctx); + void on_server_error(std::shared_ptr c, IPClient::TCPConnection& conn, short events); + + void send_pending_push_frame( + std::shared_ptr c, IPClient::TCPConnection& conn); + void send_tcp_frame( + std::shared_ptr c, + IPClient::TCPConnection& conn, + uint16_t flags = 0, + struct evbuffer* src_buf = nullptr, + size_t src_bytes = 0); + + void open_server_connection( + std::shared_ptr c, IPClient::TCPConnection& conn); + + void log_frame(const std::string& data) const; +}; diff --git a/src/Main.cc b/src/Main.cc index 13a8667e..6b80dfb3 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -20,6 +20,7 @@ #include "Text.hh" #include "ServerShell.hh" #include "ProxyShell.hh" +#include "IPStackSimulator.hh" using namespace std; @@ -83,7 +84,7 @@ void populate_state_from_config(shared_ptr s, } catch (const out_of_range&) { } // TODO: make this configurable - s->port_configuration = default_port_to_behavior; + s->set_port_configuration(default_port_to_behavior); auto enemy_categories = parse_int_vector(d.at("CommonItemDropRates-Enemy")); auto box_categories = parse_int_vector(d.at("CommonItemDropRates-Box")); @@ -140,11 +141,20 @@ void populate_state_from_config(shared_ptr s, s->all_addresses.emplace("", s->external_address); try { - s->dns_server_port = d.at("RunDNSServer")->as_bool(); + s->dns_server_port = d.at("DNSServerPort")->as_int(); } catch (const out_of_range&) { s->dns_server_port = 0; } + try { + for (const auto& item : d.at("IPStackListen")->as_list()) { + s->ip_stack_addresses.emplace_back(item->as_string()); + } + } catch (const out_of_range&) { } + try { + s->ip_stack_debug = d.at("IPStackDebug")->as_bool(); + } catch (const out_of_range&) { } + try { s->allow_unregistered_users = d.at("AllowUnregisteredUsers")->as_bool(); } catch (const out_of_range&) { @@ -265,7 +275,7 @@ int main(int argc, char* argv[]) { } else { log(INFO, "Starting game server"); game_server.reset(new Server(base, state)); - for (const auto& it : state->port_configuration) { + for (const auto& it : state->named_port_configuration) { game_server->listen("", it.second.port, it.second.version, it.second.behavior); } @@ -282,6 +292,17 @@ int main(int argc, char* argv[]) { state->quest_index.reset(new QuestIndex("system/quests")); } + shared_ptr ip_stack_simulator; + if (!state->ip_stack_addresses.empty()) { + log(INFO, "Starting IP stack simulator"); + ip_stack_simulator.reset(new IPStackSimulator( + base, game_server, proxy_server, state)); + for (const auto& it : state->ip_stack_addresses) { + auto netloc = parse_netloc(it); + ip_stack_simulator->listen(netloc.first, netloc.second); + } + } + if (!state->username.empty()) { log(INFO, "Switching to user %s", state->username.c_str()); drop_privileges(state->username); diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index 7544dad7..62b378f8 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -27,10 +27,17 @@ using namespace std; +static void flush_and_free_bufferevent(struct bufferevent* bev) { + bufferevent_flush(bev, EV_READ | EV_WRITE, BEV_FINISHED); + bufferevent_free(bev); +} + + + ProxyServer::ProxyServer(shared_ptr base, const struct sockaddr_storage& initial_destination, GameVersion version) : - base(base), client_bev(nullptr, bufferevent_free), - server_bev(nullptr, bufferevent_free), + base(base), client_bev(nullptr, flush_and_free_bufferevent), + server_bev(nullptr, flush_and_free_bufferevent), next_destination(initial_destination), version(version), header_size((version == GameVersion::BB) ? 8 : 4) { memset(&this->client_input_header, 0, sizeof(this->client_input_header)); @@ -110,24 +117,37 @@ void ProxyServer::dispatch_on_server_error(struct bufferevent* bev, short events void ProxyServer::on_listen_accept(struct evconnlistener*, evutil_socket_t fd, struct sockaddr*, int) { - if (this->client_bev.get()) { - log(WARNING, "Ignoring client connection because client already exists"); + log(WARNING, "[ProxyServer] Ignoring client connection because client already exists"); close(fd); return; - } else { - log(INFO, "Client connected"); } - this->client_bev.reset(bufferevent_socket_new(this->base.get(), fd, + log(INFO, "[ProxyServer] Client connected on fd %d", fd); + this->on_client_connect(bufferevent_socket_new(this->base.get(), fd, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS)); +} + +void ProxyServer::connect_client(struct bufferevent* bev) { + if (this->client_bev.get()) { + log(WARNING, "[ProxyServer] Ignoring client virtual connection because client already exists"); + bufferevent_flush(bev, EV_WRITE, BEV_FINISHED); + return; + } + + log(INFO, "[ProxyServer] Client connected on virtual connection %p", bev); + this->on_client_connect(bev); +} + +void ProxyServer::on_client_connect(struct bufferevent* bev) { + this->client_bev.reset(bev); bufferevent_setcb(this->client_bev.get(), &ProxyServer::dispatch_on_client_input, NULL, &ProxyServer::dispatch_on_client_error, this); bufferevent_enable(this->client_bev.get(), EV_READ | EV_WRITE); - // connect to the server, disconnecting first if needed + // Connect to the server, disconnecting first if needed this->server_bev.reset(bufferevent_socket_new(this->base.get(), -1, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS)); @@ -144,7 +164,7 @@ void ProxyServer::on_listen_accept(struct evconnlistener*, evutil_socket_t fd, sin.sin_addr.s_addr = sin_ss->sin_addr.s_addr; string netloc_str = render_sockaddr_storage(this->next_destination); - log(INFO, "connecting to %s", netloc_str.c_str()); + log(INFO, "[ProxyServer] Connecting to %s", netloc_str.c_str()); if (bufferevent_socket_connect(this->server_bev.get(), reinterpret_cast(&sin), sizeof(sin)) != 0) { throw runtime_error(string_printf("failed to connect (%d)", EVUTIL_SOCKET_ERROR())); @@ -157,7 +177,7 @@ void ProxyServer::on_listen_accept(struct evconnlistener*, evutil_socket_t fd, void ProxyServer::on_listen_error(struct evconnlistener* listener) { int err = EVUTIL_SOCKET_ERROR(); - log(ERROR, "failure on listening socket %d: %d (%s)", + log(ERROR, "[ProxyServer] Failure on listening socket %d: %d (%s)", evconnlistener_get_fd(listener), err, evutil_socket_error_to_string(err)); event_base_loopexit(this->base.get(), NULL); } @@ -173,11 +193,11 @@ void ProxyServer::on_server_input(struct bufferevent*) { void ProxyServer::on_client_error(struct bufferevent*, short events) { if (events & BEV_EVENT_ERROR) { int err = EVUTIL_SOCKET_ERROR(); - log(WARNING, "error %d (%s) in client stream", err, + log(WARNING, "[ProxyServer] Error %d (%s) in client stream", err, evutil_socket_error_to_string(err)); } if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { - log(INFO, "client has disconnected"); + log(INFO, "[ProxyServer] Client has disconnected"); this->client_bev.reset(); // "forward" the disconnection to the server this->server_bev.reset(); @@ -193,11 +213,11 @@ void ProxyServer::on_client_error(struct bufferevent*, short events) { void ProxyServer::on_server_error(struct bufferevent*, short events) { if (events & BEV_EVENT_ERROR) { int err = EVUTIL_SOCKET_ERROR(); - log(WARNING, "error %d (%s) in server stream", err, + log(WARNING, "[ProxyServer] Error %d (%s) in server stream", err, evutil_socket_error_to_string(err)); } if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { - log(INFO, "server has disconnected"); + log(INFO, "[ProxyServer] Server has disconnected"); this->server_bev.reset(); // "forward" the disconnection to the client this->client_bev.reset(); @@ -259,7 +279,7 @@ void ProxyServer::receive_and_process_commands(bool from_server) { break; } - //log(INFO, "[ProxyServer-debug] received encrypted header"); + //log(INFO, "[ProxyServer-debug] Received encrypted header"); //print_data(stderr, input_header, this->header_size); if (source_crypt) { @@ -269,7 +289,7 @@ void ProxyServer::receive_and_process_commands(bool from_server) { size_t command_size = this->get_size_field(input_header); if (evbuffer_get_length(source_buf) < command_size) { - //log(INFO, "[ProxyServer-debug] insufficient data for command (%zX/%hX bytes)", evbuffer_get_length(source_buf), this->get_size_field(input_header)); + //log(INFO, "[ProxyServer-debug] Insufficient data for command (%zX/%hX bytes)", evbuffer_get_length(source_buf), this->get_size_field(input_header)); break; } @@ -278,11 +298,11 @@ void ProxyServer::receive_and_process_commands(bool from_server) { if (bytes < static_cast(command_size)) { throw logic_error("enough bytes available, but could not remove them"); } - //log(INFO, "[ProxyServer-debug] read command (%zX bytes)", bytes); + //log(INFO, "[ProxyServer-debug] Read command (%zX bytes)", bytes); // overwrite the header with the already-decrypted header memcpy(command.data(), input_header, this->header_size); - //log(INFO, "[ProxyServer-debug] received encrypted command with pre-decrypted header"); + //log(INFO, "[ProxyServer-debug] Received encrypted command with pre-decrypted header"); //print_data(stderr, command); if (source_crypt) { @@ -290,7 +310,7 @@ void ProxyServer::receive_and_process_commands(bool from_server) { command_size - this->header_size); } - log(INFO, "%s:", from_server ? "server" : "client"); + log(INFO, "[ProxyServer] %s:", from_server ? "server" : "client"); print_data(stderr, command); // preprocess the command if needed @@ -353,20 +373,25 @@ void ProxyServer::receive_and_process_commands(bool from_server) { sin->sin_addr.s_addr = args->address; // already network byte order if (!dest_bev) { - log(WARNING, "received reconnect command with no destination present"); + log(WARNING, "[ProxyServer] Received reconnect command with no destination present"); } else { struct sockaddr_storage sockname_ss; socklen_t len = sizeof(sockname_ss); - getsockname(bufferevent_getfd(dest_bev), - reinterpret_cast(&sockname_ss), &len); - if (sockname_ss.ss_family != AF_INET) { - throw logic_error("existing connection is not ipv4"); - } + int fd = bufferevent_getfd(dest_bev); + if (fd < 0) { // virtual connection + args->address = 0x23232323; // TODO: apply the different-network logic here too + args->port = 9000; + } else { + getsockname(fd, reinterpret_cast(&sockname_ss), &len); + if (sockname_ss.ss_family != AF_INET) { + throw logic_error("existing connection is not ipv4"); + } - struct sockaddr_in* sockname_sin = reinterpret_cast( - &sockname_ss); - args->address = sockname_sin->sin_addr.s_addr; // already network byte order - args->port = this->listeners.begin()->first; + struct sockaddr_in* sockname_sin = reinterpret_cast( + &sockname_ss); + args->address = sockname_sin->sin_addr.s_addr; // Already network byte order + args->port = ntohs(sockname_sin->sin_port); // Client expects this little-endian for some reason + } } break; } @@ -378,12 +403,12 @@ void ProxyServer::receive_and_process_commands(bool from_server) { if (dest_crypt) { dest_crypt->encrypt(command.data(), command.size()); } - //log(INFO, "[ProxyServer-debug] sending encrypted command"); + //log(INFO, "[ProxyServer-debug] Sending encrypted command"); //print_data(stderr, command); evbuffer_add(dest_buf, command.data(), command.size()); } else { - log(WARNING, "no destination present; dropping command"); + log(WARNING, "[ProxyServer] No destination present; dropping command"); } // clear the input header so we can read the next command diff --git a/src/ProxyServer.hh b/src/ProxyServer.hh index 6880052d..fbad2087 100644 --- a/src/ProxyServer.hh +++ b/src/ProxyServer.hh @@ -24,6 +24,8 @@ public: void listen(int port); + void connect_client(struct bufferevent* bev); + void send_to_client(const std::string& data); void send_to_server(const std::string& data); @@ -64,6 +66,8 @@ private: void on_server_input(struct bufferevent* bev); void on_server_error(struct bufferevent* bev, short events); + void on_client_connect(struct bufferevent* bev); + size_t get_size_field(const PSOCommandHeader* header); size_t get_command_field(const PSOCommandHeader* header); diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index d9f22ae7..0fbcc034 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -43,8 +43,8 @@ enum ClientStateBB { void process_connect(std::shared_ptr s, std::shared_ptr c) { switch (c->server_behavior) { case ServerBehavior::SplitReconnect: { - uint16_t pc_port = s->port_configuration.at("pc-login").port; - uint16_t gc_port = s->port_configuration.at("gc-jp10").port; + uint16_t pc_port = s->named_port_configuration.at("pc-login").port; + uint16_t gc_port = s->named_port_configuration.at("gc-jp10").port; send_pc_gc_split_reconnect(c, s->connect_address_for_client(c), pc_port, gc_port); c->should_disconnect = true; break; @@ -373,7 +373,7 @@ void process_login_bb(shared_ptr s, shared_ptr c, case ClientStateBB::InitialLogin: // first login? send them to the other port send_reconnect(c, s->connect_address_for_client(c), - s->port_configuration.at("bb-data1").port); + s->named_port_configuration.at("bb-data1").port); break; case ClientStateBB::DownloadData: { @@ -399,7 +399,7 @@ void process_login_bb(shared_ptr s, shared_ptr c, default: send_reconnect(c, s->connect_address_for_client(c), - s->port_configuration.at("bb-login").port); + s->named_port_configuration.at("bb-login").port); } } @@ -626,7 +626,7 @@ void process_menu_selection(shared_ptr s, shared_ptr c, const auto& port_name = version_to_port_name.at(static_cast(c->version)); send_reconnect(c, s->connect_address_for_client(c), - s->port_configuration.at(port_name).port); + s->named_port_configuration.at(port_name).port); break; } @@ -842,7 +842,7 @@ void process_change_ship(shared_ptr s, shared_ptr c, const auto& port_name = version_to_port_name.at(static_cast(c->version)); send_reconnect(c, s->connect_address_for_client(c), - s->port_configuration.at(port_name).port); + s->named_port_configuration.at(port_name).port); } void process_change_block(shared_ptr s, shared_ptr c, diff --git a/src/Server.cc b/src/Server.cc index 6ec0a8b0..6bf3be0a 100644 --- a/src/Server.cc +++ b/src/Server.cc @@ -36,10 +36,18 @@ void Server::disconnect_client(shared_ptr c) { struct bufferevent* bev = c->bev; c->bev = NULL; + int fd = bufferevent_getfd(bev); + if (fd < 0) { + log(INFO, "[Server] Client on virtual connection %p disconnected", bev); + } else { + log(INFO, "[Server] Client on fd %d disconnected", fd); + } + // if the output buffer is not empty, move the client into the draining pool // instead of disconnecting it, to make sure all the data gets sent struct evbuffer* out_buffer = bufferevent_get_output(bev); if (evbuffer_get_length(out_buffer) == 0) { + bufferevent_flush(bev, EV_WRITE, BEV_FINISHED); bufferevent_free(bev); } else { // the callbacks will free it when all the data is sent or the client @@ -98,7 +106,7 @@ void Server::on_listen_accept(struct evconnlistener* listener, return; } - log(INFO, "[Server] Client connected via fd %d", listen_fd); + log(INFO, "[Server] Client fd %d connected via fd %d", fd, listen_fd); struct bufferevent *bev = bufferevent_socket_new(this->base.get(), fd, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_DEFER_CALLBACKS); @@ -113,6 +121,28 @@ void Server::on_listen_accept(struct evconnlistener* listener, process_connect(this->state, c); } +void Server::connect_client( + struct bufferevent* bev, uint32_t address, uint16_t port, + GameVersion version, ServerBehavior initial_state) { + log(INFO, "[Server] Client connected on virtual connection %p", bev); + + shared_ptr c(new Client(bev, version, initial_state)); + this->bev_to_client.emplace(make_pair(bev, c)); + + // Manually set the remote address, since the bufferevent has no fd and the + // Client constructor can't figure out the virtual remote address + auto* sin = reinterpret_cast(&c->remote_addr); + sin->sin_family = AF_INET; + sin->sin_addr.s_addr = htonl(address); + sin->sin_port = htons(port); + + bufferevent_setcb(bev, &Server::dispatch_on_client_input, NULL, + &Server::dispatch_on_client_error, this); + bufferevent_enable(bev, EV_READ | EV_WRITE); + + process_connect(this->state, c); +} + void Server::on_listen_error(struct evconnlistener* listener) { int err = EVUTIL_SOCKET_ERROR(); log(ERROR, "[Server] Failure on listening socket %d: %d (%s)", @@ -149,6 +179,7 @@ void Server::on_client_input(struct bufferevent* bev) { } void Server::on_disconnecting_client_output(struct bufferevent* bev) { + bufferevent_flush(bev, EV_WRITE, BEV_FINISHED); bufferevent_free(bev); } @@ -171,6 +202,7 @@ void Server::on_disconnecting_client_error(struct bufferevent* bev, evutil_socket_error_to_string(err)); } if (events & (BEV_EVENT_EOF | BEV_EVENT_ERROR)) { + bufferevent_flush(bev, EV_WRITE, BEV_FINISHED); bufferevent_free(bev); } } @@ -233,7 +265,7 @@ Server::Server(shared_ptr base, void Server::listen(const string& socket_path, GameVersion version, ServerBehavior behavior) { int fd = ::listen(socket_path, 0, SOMAXCONN); - log(INFO, "[Server] Listening on unix socket %s (version %s) on fd %d", + log(INFO, "[Server] Listening on Unix socket %s (%s) on fd %d", socket_path.c_str(), name_for_version(version), fd); this->add_socket(fd, version, behavior); } @@ -242,7 +274,7 @@ void Server::listen(const string& addr, int port, GameVersion version, ServerBehavior behavior) { int fd = ::listen(addr, port, SOMAXCONN); string netloc_str = render_netloc(addr, port); - log(INFO, "[Server] Listening on tcp interface %s (version %s) on fd %d", + log(INFO, "[Server] Listening on TCP interface %s (%s) on fd %d", netloc_str.c_str(), name_for_version(version), fd); this->add_socket(fd, version, behavior); } diff --git a/src/Server.hh b/src/Server.hh index 57e3e38b..fc96f4d9 100644 --- a/src/Server.hh +++ b/src/Server.hh @@ -26,6 +26,9 @@ public: void listen(int port, GameVersion version, ServerBehavior initial_state); void add_socket(int fd, GameVersion version, ServerBehavior initial_state); + void connect_client(struct bufferevent* bev, uint32_t address, uint16_t port, + GameVersion version, ServerBehavior initial_state); + private: std::shared_ptr base; diff --git a/src/ServerState.cc b/src/ServerState.cc index e3b57eec..afc389c5 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -6,6 +6,7 @@ #include "SendCommands.hh" #include "NetworkAddresses.hh" +#include "IPStackSimulator.hh" #include "Text.hh" using namespace std; @@ -14,6 +15,7 @@ using namespace std; ServerState::ServerState() : dns_server_port(0), + ip_stack_debug(false), allow_unregistered_users(false), run_shell_behavior(RunShellBehavior::Default), next_lobby_id(1), pre_lobby_event(0) { @@ -157,11 +159,33 @@ shared_ptr ServerState::find_client(const char16_t* identifier, } uint32_t ServerState::connect_address_for_client(std::shared_ptr c) { - // TODO: we can do something much smarter here, like use the sockname to find - // out which interface the client is connected to, and return that address - if (is_local_address(c->remote_addr)) { - return this->local_address; + if (c->is_virtual_connection) { + if (c->remote_addr.ss_family != AF_INET) { + throw logic_error("virtual connection is missing remote IPv4 address"); + } + const auto* sin = reinterpret_cast(&c->remote_addr); + return IPStackSimulator::connect_address_for_remote_address( + ntohl(sin->sin_addr.s_addr)); } else { - return this->external_address; + // TODO: we can do something smarter here, like use the sockname to find + // out which interface the client is connected to, and return that address + if (is_local_address(c->remote_addr)) { + return this->local_address; + } else { + return this->external_address; + } + } +} + + + +void ServerState::set_port_configuration( + const std::unordered_map& named_port_configuration) { + this->named_port_configuration = named_port_configuration; + this->numbered_port_configuration.clear(); + for (const auto& it : this->named_port_configuration) { + if (!this->numbered_port_configuration.emplace(it.second.port, it.second).second) { + throw runtime_error("duplicate port in configuration"); + } } } diff --git a/src/ServerState.hh b/src/ServerState.hh index c083339c..d004b635 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -32,9 +32,12 @@ struct ServerState { }; std::u16string name; - std::unordered_map port_configuration; + std::unordered_map named_port_configuration; + std::unordered_map numbered_port_configuration; std::string username; uint16_t dns_server_port; + std::vector ip_stack_addresses; + bool ip_stack_debug; bool allow_unregistered_users; RunShellBehavior run_shell_behavior; PSOBBEncryption::KeyFile default_key_file; @@ -78,4 +81,7 @@ struct ServerState { uint64_t serial_number = 0, std::shared_ptr l = NULL); uint32_t connect_address_for_client(std::shared_ptr c); + + void set_port_configuration( + const std::unordered_map& named_port_configuration); }; diff --git a/system/config.json b/system/config.json index 8d5896b9..c73c0e7e 100755 --- a/system/config.json +++ b/system/config.json @@ -3,13 +3,31 @@ // Server's name (maximum 16 characters) "ServerName": "Alexandria", + // Address to connect local clients to (IP address or interface name) "LocalAddress": "192.168.0.5", // Address to connect external clients to (IP address or interface name) "ExternalAddress": "en0", + // Port to listen for DNS queries on. Set this to zero (or comment it out) to // disable the DNS server. "DNSServerPort": 53, + + // Where to listen for IP stack clients. This exists to interface with PSO GC + // clients running in a local Dolphin emulator. To enable local Dolphin + // clients to connect, set this to ["/tmp/dolphin-tap"] and configure Dolphin + // to use the tapserver type of broadband adapter. You do not need to install + // or run tapserver. See README.md for details on how to get PSO to connect + // via this interface. + // If you're doing inadvisable things, you can also add numbers or + // "address:port" strings to this list to listen for tapserver connections on + // a TCP port. + // "IPStackListen": ["/tmp/dolphin-tap"], + + // Set this to true to show a lot of debugging information from the IP stack + // simulator. + // "IPStackDebug": true, + // By default, the interactive shell runs if stdin is a terminal, and doesn't // run if it's not. This option, if present, overrides that behavior. // "RunInteractiveShell": false,