mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2024-06-02 19:28:11 -04:00
982 lines
30 KiB
C++
982 lines
30 KiB
C++
// Copyright 2022 Dolphin Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include "Core/HW/EXI/BBA/BuiltIn.h"
|
|
|
|
#include <bit>
|
|
|
|
#ifdef _WIN32
|
|
#include <ws2ipdef.h>
|
|
#else
|
|
#include <sys/select.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/types.h>
|
|
#endif
|
|
|
|
#include "Common/BitUtils.h"
|
|
#include "Common/Logging/Log.h"
|
|
#include "Common/MsgHandler.h"
|
|
#include "Common/Network.h"
|
|
#include "Common/ScopeGuard.h"
|
|
#include "Core/HW/EXI/EXI_Device.h"
|
|
#include "Core/HW/EXI/EXI_DeviceEthernet.h"
|
|
|
|
namespace
|
|
{
|
|
u64 GetTickCountStd()
|
|
{
|
|
using namespace std::chrono;
|
|
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
|
|
}
|
|
|
|
std::vector<u8> BuildFINFrame(StackRef* ref)
|
|
{
|
|
const Common::TCPPacket result(ref->bba_mac, ref->my_mac, ref->from, ref->to, ref->seq_num,
|
|
ref->ack_num, TCP_FLAG_FIN | TCP_FLAG_ACK | TCP_FLAG_RST);
|
|
|
|
for (auto& tcp_buf : ref->tcp_buffers)
|
|
tcp_buf.used = false;
|
|
return result.Build();
|
|
}
|
|
|
|
std::vector<u8> BuildAckFrame(StackRef* ref)
|
|
{
|
|
const Common::TCPPacket result(ref->bba_mac, ref->my_mac, ref->from, ref->to, ref->seq_num,
|
|
ref->ack_num, TCP_FLAG_ACK);
|
|
return result.Build();
|
|
}
|
|
|
|
// Change the IP identification and recompute the checksum
|
|
void SetIPIdentification(u8* ptr, std::size_t size, u16 value)
|
|
{
|
|
if (size < Common::EthernetHeader::SIZE + Common::IPv4Header::SIZE)
|
|
return;
|
|
|
|
u8* const ip_ptr = ptr + Common::EthernetHeader::SIZE;
|
|
const u8 ip_header_size = (*ip_ptr & 0xf) * 4;
|
|
if (size < Common::EthernetHeader::SIZE + ip_header_size)
|
|
return;
|
|
|
|
u8* const ip_id_ptr = ip_ptr + offsetof(Common::IPv4Header, identification);
|
|
Common::BitCastPtr<u16>(ip_id_ptr) = htons(value);
|
|
|
|
u8* const ip_checksum_ptr = ip_ptr + offsetof(Common::IPv4Header, header_checksum);
|
|
auto checksum_bitcast_ptr = Common::BitCastPtr<u16>(ip_checksum_ptr);
|
|
checksum_bitcast_ptr = u16(0);
|
|
checksum_bitcast_ptr = htons(Common::ComputeNetworkChecksum(ip_ptr, ip_header_size));
|
|
}
|
|
} // namespace
|
|
|
|
namespace ExpansionInterface
|
|
{
|
|
bool CEXIETHERNET::BuiltInBBAInterface::Activate()
|
|
{
|
|
if (IsActivated())
|
|
return true;
|
|
|
|
m_active = true;
|
|
for (auto& buf : m_queue_data)
|
|
buf.reserve(2048);
|
|
|
|
// Workaround to get the host IP (might not be accurate)
|
|
// TODO: Fix the JNI crash and use GetSystemDefaultInterface()
|
|
// - https://pastebin.com/BFpmnxby (see https://dolp.in/pr10920)
|
|
const u32 ip = m_local_ip.empty() ? sf::IpAddress::getLocalAddress().toInteger() :
|
|
sf::IpAddress(m_local_ip).toInteger();
|
|
m_current_ip = htonl(ip);
|
|
m_current_mac = Common::BitCastPtr<Common::MACAddress>(&m_eth_ref->mBbaMem[BBA_NAFR_PAR0]);
|
|
m_arp_table[m_current_ip] = m_current_mac;
|
|
m_router_ip = (m_current_ip & 0xFFFFFF) | 0x01000000;
|
|
m_router_mac = Common::GenerateMacAddress(Common::MACConsumer::BBA);
|
|
m_arp_table[m_router_ip] = m_router_mac;
|
|
|
|
m_network_ref.Clear();
|
|
|
|
m_upnp_httpd.listen(Common::SSDP_PORT, sf::IpAddress(ip));
|
|
m_upnp_httpd.setBlocking(false);
|
|
|
|
return RecvInit();
|
|
}
|
|
|
|
void CEXIETHERNET::BuiltInBBAInterface::Deactivate()
|
|
{
|
|
// Is the BBA Active? If not skip shutdown
|
|
if (!IsActivated())
|
|
return;
|
|
// Signal read thread to exit.
|
|
m_read_enabled.Clear();
|
|
m_read_thread_shutdown.Set();
|
|
m_active = false;
|
|
|
|
m_network_ref.Clear();
|
|
m_arp_table.clear();
|
|
m_upnp_httpd.close();
|
|
|
|
// Wait for read thread to exit.
|
|
if (m_read_thread.joinable())
|
|
m_read_thread.join();
|
|
}
|
|
|
|
bool CEXIETHERNET::BuiltInBBAInterface::IsActivated()
|
|
{
|
|
return m_active;
|
|
}
|
|
|
|
void CEXIETHERNET::BuiltInBBAInterface::WriteToQueue(const std::vector<u8>& data)
|
|
{
|
|
m_queue_data[m_queue_write] = data;
|
|
const u8 next_write_index = (m_queue_write + 1) & 15;
|
|
if (next_write_index != m_queue_read)
|
|
m_queue_write = next_write_index;
|
|
}
|
|
|
|
void CEXIETHERNET::BuiltInBBAInterface::PollData(std::size_t* datasize)
|
|
{
|
|
for (auto& net_ref : m_network_ref)
|
|
{
|
|
if (net_ref.ip == 0)
|
|
continue;
|
|
|
|
// Check for sleeping TCP data
|
|
if (net_ref.type == IPPROTO_TCP)
|
|
{
|
|
for (auto& tcp_buf : net_ref.tcp_buffers)
|
|
{
|
|
if (!tcp_buf.used || (GetTickCountStd() - tcp_buf.tick) <= 1000)
|
|
continue;
|
|
|
|
tcp_buf.tick = GetTickCountStd();
|
|
// Timed out packet, resend
|
|
if (((m_queue_write + 1) & 15) != m_queue_read)
|
|
WriteToQueue(tcp_buf.data);
|
|
}
|
|
}
|
|
|
|
// Check for connection data
|
|
if (*datasize != 0)
|
|
continue;
|
|
const auto socket_data = TryGetDataFromSocket(&net_ref);
|
|
if (socket_data.has_value())
|
|
{
|
|
*datasize = socket_data->size();
|
|
std::memcpy(m_eth_ref->mRecvBuffer.get(), socket_data->data(), *datasize);
|
|
}
|
|
}
|
|
}
|
|
|
|
void CEXIETHERNET::BuiltInBBAInterface::HandleARP(const Common::ARPPacket& packet)
|
|
{
|
|
const auto& [hwdata, arpdata] = packet;
|
|
if (arpdata.sender_address == m_current_mac && arpdata.sender_ip == 0 &&
|
|
arpdata.target_ip == m_current_ip)
|
|
{
|
|
// Ignore ARP probe to itself (RFC 5227) sometimes used to prevent IP collision
|
|
return;
|
|
}
|
|
Common::ARPPacket response(m_current_mac, m_router_mac);
|
|
response.arp_header = Common::ARPHeader(arpdata.target_ip, ResolveAddress(arpdata.target_ip),
|
|
m_current_ip, m_current_mac);
|
|
WriteToQueue(response.Build());
|
|
}
|
|
|
|
void CEXIETHERNET::BuiltInBBAInterface::HandleDHCP(const Common::UDPPacket& packet)
|
|
{
|
|
const auto& [hwdata, ip, udp_header, ip_options, data] = packet;
|
|
const Common::DHCPPacket dhcp(packet.data);
|
|
const Common::DHCPBody& request = dhcp.body;
|
|
sockaddr_in from;
|
|
sockaddr_in to;
|
|
from.sin_addr.s_addr = m_router_ip;
|
|
from.sin_family = IPPROTO_UDP;
|
|
from.sin_port = htons(67);
|
|
to.sin_addr.s_addr = m_current_ip;
|
|
to.sin_family = IPPROTO_UDP;
|
|
to.sin_port = udp_header.source_port;
|
|
|
|
const u8* router_ip_ptr = reinterpret_cast<const u8*>(&m_router_ip);
|
|
const std::vector<u8> ip_part(router_ip_ptr, router_ip_ptr + sizeof(m_router_ip));
|
|
|
|
const std::vector<u8> timeout_24h = {0, 1, 0x51, 0x80};
|
|
|
|
Common::DHCPPacket reply;
|
|
reply.body = Common::DHCPBody(request.transaction_id, m_current_mac, m_current_ip, m_router_ip);
|
|
|
|
// options
|
|
// send our emulated lan settings
|
|
|
|
(dhcp.options.size() == 0 || dhcp.options[0].size() < 2 || dhcp.options[0].at(2) == 1) ?
|
|
reply.AddOption(53, {2}) : // default, send a suggestion
|
|
reply.AddOption(53, {5});
|
|
reply.AddOption(54, ip_part); // dhcp server ip
|
|
reply.AddOption(51, timeout_24h); // lease time 24h
|
|
reply.AddOption(58, timeout_24h); // renewal time
|
|
reply.AddOption(59, timeout_24h); // rebind time
|
|
reply.AddOption(1, {255, 255, 255, 0}); // submask
|
|
reply.AddOption(28, {ip_part[0], ip_part[1], ip_part[2], 255}); // broadcast ip
|
|
reply.AddOption(6, ip_part); // dns server
|
|
reply.AddOption(15, {0x6c, 0x61, 0x6e}); // domain name "lan"
|
|
reply.AddOption(3, ip_part); // router ip
|
|
reply.AddOption(255, {}); // end
|
|
|
|
const Common::UDPPacket response(m_current_mac, m_router_mac, from, to, reply.Build());
|
|
|
|
WriteToQueue(response.Build());
|
|
}
|
|
|
|
std::optional<std::vector<u8>>
|
|
CEXIETHERNET::BuiltInBBAInterface::TryGetDataFromSocket(StackRef* ref)
|
|
{
|
|
size_t datasize = 0; // Set by socket.receive using a non-const reference
|
|
unsigned short remote_port;
|
|
|
|
switch (ref->type)
|
|
{
|
|
case IPPROTO_UDP:
|
|
{
|
|
std::array<u8, MAX_UDP_LENGTH> buffer;
|
|
ref->udp_socket.receive(buffer.data(), MAX_UDP_LENGTH, datasize, ref->target, remote_port);
|
|
if (datasize > 0)
|
|
{
|
|
ref->from.sin_port = htons(remote_port);
|
|
const u32 remote_ip = htonl(ref->target.toInteger());
|
|
ref->from.sin_addr.s_addr = remote_ip;
|
|
ref->my_mac = ResolveAddress(remote_ip);
|
|
const std::vector<u8> udp_data(buffer.begin(), buffer.begin() + datasize);
|
|
const Common::UDPPacket packet(ref->bba_mac, ref->my_mac, ref->from, ref->to, udp_data);
|
|
return packet.Build();
|
|
}
|
|
break;
|
|
}
|
|
|
|
case IPPROTO_TCP:
|
|
if (!ref->tcp_socket.Connected(ref))
|
|
return std::nullopt;
|
|
|
|
sf::Socket::Status st = sf::Socket::Status::Done;
|
|
TcpBuffer* tcp_buffer = nullptr;
|
|
for (auto& tcp_buf : ref->tcp_buffers)
|
|
{
|
|
if (tcp_buf.used)
|
|
continue;
|
|
tcp_buffer = &tcp_buf;
|
|
break;
|
|
}
|
|
|
|
// set default size to 0 to avoid issue
|
|
datasize = 0;
|
|
const bool can_go = (GetTickCountStd() - ref->poke_time > 100 || ref->window_size > 2000);
|
|
std::array<u8, MAX_TCP_LENGTH> buffer;
|
|
if (tcp_buffer != nullptr && ref->ready && can_go)
|
|
st = ref->tcp_socket.receive(buffer.data(), MAX_TCP_LENGTH, datasize);
|
|
|
|
if (datasize > 0)
|
|
{
|
|
Common::TCPPacket packet(ref->bba_mac, ref->my_mac, ref->from, ref->to, ref->seq_num,
|
|
ref->ack_num, TCP_FLAG_ACK | TCP_FLAG_PSH);
|
|
packet.data = std::vector<u8>(buffer.begin(), buffer.begin() + datasize);
|
|
|
|
// build buffer
|
|
tcp_buffer->seq_id = ref->seq_num;
|
|
tcp_buffer->tick = GetTickCountStd();
|
|
tcp_buffer->data = packet.Build();
|
|
tcp_buffer->seq_id = ref->seq_num;
|
|
tcp_buffer->used = true;
|
|
ref->seq_num += static_cast<u32>(datasize);
|
|
ref->poke_time = GetTickCountStd();
|
|
return tcp_buffer->data;
|
|
}
|
|
if (GetTickCountStd() - ref->delay > 3000)
|
|
{
|
|
if (st == sf::Socket::Disconnected || st == sf::Socket::Error)
|
|
{
|
|
ref->ip = 0;
|
|
ref->tcp_socket.disconnect();
|
|
return BuildFINFrame(ref);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
void CEXIETHERNET::BuiltInBBAInterface::HandleTCPFrame(const Common::TCPPacket& packet)
|
|
{
|
|
const auto& [hwdata, ip_header, tcp_header, ip_options, tcp_options, data] = packet;
|
|
sf::IpAddress target;
|
|
StackRef* ref = m_network_ref.GetTCPSlot(tcp_header.source_port, tcp_header.destination_port,
|
|
std::bit_cast<u32>(ip_header.destination_addr));
|
|
const u16 flags = ntohs(tcp_header.properties) & 0xfff;
|
|
if (flags & (TCP_FLAG_FIN | TCP_FLAG_RST))
|
|
{
|
|
if (ref == nullptr)
|
|
return; // not found
|
|
|
|
ref->ack_num += 1 + static_cast<u32>(data.size());
|
|
WriteToQueue(BuildFINFrame(ref));
|
|
ref->ip = 0;
|
|
if (!data.empty())
|
|
ref->tcp_socket.send(data.data(), data.size());
|
|
ref->tcp_socket.disconnect();
|
|
}
|
|
else if (flags == (TCP_FLAG_SIN | TCP_FLAG_ACK))
|
|
{
|
|
if (ref == nullptr)
|
|
return; // not found
|
|
|
|
ref->seq_num++;
|
|
ref->ack_num = ntohl(tcp_header.sequence_number) + 1;
|
|
ref->ready = true;
|
|
WriteToQueue(BuildAckFrame(ref));
|
|
}
|
|
else if (flags & TCP_FLAG_SIN)
|
|
{
|
|
// new connection
|
|
if (ref != nullptr)
|
|
return;
|
|
ref = m_network_ref.GetAvailableSlot(0);
|
|
|
|
ref->delay = GetTickCountStd();
|
|
ref->local = tcp_header.source_port;
|
|
ref->remote = tcp_header.destination_port;
|
|
ref->ack_num = ntohl(tcp_header.sequence_number) + 1;
|
|
ref->ack_base = ref->ack_num;
|
|
ref->seq_num = 0x1000000;
|
|
ref->window_size = ntohs(tcp_header.window_size);
|
|
ref->type = IPPROTO_TCP;
|
|
for (auto& tcp_buf : ref->tcp_buffers)
|
|
tcp_buf.used = false;
|
|
const u32 destination_ip = std::bit_cast<u32>(ip_header.destination_addr);
|
|
ref->from.sin_addr.s_addr = destination_ip;
|
|
ref->from.sin_port = tcp_header.destination_port;
|
|
ref->to.sin_addr.s_addr = std::bit_cast<u32>(ip_header.source_addr);
|
|
ref->to.sin_port = tcp_header.source_port;
|
|
ref->bba_mac = m_current_mac;
|
|
ref->my_mac = ResolveAddress(destination_ip);
|
|
ref->tcp_socket.setBlocking(false);
|
|
ref->ready = false;
|
|
ref->ip = std::bit_cast<u32>(ip_header.destination_addr);
|
|
|
|
target = sf::IpAddress(ntohl(destination_ip));
|
|
ref->tcp_socket.Connect(target, ntohs(tcp_header.destination_port), m_current_ip);
|
|
}
|
|
else
|
|
{
|
|
// data packet
|
|
if (ref == nullptr)
|
|
return; // not found
|
|
|
|
const int size =
|
|
ntohs(ip_header.total_len) - ip_header.DefinedSize() - tcp_header.GetHeaderSize();
|
|
const u32 this_seq = ntohl(tcp_header.sequence_number);
|
|
|
|
if (size > 0)
|
|
{
|
|
// only if contain data
|
|
if (static_cast<int>(this_seq - ref->ack_num) >= 0 &&
|
|
data.size() >= static_cast<size_t>(size))
|
|
{
|
|
ref->tcp_socket.send(data.data(), size);
|
|
ref->ack_num += size;
|
|
}
|
|
|
|
// send ack
|
|
WriteToQueue(BuildAckFrame(ref));
|
|
}
|
|
// update windows size
|
|
ref->window_size = ntohs(tcp_header.window_size);
|
|
|
|
// clear any ack data
|
|
if (ntohs(tcp_header.properties) & TCP_FLAG_ACK)
|
|
{
|
|
const u32 ack_num = ntohl(tcp_header.acknowledgement_number);
|
|
for (auto& tcp_buf : ref->tcp_buffers)
|
|
{
|
|
if (!tcp_buf.used || tcp_buf.seq_id >= ack_num)
|
|
continue;
|
|
|
|
Common::PacketView view(tcp_buf.data.data(), tcp_buf.data.size());
|
|
auto tcp_packet = view.GetTCPPacket(); // This is always a tcp packet
|
|
if (!tcp_packet.has_value()) // should never happen but just in case
|
|
continue;
|
|
|
|
const u32 seq_end = static_cast<u32>(tcp_buf.seq_id + tcp_packet->data.size());
|
|
if (seq_end <= ack_num)
|
|
{
|
|
tcp_buf.used = false; // confirmed data received
|
|
if (!ref->ready && !ref->tcp_buffers[0].used)
|
|
ref->ready = true;
|
|
continue;
|
|
}
|
|
// partial data, adjust the packet for next ack
|
|
const u16 ack_size = ack_num - tcp_buf.seq_id;
|
|
tcp_packet->data.erase(tcp_packet->data.begin(), tcp_packet->data.begin() + ack_size);
|
|
|
|
tcp_buf.seq_id += ack_size;
|
|
tcp_packet->tcp_header.sequence_number = htonl(tcp_buf.seq_id);
|
|
tcp_buf.data = tcp_packet->Build();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// This is a little hack, some games open a UDP port
|
|
// and listen to it. We open it on our side manually.
|
|
void CEXIETHERNET::BuiltInBBAInterface::InitUDPPort(u16 port)
|
|
{
|
|
StackRef* ref = m_network_ref.GetAvailableSlot(htons(port));
|
|
if (ref == nullptr || ref->ip != 0)
|
|
return;
|
|
ref->ip = m_router_ip; // change for ip
|
|
ref->local = htons(port);
|
|
ref->remote = htons(port);
|
|
ref->type = IPPROTO_UDP;
|
|
ref->bba_mac = m_current_mac;
|
|
ref->my_mac = m_router_mac;
|
|
ref->from.sin_addr.s_addr = 0;
|
|
ref->from.sin_port = htons(port);
|
|
ref->to.sin_addr.s_addr = m_current_ip;
|
|
ref->to.sin_port = htons(port);
|
|
ref->udp_socket.setBlocking(false);
|
|
if (ref->udp_socket.Bind(port, m_current_ip) != sf::Socket::Done)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "Couldn't open UDP socket");
|
|
PanicAlertFmt("Could't open port {:x}, this game might not work proprely in LAN mode.", port);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void CEXIETHERNET::BuiltInBBAInterface::HandleUDPFrame(const Common::UDPPacket& packet)
|
|
{
|
|
const auto& [hwdata, ip_header, udp_header, ip_options, data] = packet;
|
|
sf::IpAddress target;
|
|
const u32 destination_addr = ip_header.destination_addr == Common::IP_ADDR_ANY ?
|
|
m_router_ip : // dns request
|
|
std::bit_cast<u32>(ip_header.destination_addr);
|
|
|
|
StackRef* ref = m_network_ref.GetAvailableSlot(udp_header.source_port);
|
|
if (ref->ip == 0)
|
|
{
|
|
ref->ip = destination_addr; // change for ip
|
|
ref->local = udp_header.source_port;
|
|
ref->remote = udp_header.destination_port;
|
|
ref->type = IPPROTO_UDP;
|
|
ref->bba_mac = m_current_mac;
|
|
ref->my_mac = m_router_mac;
|
|
ref->from.sin_addr.s_addr = destination_addr;
|
|
ref->from.sin_port = udp_header.destination_port;
|
|
ref->to.sin_addr.s_addr = std::bit_cast<u32>(ip_header.source_addr);
|
|
ref->to.sin_port = udp_header.source_port;
|
|
ref->udp_socket.setBlocking(false);
|
|
if (ref->udp_socket.Bind(ntohs(udp_header.source_port), m_current_ip) != sf::Socket::Done)
|
|
{
|
|
PanicAlertFmt(
|
|
"Port {:x} is already in use, this game might not work as intented in LAN Mode.",
|
|
htons(udp_header.source_port));
|
|
if (ref->udp_socket.Bind(sf::Socket::AnyPort, m_current_ip) != sf::Socket::Done)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "Couldn't open UDP socket");
|
|
return;
|
|
}
|
|
if (ntohs(udp_header.destination_port) == Common::SSDP_PORT && ntohs(udp_header.length) > 150)
|
|
{
|
|
// Quick hack to unlock the connection, throw it back at him
|
|
Common::UDPPacket reply = packet;
|
|
reply.eth_header.destination = hwdata.source;
|
|
reply.eth_header.source = hwdata.destination;
|
|
reply.ip_header.destination_addr = ip_header.source_addr;
|
|
reply.ip_header.source_addr = std::bit_cast<Common::IPAddress>(destination_addr);
|
|
WriteToQueue(reply.Build());
|
|
}
|
|
}
|
|
}
|
|
if (ntohs(udp_header.destination_port) == 53)
|
|
target = sf::IpAddress(m_dns_ip.c_str()); // dns server ip
|
|
else
|
|
target = sf::IpAddress(ntohl(std::bit_cast<u32>(ip_header.destination_addr)));
|
|
ref->udp_socket.send(data.data(), data.size(), target, ntohs(udp_header.destination_port));
|
|
}
|
|
|
|
void CEXIETHERNET::BuiltInBBAInterface::HandleUPnPClient()
|
|
{
|
|
StackRef* ref = m_network_ref.GetAvailableSlot(0);
|
|
if (ref == nullptr || m_upnp_httpd.accept(ref->tcp_socket) != sf::Socket::Done)
|
|
return;
|
|
|
|
if (ref->tcp_socket.GetPeerName(&ref->from) != sf::Socket::Status::Done ||
|
|
ref->tcp_socket.GetSockName(&ref->to) != sf::Socket::Status::Done)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "Failed to accept new UPnP client: {}", Common::StrNetworkError());
|
|
return;
|
|
}
|
|
|
|
if (m_current_ip == ref->from.sin_addr.s_addr)
|
|
{
|
|
ref->tcp_socket.disconnect();
|
|
WARN_LOG_FMT(SP1, "Ignoring UPnP request to itself");
|
|
return;
|
|
}
|
|
|
|
ref->delay = GetTickCountStd();
|
|
ref->ip = ref->from.sin_addr.s_addr;
|
|
ref->local = ref->to.sin_port;
|
|
ref->remote = ref->from.sin_port;
|
|
ref->ack_num = 0;
|
|
ref->ack_base = ref->ack_num;
|
|
ref->seq_num = 0x1000000;
|
|
ref->window_size = 8192;
|
|
ref->type = IPPROTO_TCP;
|
|
for (auto& tcp_buf : ref->tcp_buffers)
|
|
tcp_buf.used = false;
|
|
ref->bba_mac = m_current_mac;
|
|
ref->my_mac = ResolveAddress(ref->from.sin_addr.s_addr);
|
|
ref->tcp_socket.setBlocking(false);
|
|
ref->ready = false;
|
|
|
|
Common::TCPPacket result(ref->bba_mac, ref->my_mac, ref->from, ref->to, ref->seq_num,
|
|
ref->ack_num, TCP_FLAG_SIN);
|
|
// Based on Nintendont packet capture of Mario Kart: Double Dash!!
|
|
result.tcp_options = {
|
|
0x02, 0x04, 0x05, 0xb4, // Maximum segment size: 1460 bytes
|
|
0x01, // NOP
|
|
0x03, 0x03, 0x08, // Window scale: 8 (multiply by 256)
|
|
0x01, 0x01, // NOPs
|
|
0x04, 0x02 // SACK permitted
|
|
};
|
|
WriteToQueue(result.Build());
|
|
}
|
|
|
|
const Common::MACAddress& CEXIETHERNET::BuiltInBBAInterface::ResolveAddress(u32 inet_ip)
|
|
{
|
|
auto it = m_arp_table.lower_bound(inet_ip);
|
|
if (it != m_arp_table.end() && it->first == inet_ip)
|
|
{
|
|
return it->second;
|
|
}
|
|
else
|
|
{
|
|
return m_arp_table
|
|
.emplace_hint(it, inet_ip, Common::GenerateMacAddress(Common::MACConsumer::BBA))
|
|
->second;
|
|
}
|
|
}
|
|
|
|
bool CEXIETHERNET::BuiltInBBAInterface::SendFrame(const u8* frame, u32 size)
|
|
{
|
|
std::lock_guard<std::mutex> lock(m_mtx);
|
|
const Common::PacketView view(frame, size);
|
|
|
|
const std::optional<u16> ethertype = view.GetEtherType();
|
|
if (!ethertype.has_value())
|
|
{
|
|
ERROR_LOG_FMT(SP1, "Unable to send frame with invalid ethernet header");
|
|
return false;
|
|
}
|
|
|
|
switch (*ethertype)
|
|
{
|
|
case Common::IPV4_ETHERTYPE:
|
|
{
|
|
const std::optional<u8> ip_proto = view.GetIPProto();
|
|
if (!ip_proto.has_value())
|
|
{
|
|
ERROR_LOG_FMT(SP1, "Unable to send frame with invalid IP header");
|
|
return false;
|
|
}
|
|
|
|
switch (*ip_proto)
|
|
{
|
|
case IPPROTO_UDP:
|
|
{
|
|
const auto udp_packet = view.GetUDPPacket();
|
|
if (!udp_packet.has_value())
|
|
{
|
|
ERROR_LOG_FMT(SP1, "Unable to send frame with invalid UDP header");
|
|
return false;
|
|
}
|
|
|
|
if (ntohs(udp_packet->udp_header.destination_port) == 67)
|
|
{
|
|
HandleDHCP(*udp_packet);
|
|
}
|
|
else
|
|
{
|
|
HandleUDPFrame(*udp_packet);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case IPPROTO_TCP:
|
|
{
|
|
const auto tcp_packet = view.GetTCPPacket();
|
|
if (!tcp_packet.has_value())
|
|
{
|
|
ERROR_LOG_FMT(SP1, "Unable to send frame with invalid TCP header");
|
|
return false;
|
|
}
|
|
|
|
HandleTCPFrame(*tcp_packet);
|
|
break;
|
|
}
|
|
|
|
case IPPROTO_IGMP:
|
|
{
|
|
// Acknowledge IGMP packet
|
|
const std::vector<u8> data(frame, frame + size);
|
|
WriteToQueue(data);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
ERROR_LOG_FMT(SP1, "Unsupported IP protocol {}", *ip_proto);
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case Common::ARP_ETHERTYPE:
|
|
{
|
|
const auto arp_packet = view.GetARPPacket();
|
|
if (!arp_packet.has_value())
|
|
{
|
|
ERROR_LOG_FMT(SP1, "Unable to send frame with invalid ARP header");
|
|
return false;
|
|
}
|
|
|
|
HandleARP(*arp_packet);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
ERROR_LOG_FMT(SP1, "Unsupported EtherType {#06x}", *ethertype);
|
|
return false;
|
|
}
|
|
|
|
m_eth_ref->SendComplete();
|
|
return true;
|
|
}
|
|
|
|
void CEXIETHERNET::BuiltInBBAInterface::ReadThreadHandler(CEXIETHERNET::BuiltInBBAInterface* self)
|
|
{
|
|
while (!self->m_read_thread_shutdown.IsSet())
|
|
{
|
|
// make thread less cpu hungry
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
|
|
if (!self->m_read_enabled.IsSet())
|
|
continue;
|
|
size_t datasize = 0;
|
|
|
|
u8 wp = self->m_eth_ref->page_ptr(BBA_RWP);
|
|
const u8 rp = self->m_eth_ref->page_ptr(BBA_RRP);
|
|
if (rp > wp)
|
|
wp += 16;
|
|
|
|
if ((wp - rp) >= 8)
|
|
continue;
|
|
|
|
std::lock_guard<std::mutex> lock(self->m_mtx);
|
|
// process queue file first
|
|
if (self->m_queue_read != self->m_queue_write)
|
|
{
|
|
datasize = self->m_queue_data[self->m_queue_read].size();
|
|
if (datasize > BBA_RECV_SIZE)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "Frame size is exceiding BBA capacity, frame stack might be corrupted"
|
|
"Killing Dolphin...");
|
|
std::exit(0);
|
|
}
|
|
std::memcpy(self->m_eth_ref->mRecvBuffer.get(), self->m_queue_data[self->m_queue_read].data(),
|
|
datasize);
|
|
self->m_queue_read++;
|
|
self->m_queue_read &= 15;
|
|
}
|
|
|
|
// Check network stack references
|
|
self->PollData(&datasize);
|
|
|
|
// Check for new UPnP client
|
|
self->HandleUPnPClient();
|
|
|
|
if (datasize > 0)
|
|
{
|
|
u8* buffer = reinterpret_cast<u8*>(self->m_eth_ref->mRecvBuffer.get());
|
|
Common::PacketView packet(buffer, datasize);
|
|
const auto packet_type = packet.GetEtherType();
|
|
if (packet_type.has_value() && packet_type == Common::IPV4_ETHERTYPE)
|
|
{
|
|
SetIPIdentification(buffer, datasize, ++self->m_ip_frame_id);
|
|
}
|
|
if (datasize < 64)
|
|
{
|
|
std::fill(buffer + datasize, buffer + 64, 0);
|
|
datasize = 64;
|
|
}
|
|
self->m_eth_ref->mRecvBufferLength = static_cast<u32>(datasize);
|
|
self->m_eth_ref->RecvHandlePacket();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool CEXIETHERNET::BuiltInBBAInterface::RecvInit()
|
|
{
|
|
m_read_thread = std::thread(ReadThreadHandler, this);
|
|
return true;
|
|
}
|
|
|
|
void CEXIETHERNET::BuiltInBBAInterface::RecvStart()
|
|
{
|
|
if (m_read_enabled.IsSet())
|
|
return;
|
|
InitUDPPort(26502); // Kirby Air Ride
|
|
InitUDPPort(26512); // Mario Kart: Double Dash!! and 1080° Avalanche
|
|
m_read_enabled.Set();
|
|
}
|
|
|
|
void CEXIETHERNET::BuiltInBBAInterface::RecvStop()
|
|
{
|
|
m_read_enabled.Clear();
|
|
m_network_ref.Clear();
|
|
m_queue_read = 0;
|
|
m_queue_write = 0;
|
|
}
|
|
} // namespace ExpansionInterface
|
|
|
|
BbaTcpSocket::BbaTcpSocket() = default;
|
|
|
|
sf::Socket::Status BbaTcpSocket::Connect(const sf::IpAddress& dest, u16 port, u32 net_ip)
|
|
{
|
|
sockaddr_in addr;
|
|
addr.sin_addr.s_addr = net_ip;
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = 0;
|
|
::bind(getHandle(), reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
|
|
m_connecting_state = ConnectingState::Connecting;
|
|
return this->connect(dest, port);
|
|
}
|
|
|
|
sf::Socket::Status BbaTcpSocket::GetPeerName(sockaddr_in* addr) const
|
|
{
|
|
socklen_t size = sizeof(*addr);
|
|
if (getpeername(getHandle(), reinterpret_cast<sockaddr*>(addr), &size) == -1)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "getpeername failed: {}", Common::StrNetworkError());
|
|
return sf::Socket::Status::Error;
|
|
}
|
|
return sf::Socket::Status::Done;
|
|
}
|
|
|
|
sf::Socket::Status BbaTcpSocket::GetSockName(sockaddr_in* addr) const
|
|
{
|
|
socklen_t size = sizeof(*addr);
|
|
if (getsockname(getHandle(), reinterpret_cast<sockaddr*>(addr), &size) == -1)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "getsockname failed: {}", Common::StrNetworkError());
|
|
return sf::Socket::Status::Error;
|
|
}
|
|
return sf::Socket::Status::Done;
|
|
}
|
|
|
|
bool BbaTcpSocket::Connected(StackRef* ref)
|
|
{
|
|
// Called by ReadThreadHandler's TryGetDataFromSocket
|
|
// TODO: properly handle error state
|
|
switch (m_connecting_state)
|
|
{
|
|
case ConnectingState::Connected:
|
|
return true;
|
|
case ConnectingState::Connecting:
|
|
{
|
|
const int fd = getHandle();
|
|
const s32 nfds = fd + 1;
|
|
fd_set read_fds;
|
|
fd_set write_fds;
|
|
fd_set except_fds;
|
|
struct timeval t = {0, 0};
|
|
FD_ZERO(&read_fds);
|
|
FD_ZERO(&write_fds);
|
|
FD_ZERO(&except_fds);
|
|
FD_SET(fd, &write_fds);
|
|
FD_SET(fd, &except_fds);
|
|
|
|
if (select(nfds, &read_fds, &write_fds, &except_fds, &t) < 0)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "Failed to get BBA socket connection state: {}",
|
|
Common::StrNetworkError());
|
|
break;
|
|
}
|
|
|
|
if (FD_ISSET(fd, &write_fds) == 0 && FD_ISSET(fd, &except_fds) == 0)
|
|
break;
|
|
|
|
s32 error = 0;
|
|
socklen_t len = sizeof(error);
|
|
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, reinterpret_cast<char*>(&error), &len) != 0)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "Failed to get BBA socket error state: {}", Common::StrNetworkError());
|
|
break;
|
|
}
|
|
|
|
if (error != 0)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "BBA connect failed (err={}): {}", error,
|
|
Common::DecodeNetworkError(error));
|
|
m_connecting_state = ConnectingState::Error;
|
|
break;
|
|
}
|
|
|
|
// Get peername to ensure the socket is connected
|
|
sockaddr_in peer;
|
|
socklen_t peer_len = sizeof(peer);
|
|
if (getpeername(fd, reinterpret_cast<sockaddr*>(&peer), &peer_len) != 0)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "BBA connect failed to get peername: {}", Common::StrNetworkError());
|
|
m_connecting_state = ConnectingState::Error;
|
|
break;
|
|
}
|
|
|
|
// Create the resulting SYN ACK packet
|
|
m_connecting_state = ConnectingState::Connected;
|
|
INFO_LOG_FMT(SP1, "BBA connect succeeded");
|
|
|
|
Common::TCPPacket result(ref->bba_mac, ref->my_mac, ref->from, ref->to, ref->seq_num,
|
|
ref->ack_num, TCP_FLAG_SIN | TCP_FLAG_ACK);
|
|
|
|
result.tcp_options = {
|
|
0x02, 0x04, 0x05, 0xb4, // Maximum segment size: 1460 bytes
|
|
0x01, 0x01, 0x01, 0x01 // NOPs
|
|
};
|
|
|
|
ref->seq_num++;
|
|
ref->tcp_buffers[0].data = result.Build();
|
|
ref->tcp_buffers[0].seq_id = ref->seq_num - 1;
|
|
ref->tcp_buffers[0].tick = GetTickCountStd() - 900; // delay
|
|
ref->tcp_buffers[0].used = true;
|
|
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
BbaUdpSocket::BbaUdpSocket() = default;
|
|
|
|
sf::Socket::Status BbaUdpSocket::Bind(u16 port, u32 net_ip)
|
|
{
|
|
if (port != Common::SSDP_PORT)
|
|
return this->bind(port, sf::IpAddress(ntohl(net_ip)));
|
|
|
|
// Handle SSDP multicast
|
|
create();
|
|
const int on = 1;
|
|
if (setsockopt(getHandle(), SOL_SOCKET, SO_REUSEADDR, reinterpret_cast<const char*>(&on),
|
|
sizeof(on)) != 0)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "setsockopt failed to reuse SSDP address: {}", Common::StrNetworkError());
|
|
}
|
|
#ifdef SO_REUSEPORT
|
|
if (setsockopt(getHandle(), SOL_SOCKET, SO_REUSEPORT, reinterpret_cast<const char*>(&on),
|
|
sizeof(on)) != 0)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "setsockopt failed to reuse SSDP port: {}", Common::StrNetworkError());
|
|
}
|
|
#endif
|
|
if (const char loop = 1;
|
|
setsockopt(getHandle(), IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)) != 0)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "setsockopt failed to set SSDP loopback: {}", Common::StrNetworkError());
|
|
}
|
|
|
|
// sf::UdpSocket::bind will close the socket and get rid of its options
|
|
sockaddr_in addr;
|
|
addr.sin_addr.s_addr = INADDR_ANY;
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = htons(Common::SSDP_PORT);
|
|
Common::ScopeGuard error_guard([this] { close(); });
|
|
if (::bind(getHandle(), reinterpret_cast<const sockaddr*>(&addr), sizeof(addr)) != 0)
|
|
{
|
|
WARN_LOG_FMT(SP1, "bind with SSDP port and INADDR_ANY failed: {}", Common::StrNetworkError());
|
|
addr.sin_addr.s_addr = net_ip;
|
|
if (::bind(getHandle(), reinterpret_cast<const sockaddr*>(&addr), sizeof(addr)) != 0)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "bind with SSDP port failed: {}", Common::StrNetworkError());
|
|
return sf::Socket::Status::Error;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
addr.sin_addr.s_addr = net_ip; // Set this here for IP_MULTICAST_IF
|
|
}
|
|
INFO_LOG_FMT(SP1, "SSDP bind successful");
|
|
|
|
// Bind to the right interface
|
|
if (setsockopt(getHandle(), IPPROTO_IP, IP_MULTICAST_IF,
|
|
reinterpret_cast<const char*>(&addr.sin_addr), sizeof(addr.sin_addr)) != 0)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "setsockopt failed to bind to the network interface: {}",
|
|
Common::StrNetworkError());
|
|
return sf::Socket::Status::Error;
|
|
}
|
|
|
|
// Subscribe to the SSDP multicast group
|
|
// NB: Other groups aren't supported because of HLE
|
|
struct ip_mreq mreq;
|
|
mreq.imr_multiaddr.s_addr = std::bit_cast<u32>(Common::IP_ADDR_SSDP);
|
|
mreq.imr_interface.s_addr = net_ip;
|
|
if (setsockopt(getHandle(), IPPROTO_IP, IP_ADD_MEMBERSHIP, reinterpret_cast<const char*>(&mreq),
|
|
sizeof(mreq)) != 0)
|
|
{
|
|
ERROR_LOG_FMT(SP1, "setsockopt failed to subscribe to SSDP multicast group: {}",
|
|
Common::StrNetworkError());
|
|
return sf::Socket::Status::Error;
|
|
}
|
|
|
|
error_guard.Dismiss();
|
|
INFO_LOG_FMT(SP1, "SSDP multicast membership successful");
|
|
return sf::Socket::Status::Done;
|
|
}
|
|
|
|
StackRef* NetworkRef::GetAvailableSlot(u16 port)
|
|
{
|
|
if (port > 0) // existing connection?
|
|
{
|
|
for (auto& ref : m_stacks)
|
|
{
|
|
if (ref.ip != 0 && ref.local == port)
|
|
return &ref;
|
|
}
|
|
}
|
|
for (auto& ref : m_stacks)
|
|
{
|
|
if (ref.ip == 0)
|
|
return &ref;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
StackRef* NetworkRef::GetTCPSlot(u16 src_port, u16 dst_port, u32 ip)
|
|
{
|
|
for (auto& ref : m_stacks)
|
|
{
|
|
if (ref.ip == ip && ref.remote == dst_port && ref.local == src_port)
|
|
{
|
|
return &ref;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void NetworkRef::Clear()
|
|
{
|
|
for (auto& ref : m_stacks)
|
|
{
|
|
if (ref.ip != 0)
|
|
{
|
|
ref.type == IPPROTO_TCP ? ref.tcp_socket.disconnect() : ref.udp_socket.unbind();
|
|
}
|
|
ref.ip = 0;
|
|
}
|
|
}
|