diff --git a/.gitmodules b/.gitmodules
index 3c0d15951..d8e04923a 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
+[submodule "enet"]
+    path = externals/enet
+    url = https://github.com/lsalzman/enet.git
 [submodule "inih"]
     path = externals/inih/inih
     url = https://github.com/benhoyt/inih.git
diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt
index bd01f4c4d..b4570bb69 100644
--- a/externals/CMakeLists.txt
+++ b/externals/CMakeLists.txt
@@ -73,6 +73,10 @@ if (YUZU_USE_EXTERNAL_SDL2)
     add_library(SDL2 ALIAS SDL2-static)
 endif()
 
+# ENet
+add_subdirectory(enet)
+target_include_directories(enet INTERFACE ./enet/include)
+
 # Cubeb
 if(ENABLE_CUBEB)
     set(BUILD_TESTS OFF CACHE BOOL "")
diff --git a/externals/enet b/externals/enet
new file mode 160000
index 000000000..39a72ab19
--- /dev/null
+++ b/externals/enet
@@ -0,0 +1 @@
+Subproject commit 39a72ab1990014eb399cee9d538fd529df99c6a0
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 39ae573b2..9367f67c1 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -156,6 +156,7 @@ add_subdirectory(common)
 add_subdirectory(core)
 add_subdirectory(audio_core)
 add_subdirectory(video_core)
+add_subdirectory(network)
 add_subdirectory(input_common)
 add_subdirectory(shader_recompiler)
 
diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt
new file mode 100644
index 000000000..382a69e2f
--- /dev/null
+++ b/src/network/CMakeLists.txt
@@ -0,0 +1,16 @@
+add_library(network STATIC
+    network.cpp
+    network.h
+    packet.cpp
+    packet.h
+    room.cpp
+    room.h
+    room_member.cpp
+    room_member.h
+    verify_user.cpp
+    verify_user.h
+)
+
+create_target_directory_groups(network)
+
+target_link_libraries(network PRIVATE common enet Boost::boost)
diff --git a/src/network/network.cpp b/src/network/network.cpp
new file mode 100644
index 000000000..51b5d6a9f
--- /dev/null
+++ b/src/network/network.cpp
@@ -0,0 +1,50 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/assert.h"
+#include "common/logging/log.h"
+#include "enet/enet.h"
+#include "network/network.h"
+
+namespace Network {
+
+static std::shared_ptr<RoomMember> g_room_member; ///< RoomMember (Client) for network games
+static std::shared_ptr<Room> g_room;              ///< Room (Server) for network games
+// TODO(B3N30): Put these globals into a networking class
+
+bool Init() {
+    if (enet_initialize() != 0) {
+        LOG_ERROR(Network, "Error initalizing ENet");
+        return false;
+    }
+    g_room = std::make_shared<Room>();
+    g_room_member = std::make_shared<RoomMember>();
+    LOG_DEBUG(Network, "initialized OK");
+    return true;
+}
+
+std::weak_ptr<Room> GetRoom() {
+    return g_room;
+}
+
+std::weak_ptr<RoomMember> GetRoomMember() {
+    return g_room_member;
+}
+
+void Shutdown() {
+    if (g_room_member) {
+        if (g_room_member->IsConnected())
+            g_room_member->Leave();
+        g_room_member.reset();
+    }
+    if (g_room) {
+        if (g_room->GetState() == Room::State::Open)
+            g_room->Destroy();
+        g_room.reset();
+    }
+    enet_deinitialize();
+    LOG_DEBUG(Network, "shutdown OK");
+}
+
+} // namespace Network
diff --git a/src/network/network.h b/src/network/network.h
new file mode 100644
index 000000000..6d002d693
--- /dev/null
+++ b/src/network/network.h
@@ -0,0 +1,25 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include "network/room.h"
+#include "network/room_member.h"
+
+namespace Network {
+
+/// Initializes and registers the network device, the room, and the room member.
+bool Init();
+
+/// Returns a pointer to the room handle
+std::weak_ptr<Room> GetRoom();
+
+/// Returns a pointer to the room member handle
+std::weak_ptr<RoomMember> GetRoomMember();
+
+/// Unregisters the network device, the room, and the room member and shut them down.
+void Shutdown();
+
+} // namespace Network
diff --git a/src/network/packet.cpp b/src/network/packet.cpp
new file mode 100644
index 000000000..8fc5feabd
--- /dev/null
+++ b/src/network/packet.cpp
@@ -0,0 +1,263 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#ifdef _WIN32
+#include <winsock2.h>
+#else
+#include <arpa/inet.h>
+#endif
+#include <cstring>
+#include <string>
+#include "network/packet.h"
+
+namespace Network {
+
+#ifndef htonll
+u64 htonll(u64 x) {
+    return ((1 == htonl(1)) ? (x) : ((uint64_t)htonl((x)&0xFFFFFFFF) << 32) | htonl((x) >> 32));
+}
+#endif
+
+#ifndef ntohll
+u64 ntohll(u64 x) {
+    return ((1 == ntohl(1)) ? (x) : ((uint64_t)ntohl((x)&0xFFFFFFFF) << 32) | ntohl((x) >> 32));
+}
+#endif
+
+void Packet::Append(const void* in_data, std::size_t size_in_bytes) {
+    if (in_data && (size_in_bytes > 0)) {
+        std::size_t start = data.size();
+        data.resize(start + size_in_bytes);
+        std::memcpy(&data[start], in_data, size_in_bytes);
+    }
+}
+
+void Packet::Read(void* out_data, std::size_t size_in_bytes) {
+    if (out_data && CheckSize(size_in_bytes)) {
+        std::memcpy(out_data, &data[read_pos], size_in_bytes);
+        read_pos += size_in_bytes;
+    }
+}
+
+void Packet::Clear() {
+    data.clear();
+    read_pos = 0;
+    is_valid = true;
+}
+
+const void* Packet::GetData() const {
+    return !data.empty() ? &data[0] : nullptr;
+}
+
+void Packet::IgnoreBytes(u32 length) {
+    read_pos += length;
+}
+
+std::size_t Packet::GetDataSize() const {
+    return data.size();
+}
+
+bool Packet::EndOfPacket() const {
+    return read_pos >= data.size();
+}
+
+Packet::operator bool() const {
+    return is_valid;
+}
+
+Packet& Packet::operator>>(bool& out_data) {
+    u8 value;
+    if (*this >> value) {
+        out_data = (value != 0);
+    }
+    return *this;
+}
+
+Packet& Packet::operator>>(s8& out_data) {
+    Read(&out_data, sizeof(out_data));
+    return *this;
+}
+
+Packet& Packet::operator>>(u8& out_data) {
+    Read(&out_data, sizeof(out_data));
+    return *this;
+}
+
+Packet& Packet::operator>>(s16& out_data) {
+    s16 value;
+    Read(&value, sizeof(value));
+    out_data = ntohs(value);
+    return *this;
+}
+
+Packet& Packet::operator>>(u16& out_data) {
+    u16 value;
+    Read(&value, sizeof(value));
+    out_data = ntohs(value);
+    return *this;
+}
+
+Packet& Packet::operator>>(s32& out_data) {
+    s32 value;
+    Read(&value, sizeof(value));
+    out_data = ntohl(value);
+    return *this;
+}
+
+Packet& Packet::operator>>(u32& out_data) {
+    u32 value;
+    Read(&value, sizeof(value));
+    out_data = ntohl(value);
+    return *this;
+}
+
+Packet& Packet::operator>>(s64& out_data) {
+    s64 value;
+    Read(&value, sizeof(value));
+    out_data = ntohll(value);
+    return *this;
+}
+
+Packet& Packet::operator>>(u64& out_data) {
+    u64 value;
+    Read(&value, sizeof(value));
+    out_data = ntohll(value);
+    return *this;
+}
+
+Packet& Packet::operator>>(float& out_data) {
+    Read(&out_data, sizeof(out_data));
+    return *this;
+}
+
+Packet& Packet::operator>>(double& out_data) {
+    Read(&out_data, sizeof(out_data));
+    return *this;
+}
+
+Packet& Packet::operator>>(char* out_data) {
+    // First extract string length
+    u32 length = 0;
+    *this >> length;
+
+    if ((length > 0) && CheckSize(length)) {
+        // Then extract characters
+        std::memcpy(out_data, &data[read_pos], length);
+        out_data[length] = '\0';
+
+        // Update reading position
+        read_pos += length;
+    }
+
+    return *this;
+}
+
+Packet& Packet::operator>>(std::string& out_data) {
+    // First extract string length
+    u32 length = 0;
+    *this >> length;
+
+    out_data.clear();
+    if ((length > 0) && CheckSize(length)) {
+        // Then extract characters
+        out_data.assign(&data[read_pos], length);
+
+        // Update reading position
+        read_pos += length;
+    }
+
+    return *this;
+}
+
+Packet& Packet::operator<<(bool in_data) {
+    *this << static_cast<u8>(in_data);
+    return *this;
+}
+
+Packet& Packet::operator<<(s8 in_data) {
+    Append(&in_data, sizeof(in_data));
+    return *this;
+}
+
+Packet& Packet::operator<<(u8 in_data) {
+    Append(&in_data, sizeof(in_data));
+    return *this;
+}
+
+Packet& Packet::operator<<(s16 in_data) {
+    s16 toWrite = htons(in_data);
+    Append(&toWrite, sizeof(toWrite));
+    return *this;
+}
+
+Packet& Packet::operator<<(u16 in_data) {
+    u16 toWrite = htons(in_data);
+    Append(&toWrite, sizeof(toWrite));
+    return *this;
+}
+
+Packet& Packet::operator<<(s32 in_data) {
+    s32 toWrite = htonl(in_data);
+    Append(&toWrite, sizeof(toWrite));
+    return *this;
+}
+
+Packet& Packet::operator<<(u32 in_data) {
+    u32 toWrite = htonl(in_data);
+    Append(&toWrite, sizeof(toWrite));
+    return *this;
+}
+
+Packet& Packet::operator<<(s64 in_data) {
+    s64 toWrite = htonll(in_data);
+    Append(&toWrite, sizeof(toWrite));
+    return *this;
+}
+
+Packet& Packet::operator<<(u64 in_data) {
+    u64 toWrite = htonll(in_data);
+    Append(&toWrite, sizeof(toWrite));
+    return *this;
+}
+
+Packet& Packet::operator<<(float in_data) {
+    Append(&in_data, sizeof(in_data));
+    return *this;
+}
+
+Packet& Packet::operator<<(double in_data) {
+    Append(&in_data, sizeof(in_data));
+    return *this;
+}
+
+Packet& Packet::operator<<(const char* in_data) {
+    // First insert string length
+    u32 length = static_cast<u32>(std::strlen(in_data));
+    *this << length;
+
+    // Then insert characters
+    Append(in_data, length * sizeof(char));
+
+    return *this;
+}
+
+Packet& Packet::operator<<(const std::string& in_data) {
+    // First insert string length
+    u32 length = static_cast<u32>(in_data.size());
+    *this << length;
+
+    // Then insert characters
+    if (length > 0)
+        Append(in_data.c_str(), length * sizeof(std::string::value_type));
+
+    return *this;
+}
+
+bool Packet::CheckSize(std::size_t size) {
+    is_valid = is_valid && (read_pos + size <= data.size());
+
+    return is_valid;
+}
+
+} // namespace Network
diff --git a/src/network/packet.h b/src/network/packet.h
new file mode 100644
index 000000000..7bdc3da95
--- /dev/null
+++ b/src/network/packet.h
@@ -0,0 +1,166 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <array>
+#include <vector>
+#include "common/common_types.h"
+
+namespace Network {
+
+/// A class that serializes data for network transfer. It also handles endianess
+class Packet {
+public:
+    Packet() = default;
+    ~Packet() = default;
+
+    /**
+     * Append data to the end of the packet
+     * @param data        Pointer to the sequence of bytes to append
+     * @param size_in_bytes Number of bytes to append
+     */
+    void Append(const void* data, std::size_t size_in_bytes);
+
+    /**
+     * Reads data from the current read position of the packet
+     * @param out_data        Pointer where the data should get written to
+     * @param size_in_bytes Number of bytes to read
+     */
+    void Read(void* out_data, std::size_t size_in_bytes);
+
+    /**
+     * Clear the packet
+     * After calling Clear, the packet is empty.
+     */
+    void Clear();
+
+    /**
+     * Ignores bytes while reading
+     * @param length THe number of bytes to ignore
+     */
+    void IgnoreBytes(u32 length);
+
+    /**
+     * Get a pointer to the data contained in the packet
+     * @return Pointer to the data
+     */
+    const void* GetData() const;
+
+    /**
+     * This function returns the number of bytes pointed to by
+     * what getData returns.
+     * @return Data size, in bytes
+     */
+    std::size_t GetDataSize() const;
+
+    /**
+     * This function is useful to know if there is some data
+     * left to be read, without actually reading it.
+     * @return True if all data was read, false otherwise
+     */
+    bool EndOfPacket() const;
+
+    explicit operator bool() const;
+
+    /// Overloads of operator >> to read data from the packet
+    Packet& operator>>(bool& out_data);
+    Packet& operator>>(s8& out_data);
+    Packet& operator>>(u8& out_data);
+    Packet& operator>>(s16& out_data);
+    Packet& operator>>(u16& out_data);
+    Packet& operator>>(s32& out_data);
+    Packet& operator>>(u32& out_data);
+    Packet& operator>>(s64& out_data);
+    Packet& operator>>(u64& out_data);
+    Packet& operator>>(float& out_data);
+    Packet& operator>>(double& out_data);
+    Packet& operator>>(char* out_data);
+    Packet& operator>>(std::string& out_data);
+    template <typename T>
+    Packet& operator>>(std::vector<T>& out_data);
+    template <typename T, std::size_t S>
+    Packet& operator>>(std::array<T, S>& out_data);
+
+    /// Overloads of operator << to write data into the packet
+    Packet& operator<<(bool in_data);
+    Packet& operator<<(s8 in_data);
+    Packet& operator<<(u8 in_data);
+    Packet& operator<<(s16 in_data);
+    Packet& operator<<(u16 in_data);
+    Packet& operator<<(s32 in_data);
+    Packet& operator<<(u32 in_data);
+    Packet& operator<<(s64 in_data);
+    Packet& operator<<(u64 in_data);
+    Packet& operator<<(float in_data);
+    Packet& operator<<(double in_data);
+    Packet& operator<<(const char* in_data);
+    Packet& operator<<(const std::string& in_data);
+    template <typename T>
+    Packet& operator<<(const std::vector<T>& in_data);
+    template <typename T, std::size_t S>
+    Packet& operator<<(const std::array<T, S>& data);
+
+private:
+    /**
+     * Check if the packet can extract a given number of bytes
+     * This function updates accordingly the state of the packet.
+     * @param size Size to check
+     * @return True if size bytes can be read from the packet
+     */
+    bool CheckSize(std::size_t size);
+
+    // Member data
+    std::vector<char> data;   ///< Data stored in the packet
+    std::size_t read_pos = 0; ///< Current reading position in the packet
+    bool is_valid = true;     ///< Reading state of the packet
+};
+
+template <typename T>
+Packet& Packet::operator>>(std::vector<T>& out_data) {
+    // First extract the size
+    u32 size = 0;
+    *this >> size;
+    out_data.resize(size);
+
+    // Then extract the data
+    for (std::size_t i = 0; i < out_data.size(); ++i) {
+        T character;
+        *this >> character;
+        out_data[i] = character;
+    }
+    return *this;
+}
+
+template <typename T, std::size_t S>
+Packet& Packet::operator>>(std::array<T, S>& out_data) {
+    for (std::size_t i = 0; i < out_data.size(); ++i) {
+        T character;
+        *this >> character;
+        out_data[i] = character;
+    }
+    return *this;
+}
+
+template <typename T>
+Packet& Packet::operator<<(const std::vector<T>& in_data) {
+    // First insert the size
+    *this << static_cast<u32>(in_data.size());
+
+    // Then insert the data
+    for (std::size_t i = 0; i < in_data.size(); ++i) {
+        *this << in_data[i];
+    }
+    return *this;
+}
+
+template <typename T, std::size_t S>
+Packet& Packet::operator<<(const std::array<T, S>& in_data) {
+    for (std::size_t i = 0; i < in_data.size(); ++i) {
+        *this << in_data[i];
+    }
+    return *this;
+}
+
+} // namespace Network
diff --git a/src/network/room.cpp b/src/network/room.cpp
new file mode 100644
index 000000000..cd0c0ebc4
--- /dev/null
+++ b/src/network/room.cpp
@@ -0,0 +1,1111 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <algorithm>
+#include <atomic>
+#include <iomanip>
+#include <mutex>
+#include <random>
+#include <regex>
+#include <sstream>
+#include <thread>
+#include "common/logging/log.h"
+#include "enet/enet.h"
+#include "network/packet.h"
+#include "network/room.h"
+#include "network/verify_user.h"
+
+namespace Network {
+
+class Room::RoomImpl {
+public:
+    // This MAC address is used to generate a 'Nintendo' like Mac address.
+    const MacAddress NintendoOUI;
+    std::mt19937 random_gen; ///< Random number generator. Used for GenerateMacAddress
+
+    ENetHost* server = nullptr; ///< Network interface.
+
+    std::atomic<State> state{State::Closed}; ///< Current state of the room.
+    RoomInformation room_information;        ///< Information about this room.
+
+    std::string verify_UID;              ///< A GUID which may be used for verfication.
+    mutable std::mutex verify_UID_mutex; ///< Mutex for verify_UID
+
+    std::string password; ///< The password required to connect to this room.
+
+    struct Member {
+        std::string nickname;        ///< The nickname of the member.
+        std::string console_id_hash; ///< A hash of the console ID of the member.
+        GameInfo game_info;          ///< The current game of the member
+        MacAddress mac_address;      ///< The assigned mac address of the member.
+        /// Data of the user, often including authenticated forum username.
+        VerifyUser::UserData user_data;
+        ENetPeer* peer; ///< The remote peer.
+    };
+    using MemberList = std::vector<Member>;
+    MemberList members;              ///< Information about the members of this room
+    mutable std::mutex member_mutex; ///< Mutex for locking the members list
+    /// This should be a std::shared_mutex as soon as C++17 is supported
+
+    UsernameBanList username_ban_list; ///< List of banned usernames
+    IPBanList ip_ban_list;             ///< List of banned IP addresses
+    mutable std::mutex ban_list_mutex; ///< Mutex for the ban lists
+
+    RoomImpl()
+        : NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00}, random_gen(std::random_device()()) {}
+
+    /// Thread that receives and dispatches network packets
+    std::unique_ptr<std::thread> room_thread;
+
+    /// Verification backend of the room
+    std::unique_ptr<VerifyUser::Backend> verify_backend;
+
+    /// Thread function that will receive and dispatch messages until the room is destroyed.
+    void ServerLoop();
+    void StartLoop();
+
+    /**
+     * Parses and answers a room join request from a client.
+     * Validates the uniqueness of the username and assigns the MAC address
+     * that the client will use for the remainder of the connection.
+     */
+    void HandleJoinRequest(const ENetEvent* event);
+
+    /**
+     * Parses and answers a kick request from a client.
+     * Validates the permissions and that the given user exists and then kicks the member.
+     */
+    void HandleModKickPacket(const ENetEvent* event);
+
+    /**
+     * Parses and answers a ban request from a client.
+     * Validates the permissions and bans the user (by forum username or IP).
+     */
+    void HandleModBanPacket(const ENetEvent* event);
+
+    /**
+     * Parses and answers a unban request from a client.
+     * Validates the permissions and unbans the address.
+     */
+    void HandleModUnbanPacket(const ENetEvent* event);
+
+    /**
+     * Parses and answers a get ban list request from a client.
+     * Validates the permissions and returns the ban list.
+     */
+    void HandleModGetBanListPacket(const ENetEvent* event);
+
+    /**
+     * Returns whether the nickname is valid, ie. isn't already taken by someone else in the room.
+     */
+    bool IsValidNickname(const std::string& nickname) const;
+
+    /**
+     * Returns whether the MAC address is valid, ie. isn't already taken by someone else in the
+     * room.
+     */
+    bool IsValidMacAddress(const MacAddress& address) const;
+
+    /**
+     * Returns whether the console ID (hash) is valid, ie. isn't already taken by someone else in
+     * the room.
+     */
+    bool IsValidConsoleId(const std::string& console_id_hash) const;
+
+    /**
+     * Returns whether a user has mod permissions.
+     */
+    bool HasModPermission(const ENetPeer* client) const;
+
+    /**
+     * Sends a ID_ROOM_IS_FULL message telling the client that the room is full.
+     */
+    void SendRoomIsFull(ENetPeer* client);
+
+    /**
+     * Sends a ID_ROOM_NAME_COLLISION message telling the client that the name is invalid.
+     */
+    void SendNameCollision(ENetPeer* client);
+
+    /**
+     * Sends a ID_ROOM_MAC_COLLISION message telling the client that the MAC is invalid.
+     */
+    void SendMacCollision(ENetPeer* client);
+
+    /**
+     * Sends a IdConsoleIdCollison message telling the client that another member with the same
+     * console ID exists.
+     */
+    void SendConsoleIdCollision(ENetPeer* client);
+
+    /**
+     * Sends a ID_ROOM_VERSION_MISMATCH message telling the client that the version is invalid.
+     */
+    void SendVersionMismatch(ENetPeer* client);
+
+    /**
+     * Sends a ID_ROOM_WRONG_PASSWORD message telling the client that the password is wrong.
+     */
+    void SendWrongPassword(ENetPeer* client);
+
+    /**
+     * Notifies the member that its connection attempt was successful,
+     * and it is now part of the room.
+     */
+    void SendJoinSuccess(ENetPeer* client, MacAddress mac_address);
+
+    /**
+     * Notifies the member that its connection attempt was successful,
+     * and it is now part of the room, and it has been granted mod permissions.
+     */
+    void SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_address);
+
+    /**
+     * Sends a IdHostKicked message telling the client that they have been kicked.
+     */
+    void SendUserKicked(ENetPeer* client);
+
+    /**
+     * Sends a IdHostBanned message telling the client that they have been banned.
+     */
+    void SendUserBanned(ENetPeer* client);
+
+    /**
+     * Sends a IdModPermissionDenied message telling the client that they do not have mod
+     * permission.
+     */
+    void SendModPermissionDenied(ENetPeer* client);
+
+    /**
+     * Sends a IdModNoSuchUser message telling the client that the given user could not be found.
+     */
+    void SendModNoSuchUser(ENetPeer* client);
+
+    /**
+     * Sends the ban list in response to a client's request for getting ban list.
+     */
+    void SendModBanListResponse(ENetPeer* client);
+
+    /**
+     * Notifies the members that the room is closed,
+     */
+    void SendCloseMessage();
+
+    /**
+     * Sends a system message to all the connected clients.
+     */
+    void SendStatusMessage(StatusMessageTypes type, const std::string& nickname,
+                           const std::string& username, const std::string& ip);
+
+    /**
+     * Sends the information about the room, along with the list of members
+     * to every connected client in the room.
+     * The packet has the structure:
+     * <MessageID>ID_ROOM_INFORMATION
+     * <String> room_name
+     * <String> room_description
+     * <u32> member_slots: The max number of clients allowed in this room
+     * <String> uid
+     * <u16> port
+     * <u32> num_members: the number of currently joined clients
+     * This is followed by the following three values for each member:
+     * <String> nickname of that member
+     * <MacAddress> mac_address of that member
+     * <String> game_name of that member
+     */
+    void BroadcastRoomInformation();
+
+    /**
+     * Generates a free MAC address to assign to a new client.
+     * The first 3 bytes are the NintendoOUI 0x00, 0x1F, 0x32
+     */
+    MacAddress GenerateMacAddress();
+
+    /**
+     * Broadcasts this packet to all members except the sender.
+     * @param event The ENet event containing the data
+     */
+    void HandleWifiPacket(const ENetEvent* event);
+
+    /**
+     * Extracts a chat entry from a received ENet packet and adds it to the chat queue.
+     * @param event The ENet event that was received.
+     */
+    void HandleChatPacket(const ENetEvent* event);
+
+    /**
+     * Extracts the game name from a received ENet packet and broadcasts it.
+     * @param event The ENet event that was received.
+     */
+    void HandleGameNamePacket(const ENetEvent* event);
+
+    /**
+     * Removes the client from the members list if it was in it and announces the change
+     * to all other clients.
+     */
+    void HandleClientDisconnection(ENetPeer* client);
+};
+
+// RoomImpl
+void Room::RoomImpl::ServerLoop() {
+    while (state != State::Closed) {
+        ENetEvent event;
+        if (enet_host_service(server, &event, 50) > 0) {
+            switch (event.type) {
+            case ENET_EVENT_TYPE_RECEIVE:
+                switch (event.packet->data[0]) {
+                case IdJoinRequest:
+                    HandleJoinRequest(&event);
+                    break;
+                case IdSetGameInfo:
+                    HandleGameNamePacket(&event);
+                    break;
+                case IdWifiPacket:
+                    HandleWifiPacket(&event);
+                    break;
+                case IdChatMessage:
+                    HandleChatPacket(&event);
+                    break;
+                // Moderation
+                case IdModKick:
+                    HandleModKickPacket(&event);
+                    break;
+                case IdModBan:
+                    HandleModBanPacket(&event);
+                    break;
+                case IdModUnban:
+                    HandleModUnbanPacket(&event);
+                    break;
+                case IdModGetBanList:
+                    HandleModGetBanListPacket(&event);
+                    break;
+                }
+                enet_packet_destroy(event.packet);
+                break;
+            case ENET_EVENT_TYPE_DISCONNECT:
+                HandleClientDisconnection(event.peer);
+                break;
+            case ENET_EVENT_TYPE_NONE:
+            case ENET_EVENT_TYPE_CONNECT:
+                break;
+            }
+        }
+    }
+    // Close the connection to all members:
+    SendCloseMessage();
+}
+
+void Room::RoomImpl::StartLoop() {
+    room_thread = std::make_unique<std::thread>(&Room::RoomImpl::ServerLoop, this);
+}
+
+void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) {
+    {
+        std::lock_guard lock(member_mutex);
+        if (members.size() >= room_information.member_slots) {
+            SendRoomIsFull(event->peer);
+            return;
+        }
+    }
+    Packet packet;
+    packet.Append(event->packet->data, event->packet->dataLength);
+    packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
+    std::string nickname;
+    packet >> nickname;
+
+    std::string console_id_hash;
+    packet >> console_id_hash;
+
+    MacAddress preferred_mac;
+    packet >> preferred_mac;
+
+    u32 client_version;
+    packet >> client_version;
+
+    std::string pass;
+    packet >> pass;
+
+    std::string token;
+    packet >> token;
+
+    if (pass != password) {
+        SendWrongPassword(event->peer);
+        return;
+    }
+
+    if (!IsValidNickname(nickname)) {
+        SendNameCollision(event->peer);
+        return;
+    }
+
+    if (preferred_mac != NoPreferredMac) {
+        // Verify if the preferred mac is available
+        if (!IsValidMacAddress(preferred_mac)) {
+            SendMacCollision(event->peer);
+            return;
+        }
+    } else {
+        // Assign a MAC address of this client automatically
+        preferred_mac = GenerateMacAddress();
+    }
+
+    if (!IsValidConsoleId(console_id_hash)) {
+        SendConsoleIdCollision(event->peer);
+        return;
+    }
+
+    if (client_version != network_version) {
+        SendVersionMismatch(event->peer);
+        return;
+    }
+
+    // At this point the client is ready to be added to the room.
+    Member member{};
+    member.mac_address = preferred_mac;
+    member.console_id_hash = console_id_hash;
+    member.nickname = nickname;
+    member.peer = event->peer;
+
+    std::string uid;
+    {
+        std::lock_guard lock(verify_UID_mutex);
+        uid = verify_UID;
+    }
+    member.user_data = verify_backend->LoadUserData(uid, token);
+
+    std::string ip;
+    {
+        std::lock_guard lock(ban_list_mutex);
+
+        // Check username ban
+        if (!member.user_data.username.empty() &&
+            std::find(username_ban_list.begin(), username_ban_list.end(),
+                      member.user_data.username) != username_ban_list.end()) {
+
+            SendUserBanned(event->peer);
+            return;
+        }
+
+        // Check IP ban
+        char ip_raw[256];
+        enet_address_get_host_ip(&event->peer->address, ip_raw, sizeof(ip_raw) - 1);
+        ip = ip_raw;
+
+        if (std::find(ip_ban_list.begin(), ip_ban_list.end(), ip) != ip_ban_list.end()) {
+            SendUserBanned(event->peer);
+            return;
+        }
+    }
+
+    // Notify everyone that the user has joined.
+    SendStatusMessage(IdMemberJoin, member.nickname, member.user_data.username, ip);
+
+    {
+        std::lock_guard lock(member_mutex);
+        members.push_back(std::move(member));
+    }
+
+    // Notify everyone that the room information has changed.
+    BroadcastRoomInformation();
+    if (HasModPermission(event->peer)) {
+        SendJoinSuccessAsMod(event->peer, preferred_mac);
+    } else {
+        SendJoinSuccess(event->peer, preferred_mac);
+    }
+}
+
+void Room::RoomImpl::HandleModKickPacket(const ENetEvent* event) {
+    if (!HasModPermission(event->peer)) {
+        SendModPermissionDenied(event->peer);
+        return;
+    }
+
+    Packet packet;
+    packet.Append(event->packet->data, event->packet->dataLength);
+    packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
+
+    std::string nickname;
+    packet >> nickname;
+
+    std::string username, ip;
+    {
+        std::lock_guard lock(member_mutex);
+        const auto target_member =
+            std::find_if(members.begin(), members.end(),
+                         [&nickname](const auto& member) { return member.nickname == nickname; });
+        if (target_member == members.end()) {
+            SendModNoSuchUser(event->peer);
+            return;
+        }
+
+        // Notify the kicked member
+        SendUserKicked(target_member->peer);
+
+        username = target_member->user_data.username;
+
+        char ip_raw[256];
+        enet_address_get_host_ip(&target_member->peer->address, ip_raw, sizeof(ip_raw) - 1);
+        ip = ip_raw;
+
+        enet_peer_disconnect(target_member->peer, 0);
+        members.erase(target_member);
+    }
+
+    // Announce the change to all clients.
+    SendStatusMessage(IdMemberKicked, nickname, username, ip);
+    BroadcastRoomInformation();
+}
+
+void Room::RoomImpl::HandleModBanPacket(const ENetEvent* event) {
+    if (!HasModPermission(event->peer)) {
+        SendModPermissionDenied(event->peer);
+        return;
+    }
+
+    Packet packet;
+    packet.Append(event->packet->data, event->packet->dataLength);
+    packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
+
+    std::string nickname;
+    packet >> nickname;
+
+    std::string username, ip;
+    {
+        std::lock_guard lock(member_mutex);
+        const auto target_member =
+            std::find_if(members.begin(), members.end(),
+                         [&nickname](const auto& member) { return member.nickname == nickname; });
+        if (target_member == members.end()) {
+            SendModNoSuchUser(event->peer);
+            return;
+        }
+
+        // Notify the banned member
+        SendUserBanned(target_member->peer);
+
+        nickname = target_member->nickname;
+        username = target_member->user_data.username;
+
+        char ip_raw[256];
+        enet_address_get_host_ip(&target_member->peer->address, ip_raw, sizeof(ip_raw) - 1);
+        ip = ip_raw;
+
+        enet_peer_disconnect(target_member->peer, 0);
+        members.erase(target_member);
+    }
+
+    {
+        std::lock_guard lock(ban_list_mutex);
+
+        if (!username.empty()) {
+            // Ban the forum username
+            if (std::find(username_ban_list.begin(), username_ban_list.end(), username) ==
+                username_ban_list.end()) {
+
+                username_ban_list.emplace_back(username);
+            }
+        }
+
+        // Ban the member's IP as well
+        if (std::find(ip_ban_list.begin(), ip_ban_list.end(), ip) == ip_ban_list.end()) {
+            ip_ban_list.emplace_back(ip);
+        }
+    }
+
+    // Announce the change to all clients.
+    SendStatusMessage(IdMemberBanned, nickname, username, ip);
+    BroadcastRoomInformation();
+}
+
+void Room::RoomImpl::HandleModUnbanPacket(const ENetEvent* event) {
+    if (!HasModPermission(event->peer)) {
+        SendModPermissionDenied(event->peer);
+        return;
+    }
+
+    Packet packet;
+    packet.Append(event->packet->data, event->packet->dataLength);
+    packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
+
+    std::string address;
+    packet >> address;
+
+    bool unbanned = false;
+    {
+        std::lock_guard lock(ban_list_mutex);
+
+        auto it = std::find(username_ban_list.begin(), username_ban_list.end(), address);
+        if (it != username_ban_list.end()) {
+            unbanned = true;
+            username_ban_list.erase(it);
+        }
+
+        it = std::find(ip_ban_list.begin(), ip_ban_list.end(), address);
+        if (it != ip_ban_list.end()) {
+            unbanned = true;
+            ip_ban_list.erase(it);
+        }
+    }
+
+    if (unbanned) {
+        SendStatusMessage(IdAddressUnbanned, address, "", "");
+    } else {
+        SendModNoSuchUser(event->peer);
+    }
+}
+
+void Room::RoomImpl::HandleModGetBanListPacket(const ENetEvent* event) {
+    if (!HasModPermission(event->peer)) {
+        SendModPermissionDenied(event->peer);
+        return;
+    }
+
+    SendModBanListResponse(event->peer);
+}
+
+bool Room::RoomImpl::IsValidNickname(const std::string& nickname) const {
+    // A nickname is valid if it matches the regex and is not already taken by anybody else in the
+    // room.
+    const std::regex nickname_regex("^[ a-zA-Z0-9._-]{4,20}$");
+    if (!std::regex_match(nickname, nickname_regex))
+        return false;
+
+    std::lock_guard lock(member_mutex);
+    return std::all_of(members.begin(), members.end(),
+                       [&nickname](const auto& member) { return member.nickname != nickname; });
+}
+
+bool Room::RoomImpl::IsValidMacAddress(const MacAddress& address) const {
+    // A MAC address is valid if it is not already taken by anybody else in the room.
+    std::lock_guard lock(member_mutex);
+    return std::all_of(members.begin(), members.end(),
+                       [&address](const auto& member) { return member.mac_address != address; });
+}
+
+bool Room::RoomImpl::IsValidConsoleId(const std::string& console_id_hash) const {
+    // A Console ID is valid if it is not already taken by anybody else in the room.
+    std::lock_guard lock(member_mutex);
+    return std::all_of(members.begin(), members.end(), [&console_id_hash](const auto& member) {
+        return member.console_id_hash != console_id_hash;
+    });
+}
+
+bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const {
+    std::lock_guard lock(member_mutex);
+    const auto sending_member =
+        std::find_if(members.begin(), members.end(),
+                     [client](const auto& member) { return member.peer == client; });
+    if (sending_member == members.end()) {
+        return false;
+    }
+    if (room_information.enable_citra_mods &&
+        sending_member->user_data.moderator) { // Community moderator
+
+        return true;
+    }
+    if (!room_information.host_username.empty() &&
+        sending_member->user_data.username == room_information.host_username) { // Room host
+
+        return true;
+    }
+    return false;
+}
+
+void Room::RoomImpl::SendNameCollision(ENetPeer* client) {
+    Packet packet;
+    packet << static_cast<u8>(IdNameCollision);
+
+    ENetPacket* enet_packet =
+        enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+    enet_peer_send(client, 0, enet_packet);
+    enet_host_flush(server);
+}
+
+void Room::RoomImpl::SendMacCollision(ENetPeer* client) {
+    Packet packet;
+    packet << static_cast<u8>(IdMacCollision);
+
+    ENetPacket* enet_packet =
+        enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+    enet_peer_send(client, 0, enet_packet);
+    enet_host_flush(server);
+}
+
+void Room::RoomImpl::SendConsoleIdCollision(ENetPeer* client) {
+    Packet packet;
+    packet << static_cast<u8>(IdConsoleIdCollision);
+
+    ENetPacket* enet_packet =
+        enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+    enet_peer_send(client, 0, enet_packet);
+    enet_host_flush(server);
+}
+
+void Room::RoomImpl::SendWrongPassword(ENetPeer* client) {
+    Packet packet;
+    packet << static_cast<u8>(IdWrongPassword);
+
+    ENetPacket* enet_packet =
+        enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+    enet_peer_send(client, 0, enet_packet);
+    enet_host_flush(server);
+}
+
+void Room::RoomImpl::SendRoomIsFull(ENetPeer* client) {
+    Packet packet;
+    packet << static_cast<u8>(IdRoomIsFull);
+
+    ENetPacket* enet_packet =
+        enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+    enet_peer_send(client, 0, enet_packet);
+    enet_host_flush(server);
+}
+
+void Room::RoomImpl::SendVersionMismatch(ENetPeer* client) {
+    Packet packet;
+    packet << static_cast<u8>(IdVersionMismatch);
+    packet << network_version;
+
+    ENetPacket* enet_packet =
+        enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+    enet_peer_send(client, 0, enet_packet);
+    enet_host_flush(server);
+}
+
+void Room::RoomImpl::SendJoinSuccess(ENetPeer* client, MacAddress mac_address) {
+    Packet packet;
+    packet << static_cast<u8>(IdJoinSuccess);
+    packet << mac_address;
+    ENetPacket* enet_packet =
+        enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+    enet_peer_send(client, 0, enet_packet);
+    enet_host_flush(server);
+}
+
+void Room::RoomImpl::SendJoinSuccessAsMod(ENetPeer* client, MacAddress mac_address) {
+    Packet packet;
+    packet << static_cast<u8>(IdJoinSuccessAsMod);
+    packet << mac_address;
+    ENetPacket* enet_packet =
+        enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+    enet_peer_send(client, 0, enet_packet);
+    enet_host_flush(server);
+}
+
+void Room::RoomImpl::SendUserKicked(ENetPeer* client) {
+    Packet packet;
+    packet << static_cast<u8>(IdHostKicked);
+
+    ENetPacket* enet_packet =
+        enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+    enet_peer_send(client, 0, enet_packet);
+    enet_host_flush(server);
+}
+
+void Room::RoomImpl::SendUserBanned(ENetPeer* client) {
+    Packet packet;
+    packet << static_cast<u8>(IdHostBanned);
+
+    ENetPacket* enet_packet =
+        enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+    enet_peer_send(client, 0, enet_packet);
+    enet_host_flush(server);
+}
+
+void Room::RoomImpl::SendModPermissionDenied(ENetPeer* client) {
+    Packet packet;
+    packet << static_cast<u8>(IdModPermissionDenied);
+
+    ENetPacket* enet_packet =
+        enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+    enet_peer_send(client, 0, enet_packet);
+    enet_host_flush(server);
+}
+
+void Room::RoomImpl::SendModNoSuchUser(ENetPeer* client) {
+    Packet packet;
+    packet << static_cast<u8>(IdModNoSuchUser);
+
+    ENetPacket* enet_packet =
+        enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+    enet_peer_send(client, 0, enet_packet);
+    enet_host_flush(server);
+}
+
+void Room::RoomImpl::SendModBanListResponse(ENetPeer* client) {
+    Packet packet;
+    packet << static_cast<u8>(IdModBanListResponse);
+    {
+        std::lock_guard lock(ban_list_mutex);
+        packet << username_ban_list;
+        packet << ip_ban_list;
+    }
+
+    ENetPacket* enet_packet =
+        enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+    enet_peer_send(client, 0, enet_packet);
+    enet_host_flush(server);
+}
+
+void Room::RoomImpl::SendCloseMessage() {
+    Packet packet;
+    packet << static_cast<u8>(IdCloseRoom);
+    std::lock_guard lock(member_mutex);
+    if (!members.empty()) {
+        ENetPacket* enet_packet =
+            enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+        for (auto& member : members) {
+            enet_peer_send(member.peer, 0, enet_packet);
+        }
+    }
+    enet_host_flush(server);
+    for (auto& member : members) {
+        enet_peer_disconnect(member.peer, 0);
+    }
+}
+
+void Room::RoomImpl::SendStatusMessage(StatusMessageTypes type, const std::string& nickname,
+                                       const std::string& username, const std::string& ip) {
+    Packet packet;
+    packet << static_cast<u8>(IdStatusMessage);
+    packet << static_cast<u8>(type);
+    packet << nickname;
+    packet << username;
+    std::lock_guard lock(member_mutex);
+    if (!members.empty()) {
+        ENetPacket* enet_packet =
+            enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+        for (auto& member : members) {
+            enet_peer_send(member.peer, 0, enet_packet);
+        }
+    }
+    enet_host_flush(server);
+
+    const std::string display_name =
+        username.empty() ? nickname : fmt::format("{} ({})", nickname, username);
+
+    switch (type) {
+    case IdMemberJoin:
+        LOG_INFO(Network, "[{}] {} has joined.", ip, display_name);
+        break;
+    case IdMemberLeave:
+        LOG_INFO(Network, "[{}] {} has left.", ip, display_name);
+        break;
+    case IdMemberKicked:
+        LOG_INFO(Network, "[{}] {} has been kicked.", ip, display_name);
+        break;
+    case IdMemberBanned:
+        LOG_INFO(Network, "[{}] {} has been banned.", ip, display_name);
+        break;
+    case IdAddressUnbanned:
+        LOG_INFO(Network, "{} has been unbanned.", display_name);
+        break;
+    }
+}
+
+void Room::RoomImpl::BroadcastRoomInformation() {
+    Packet packet;
+    packet << static_cast<u8>(IdRoomInformation);
+    packet << room_information.name;
+    packet << room_information.description;
+    packet << room_information.member_slots;
+    packet << room_information.port;
+    packet << room_information.preferred_game;
+    packet << room_information.host_username;
+
+    packet << static_cast<u32>(members.size());
+    {
+        std::lock_guard lock(member_mutex);
+        for (const auto& member : members) {
+            packet << member.nickname;
+            packet << member.mac_address;
+            packet << member.game_info.name;
+            packet << member.game_info.id;
+            packet << member.user_data.username;
+            packet << member.user_data.display_name;
+            packet << member.user_data.avatar_url;
+        }
+    }
+
+    ENetPacket* enet_packet =
+        enet_packet_create(packet.GetData(), packet.GetDataSize(), ENET_PACKET_FLAG_RELIABLE);
+    enet_host_broadcast(server, 0, enet_packet);
+    enet_host_flush(server);
+}
+
+MacAddress Room::RoomImpl::GenerateMacAddress() {
+    MacAddress result_mac =
+        NintendoOUI; // The first three bytes of each MAC address will be the NintendoOUI
+    std::uniform_int_distribution<> dis(0x00, 0xFF); // Random byte between 0 and 0xFF
+    do {
+        for (std::size_t i = 3; i < result_mac.size(); ++i) {
+            result_mac[i] = dis(random_gen);
+        }
+    } while (!IsValidMacAddress(result_mac));
+    return result_mac;
+}
+
+void Room::RoomImpl::HandleWifiPacket(const ENetEvent* event) {
+    Packet in_packet;
+    in_packet.Append(event->packet->data, event->packet->dataLength);
+    in_packet.IgnoreBytes(sizeof(u8));         // Message type
+    in_packet.IgnoreBytes(sizeof(u8));         // WifiPacket Type
+    in_packet.IgnoreBytes(sizeof(u8));         // WifiPacket Channel
+    in_packet.IgnoreBytes(sizeof(MacAddress)); // WifiPacket Transmitter Address
+    MacAddress destination_address;
+    in_packet >> destination_address;
+
+    Packet out_packet;
+    out_packet.Append(event->packet->data, event->packet->dataLength);
+    ENetPacket* enet_packet = enet_packet_create(out_packet.GetData(), out_packet.GetDataSize(),
+                                                 ENET_PACKET_FLAG_RELIABLE);
+
+    if (destination_address == BroadcastMac) { // Send the data to everyone except the sender
+        std::lock_guard lock(member_mutex);
+        bool sent_packet = false;
+        for (const auto& member : members) {
+            if (member.peer != event->peer) {
+                sent_packet = true;
+                enet_peer_send(member.peer, 0, enet_packet);
+            }
+        }
+
+        if (!sent_packet) {
+            enet_packet_destroy(enet_packet);
+        }
+    } else { // Send the data only to the destination client
+        std::lock_guard lock(member_mutex);
+        auto member = std::find_if(members.begin(), members.end(),
+                                   [destination_address](const Member& member) -> bool {
+                                       return member.mac_address == destination_address;
+                                   });
+        if (member != members.end()) {
+            enet_peer_send(member->peer, 0, enet_packet);
+        } else {
+            LOG_ERROR(Network,
+                      "Attempting to send to unknown MAC address: "
+                      "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}",
+                      destination_address[0], destination_address[1], destination_address[2],
+                      destination_address[3], destination_address[4], destination_address[5]);
+            enet_packet_destroy(enet_packet);
+        }
+    }
+    enet_host_flush(server);
+}
+
+void Room::RoomImpl::HandleChatPacket(const ENetEvent* event) {
+    Packet in_packet;
+    in_packet.Append(event->packet->data, event->packet->dataLength);
+
+    in_packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
+    std::string message;
+    in_packet >> message;
+    auto CompareNetworkAddress = [event](const Member member) -> bool {
+        return member.peer == event->peer;
+    };
+
+    std::lock_guard lock(member_mutex);
+    const auto sending_member = std::find_if(members.begin(), members.end(), CompareNetworkAddress);
+    if (sending_member == members.end()) {
+        return; // Received a chat message from a unknown sender
+    }
+
+    // Limit the size of chat messages to MaxMessageSize
+    message.resize(std::min(static_cast<u32>(message.size()), MaxMessageSize));
+
+    Packet out_packet;
+    out_packet << static_cast<u8>(IdChatMessage);
+    out_packet << sending_member->nickname;
+    out_packet << sending_member->user_data.username;
+    out_packet << message;
+
+    ENetPacket* enet_packet = enet_packet_create(out_packet.GetData(), out_packet.GetDataSize(),
+                                                 ENET_PACKET_FLAG_RELIABLE);
+    bool sent_packet = false;
+    for (const auto& member : members) {
+        if (member.peer != event->peer) {
+            sent_packet = true;
+            enet_peer_send(member.peer, 0, enet_packet);
+        }
+    }
+
+    if (!sent_packet) {
+        enet_packet_destroy(enet_packet);
+    }
+
+    enet_host_flush(server);
+
+    if (sending_member->user_data.username.empty()) {
+        LOG_INFO(Network, "{}: {}", sending_member->nickname, message);
+    } else {
+        LOG_INFO(Network, "{} ({}): {}", sending_member->nickname,
+                 sending_member->user_data.username, message);
+    }
+}
+
+void Room::RoomImpl::HandleGameNamePacket(const ENetEvent* event) {
+    Packet in_packet;
+    in_packet.Append(event->packet->data, event->packet->dataLength);
+
+    in_packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
+    GameInfo game_info;
+    in_packet >> game_info.name;
+    in_packet >> game_info.id;
+
+    {
+        std::lock_guard lock(member_mutex);
+        auto member =
+            std::find_if(members.begin(), members.end(), [event](const Member& member) -> bool {
+                return member.peer == event->peer;
+            });
+        if (member != members.end()) {
+            member->game_info = game_info;
+
+            const std::string display_name =
+                member->user_data.username.empty()
+                    ? member->nickname
+                    : fmt::format("{} ({})", member->nickname, member->user_data.username);
+
+            if (game_info.name.empty()) {
+                LOG_INFO(Network, "{} is not playing", display_name);
+            } else {
+                LOG_INFO(Network, "{} is playing {}", display_name, game_info.name);
+            }
+        }
+    }
+    BroadcastRoomInformation();
+}
+
+void Room::RoomImpl::HandleClientDisconnection(ENetPeer* client) {
+    // Remove the client from the members list.
+    std::string nickname, username, ip;
+    {
+        std::lock_guard lock(member_mutex);
+        auto member = std::find_if(members.begin(), members.end(), [client](const Member& member) {
+            return member.peer == client;
+        });
+        if (member != members.end()) {
+            nickname = member->nickname;
+            username = member->user_data.username;
+
+            char ip_raw[256];
+            enet_address_get_host_ip(&member->peer->address, ip_raw, sizeof(ip_raw) - 1);
+            ip = ip_raw;
+
+            members.erase(member);
+        }
+    }
+
+    // Announce the change to all clients.
+    enet_peer_disconnect(client, 0);
+    if (!nickname.empty())
+        SendStatusMessage(IdMemberLeave, nickname, username, ip);
+    BroadcastRoomInformation();
+}
+
+// Room
+Room::Room() : room_impl{std::make_unique<RoomImpl>()} {}
+
+Room::~Room() = default;
+
+bool Room::Create(const std::string& name, const std::string& description,
+                  const std::string& server_address, u16 server_port, const std::string& password,
+                  const u32 max_connections, const std::string& host_username,
+                  const std::string& preferred_game, u64 preferred_game_id,
+                  std::unique_ptr<VerifyUser::Backend> verify_backend,
+                  const Room::BanList& ban_list, bool enable_citra_mods) {
+    ENetAddress address;
+    address.host = ENET_HOST_ANY;
+    if (!server_address.empty()) {
+        enet_address_set_host(&address, server_address.c_str());
+    }
+    address.port = server_port;
+
+    // In order to send the room is full message to the connecting client, we need to leave one
+    // slot open so enet won't reject the incoming connection without telling us
+    room_impl->server = enet_host_create(&address, max_connections + 1, NumChannels, 0, 0);
+    if (!room_impl->server) {
+        return false;
+    }
+    room_impl->state = State::Open;
+
+    room_impl->room_information.name = name;
+    room_impl->room_information.description = description;
+    room_impl->room_information.member_slots = max_connections;
+    room_impl->room_information.port = server_port;
+    room_impl->room_information.preferred_game = preferred_game;
+    room_impl->room_information.preferred_game_id = preferred_game_id;
+    room_impl->room_information.host_username = host_username;
+    room_impl->room_information.enable_citra_mods = enable_citra_mods;
+    room_impl->password = password;
+    room_impl->verify_backend = std::move(verify_backend);
+    room_impl->username_ban_list = ban_list.first;
+    room_impl->ip_ban_list = ban_list.second;
+
+    room_impl->StartLoop();
+    return true;
+}
+
+Room::State Room::GetState() const {
+    return room_impl->state;
+}
+
+const RoomInformation& Room::GetRoomInformation() const {
+    return room_impl->room_information;
+}
+
+std::string Room::GetVerifyUID() const {
+    std::lock_guard lock(room_impl->verify_UID_mutex);
+    return room_impl->verify_UID;
+}
+
+Room::BanList Room::GetBanList() const {
+    std::lock_guard lock(room_impl->ban_list_mutex);
+    return {room_impl->username_ban_list, room_impl->ip_ban_list};
+}
+
+std::vector<Room::Member> Room::GetRoomMemberList() const {
+    std::vector<Room::Member> member_list;
+    std::lock_guard lock(room_impl->member_mutex);
+    for (const auto& member_impl : room_impl->members) {
+        Member member;
+        member.nickname = member_impl.nickname;
+        member.username = member_impl.user_data.username;
+        member.display_name = member_impl.user_data.display_name;
+        member.avatar_url = member_impl.user_data.avatar_url;
+        member.mac_address = member_impl.mac_address;
+        member.game_info = member_impl.game_info;
+        member_list.push_back(member);
+    }
+    return member_list;
+}
+
+bool Room::HasPassword() const {
+    return !room_impl->password.empty();
+}
+
+void Room::SetVerifyUID(const std::string& uid) {
+    std::lock_guard lock(room_impl->verify_UID_mutex);
+    room_impl->verify_UID = uid;
+}
+
+void Room::Destroy() {
+    room_impl->state = State::Closed;
+    room_impl->room_thread->join();
+    room_impl->room_thread.reset();
+
+    if (room_impl->server) {
+        enet_host_destroy(room_impl->server);
+    }
+    room_impl->room_information = {};
+    room_impl->server = nullptr;
+    {
+        std::lock_guard lock(room_impl->member_mutex);
+        room_impl->members.clear();
+    }
+    room_impl->room_information.member_slots = 0;
+    room_impl->room_information.name.clear();
+}
+
+} // namespace Network
diff --git a/src/network/room.h b/src/network/room.h
new file mode 100644
index 000000000..a67984837
--- /dev/null
+++ b/src/network/room.h
@@ -0,0 +1,173 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <array>
+#include <memory>
+#include <string>
+#include <vector>
+#include "common/common_types.h"
+#include "network/verify_user.h"
+
+namespace Network {
+
+constexpr u32 network_version = 4; ///< The version of this Room and RoomMember
+
+constexpr u16 DefaultRoomPort = 24872;
+
+constexpr u32 MaxMessageSize = 500;
+
+/// Maximum number of concurrent connections allowed to this room.
+static constexpr u32 MaxConcurrentConnections = 254;
+
+constexpr std::size_t NumChannels = 1; // Number of channels used for the connection
+
+struct RoomInformation {
+    std::string name;           ///< Name of the server
+    std::string description;    ///< Server description
+    u32 member_slots;           ///< Maximum number of members in this room
+    u16 port;                   ///< The port of this room
+    std::string preferred_game; ///< Game to advertise that you want to play
+    u64 preferred_game_id;      ///< Title ID for the advertised game
+    std::string host_username;  ///< Forum username of the host
+    bool enable_citra_mods;     ///< Allow Citra Moderators to moderate on this room
+};
+
+struct GameInfo {
+    std::string name{""};
+    u64 id{0};
+};
+
+using MacAddress = std::array<u8, 6>;
+/// A special MAC address that tells the room we're joining to assign us a MAC address
+/// automatically.
+constexpr MacAddress NoPreferredMac = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
+
+// 802.11 broadcast MAC address
+constexpr MacAddress BroadcastMac = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
+
+// The different types of messages that can be sent. The first byte of each packet defines the type
+enum RoomMessageTypes : u8 {
+    IdJoinRequest = 1,
+    IdJoinSuccess,
+    IdRoomInformation,
+    IdSetGameInfo,
+    IdWifiPacket,
+    IdChatMessage,
+    IdNameCollision,
+    IdMacCollision,
+    IdVersionMismatch,
+    IdWrongPassword,
+    IdCloseRoom,
+    IdRoomIsFull,
+    IdConsoleIdCollision,
+    IdStatusMessage,
+    IdHostKicked,
+    IdHostBanned,
+    /// Moderation requests
+    IdModKick,
+    IdModBan,
+    IdModUnban,
+    IdModGetBanList,
+    // Moderation responses
+    IdModBanListResponse,
+    IdModPermissionDenied,
+    IdModNoSuchUser,
+    IdJoinSuccessAsMod,
+};
+
+/// Types of system status messages
+enum StatusMessageTypes : u8 {
+    IdMemberJoin = 1,  ///< Member joining
+    IdMemberLeave,     ///< Member leaving
+    IdMemberKicked,    ///< A member is kicked from the room
+    IdMemberBanned,    ///< A member is banned from the room
+    IdAddressUnbanned, ///< A username / ip address is unbanned from the room
+};
+
+/// This is what a server [person creating a server] would use.
+class Room final {
+public:
+    enum class State : u8 {
+        Open,   ///< The room is open and ready to accept connections.
+        Closed, ///< The room is not opened and can not accept connections.
+    };
+
+    struct Member {
+        std::string nickname;     ///< The nickname of the member.
+        std::string username;     ///< The web services username of the member. Can be empty.
+        std::string display_name; ///< The web services display name of the member. Can be empty.
+        std::string avatar_url;   ///< Url to the member's avatar. Can be empty.
+        GameInfo game_info;       ///< The current game of the member
+        MacAddress mac_address;   ///< The assigned mac address of the member.
+    };
+
+    Room();
+    ~Room();
+
+    /**
+     * Gets the current state of the room.
+     */
+    State GetState() const;
+
+    /**
+     * Gets the room information of the room.
+     */
+    const RoomInformation& GetRoomInformation() const;
+
+    /**
+     * Gets the verify UID of this room.
+     */
+    std::string GetVerifyUID() const;
+
+    /**
+     * Gets a list of the mbmers connected to the room.
+     */
+    std::vector<Member> GetRoomMemberList() const;
+
+    /**
+     * Checks if the room is password protected
+     */
+    bool HasPassword() const;
+
+    using UsernameBanList = std::vector<std::string>;
+    using IPBanList = std::vector<std::string>;
+
+    using BanList = std::pair<UsernameBanList, IPBanList>;
+
+    /**
+     * Creates the socket for this room. Will bind to default address if
+     * server is empty string.
+     */
+    bool Create(const std::string& name, const std::string& description = "",
+                const std::string& server = "", u16 server_port = DefaultRoomPort,
+                const std::string& password = "",
+                const u32 max_connections = MaxConcurrentConnections,
+                const std::string& host_username = "", const std::string& preferred_game = "",
+                u64 preferred_game_id = 0,
+                std::unique_ptr<VerifyUser::Backend> verify_backend = nullptr,
+                const BanList& ban_list = {}, bool enable_citra_mods = false);
+
+    /**
+     * Sets the verification GUID of the room.
+     */
+    void SetVerifyUID(const std::string& uid);
+
+    /**
+     * Gets the ban list (including banned forum usernames and IPs) of the room.
+     */
+    BanList GetBanList() const;
+
+    /**
+     * Destroys the socket
+     */
+    void Destroy();
+
+private:
+    class RoomImpl;
+    std::unique_ptr<RoomImpl> room_impl;
+};
+
+} // namespace Network
diff --git a/src/network/room_member.cpp b/src/network/room_member.cpp
new file mode 100644
index 000000000..e43004027
--- /dev/null
+++ b/src/network/room_member.cpp
@@ -0,0 +1,694 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <atomic>
+#include <list>
+#include <mutex>
+#include <set>
+#include <thread>
+#include "common/assert.h"
+#include "enet/enet.h"
+#include "network/packet.h"
+#include "network/room_member.h"
+
+namespace Network {
+
+constexpr u32 ConnectionTimeoutMs = 5000;
+
+class RoomMember::RoomMemberImpl {
+public:
+    ENetHost* client = nullptr; ///< ENet network interface.
+    ENetPeer* server = nullptr; ///< The server peer the client is connected to
+
+    /// Information about the clients connected to the same room as us.
+    MemberList member_information;
+    /// Information about the room we're connected to.
+    RoomInformation room_information;
+
+    /// The current game name, id and version
+    GameInfo current_game_info;
+
+    std::atomic<State> state{State::Idle}; ///< Current state of the RoomMember.
+    void SetState(const State new_state);
+    void SetError(const Error new_error);
+    bool IsConnected() const;
+
+    std::string nickname; ///< The nickname of this member.
+
+    std::string username;              ///< The username of this member.
+    mutable std::mutex username_mutex; ///< Mutex for locking username.
+
+    MacAddress mac_address; ///< The mac_address of this member.
+
+    std::mutex network_mutex; ///< Mutex that controls access to the `client` variable.
+    /// Thread that receives and dispatches network packets
+    std::unique_ptr<std::thread> loop_thread;
+    std::mutex send_list_mutex;  ///< Mutex that controls access to the `send_list` variable.
+    std::list<Packet> send_list; ///< A list that stores all packets to send the async
+
+    template <typename T>
+    using CallbackSet = std::set<CallbackHandle<T>>;
+    std::mutex callback_mutex; ///< The mutex used for handling callbacks
+
+    class Callbacks {
+    public:
+        template <typename T>
+        CallbackSet<T>& Get();
+
+    private:
+        CallbackSet<WifiPacket> callback_set_wifi_packet;
+        CallbackSet<ChatEntry> callback_set_chat_messages;
+        CallbackSet<StatusMessageEntry> callback_set_status_messages;
+        CallbackSet<RoomInformation> callback_set_room_information;
+        CallbackSet<State> callback_set_state;
+        CallbackSet<Error> callback_set_error;
+        CallbackSet<Room::BanList> callback_set_ban_list;
+    };
+    Callbacks callbacks; ///< All CallbackSets to all events
+
+    void MemberLoop();
+
+    void StartLoop();
+
+    /**
+     * Sends data to the room. It will be send on channel 0 with flag RELIABLE
+     * @param packet The data to send
+     */
+    void Send(Packet&& packet);
+
+    /**
+     * Sends a request to the server, asking for permission to join a room with the specified
+     * nickname and preferred mac.
+     * @params nickname The desired nickname.
+     * @params console_id_hash A hash of the Console ID.
+     * @params preferred_mac The preferred MAC address to use in the room, the NoPreferredMac tells
+     * @params password The password for the room
+     * the server to assign one for us.
+     */
+    void SendJoinRequest(const std::string& nickname, const std::string& console_id_hash,
+                         const MacAddress& preferred_mac = NoPreferredMac,
+                         const std::string& password = "", const std::string& token = "");
+
+    /**
+     * Extracts a MAC Address from a received ENet packet.
+     * @param event The ENet event that was received.
+     */
+    void HandleJoinPacket(const ENetEvent* event);
+    /**
+     * Extracts RoomInformation and MemberInformation from a received ENet packet.
+     * @param event The ENet event that was received.
+     */
+    void HandleRoomInformationPacket(const ENetEvent* event);
+
+    /**
+     * Extracts a WifiPacket from a received ENet packet.
+     * @param event The  ENet event that was received.
+     */
+    void HandleWifiPackets(const ENetEvent* event);
+
+    /**
+     * Extracts a chat entry from a received ENet packet and adds it to the chat queue.
+     * @param event The ENet event that was received.
+     */
+    void HandleChatPacket(const ENetEvent* event);
+
+    /**
+     * Extracts a system message entry from a received ENet packet and adds it to the system message
+     * queue.
+     * @param event The ENet event that was received.
+     */
+    void HandleStatusMessagePacket(const ENetEvent* event);
+
+    /**
+     * Extracts a ban list request response from a received ENet packet.
+     * @param event The ENet event that was received.
+     */
+    void HandleModBanListResponsePacket(const ENetEvent* event);
+
+    /**
+     * Disconnects the RoomMember from the Room
+     */
+    void Disconnect();
+
+    template <typename T>
+    void Invoke(const T& data);
+
+    template <typename T>
+    CallbackHandle<T> Bind(std::function<void(const T&)> callback);
+};
+
+// RoomMemberImpl
+void RoomMember::RoomMemberImpl::SetState(const State new_state) {
+    if (state != new_state) {
+        state = new_state;
+        Invoke<State>(state);
+    }
+}
+
+void RoomMember::RoomMemberImpl::SetError(const Error new_error) {
+    Invoke<Error>(new_error);
+}
+
+bool RoomMember::RoomMemberImpl::IsConnected() const {
+    return state == State::Joining || state == State::Joined || state == State::Moderator;
+}
+
+void RoomMember::RoomMemberImpl::MemberLoop() {
+    // Receive packets while the connection is open
+    while (IsConnected()) {
+        std::lock_guard lock(network_mutex);
+        ENetEvent event;
+        if (enet_host_service(client, &event, 100) > 0) {
+            switch (event.type) {
+            case ENET_EVENT_TYPE_RECEIVE:
+                switch (event.packet->data[0]) {
+                case IdWifiPacket:
+                    HandleWifiPackets(&event);
+                    break;
+                case IdChatMessage:
+                    HandleChatPacket(&event);
+                    break;
+                case IdStatusMessage:
+                    HandleStatusMessagePacket(&event);
+                    break;
+                case IdRoomInformation:
+                    HandleRoomInformationPacket(&event);
+                    break;
+                case IdJoinSuccess:
+                case IdJoinSuccessAsMod:
+                    // The join request was successful, we are now in the room.
+                    // If we joined successfully, there must be at least one client in the room: us.
+                    ASSERT_MSG(member_information.size() > 0,
+                               "We have not yet received member information.");
+                    HandleJoinPacket(&event); // Get the MAC Address for the client
+                    if (event.packet->data[0] == IdJoinSuccessAsMod) {
+                        SetState(State::Moderator);
+                    } else {
+                        SetState(State::Joined);
+                    }
+                    break;
+                case IdModBanListResponse:
+                    HandleModBanListResponsePacket(&event);
+                    break;
+                case IdRoomIsFull:
+                    SetState(State::Idle);
+                    SetError(Error::RoomIsFull);
+                    break;
+                case IdNameCollision:
+                    SetState(State::Idle);
+                    SetError(Error::NameCollision);
+                    break;
+                case IdMacCollision:
+                    SetState(State::Idle);
+                    SetError(Error::MacCollision);
+                    break;
+                case IdConsoleIdCollision:
+                    SetState(State::Idle);
+                    SetError(Error::ConsoleIdCollision);
+                    break;
+                case IdVersionMismatch:
+                    SetState(State::Idle);
+                    SetError(Error::WrongVersion);
+                    break;
+                case IdWrongPassword:
+                    SetState(State::Idle);
+                    SetError(Error::WrongPassword);
+                    break;
+                case IdCloseRoom:
+                    SetState(State::Idle);
+                    SetError(Error::LostConnection);
+                    break;
+                case IdHostKicked:
+                    SetState(State::Idle);
+                    SetError(Error::HostKicked);
+                    break;
+                case IdHostBanned:
+                    SetState(State::Idle);
+                    SetError(Error::HostBanned);
+                    break;
+                case IdModPermissionDenied:
+                    SetError(Error::PermissionDenied);
+                    break;
+                case IdModNoSuchUser:
+                    SetError(Error::NoSuchUser);
+                    break;
+                }
+                enet_packet_destroy(event.packet);
+                break;
+            case ENET_EVENT_TYPE_DISCONNECT:
+                if (state == State::Joined || state == State::Moderator) {
+                    SetState(State::Idle);
+                    SetError(Error::LostConnection);
+                }
+                break;
+            case ENET_EVENT_TYPE_NONE:
+                break;
+            case ENET_EVENT_TYPE_CONNECT:
+                // The ENET_EVENT_TYPE_CONNECT event can not possibly happen here because we're
+                // already connected
+                ASSERT_MSG(false, "Received unexpected connect event while already connected");
+                break;
+            }
+        }
+        {
+            std::lock_guard lock(send_list_mutex);
+            for (const auto& packet : send_list) {
+                ENetPacket* enetPacket = enet_packet_create(packet.GetData(), packet.GetDataSize(),
+                                                            ENET_PACKET_FLAG_RELIABLE);
+                enet_peer_send(server, 0, enetPacket);
+            }
+            enet_host_flush(client);
+            send_list.clear();
+        }
+    }
+    Disconnect();
+};
+
+void RoomMember::RoomMemberImpl::StartLoop() {
+    loop_thread = std::make_unique<std::thread>(&RoomMember::RoomMemberImpl::MemberLoop, this);
+}
+
+void RoomMember::RoomMemberImpl::Send(Packet&& packet) {
+    std::lock_guard lock(send_list_mutex);
+    send_list.push_back(std::move(packet));
+}
+
+void RoomMember::RoomMemberImpl::SendJoinRequest(const std::string& nickname,
+                                                 const std::string& console_id_hash,
+                                                 const MacAddress& preferred_mac,
+                                                 const std::string& password,
+                                                 const std::string& token) {
+    Packet packet;
+    packet << static_cast<u8>(IdJoinRequest);
+    packet << nickname;
+    packet << console_id_hash;
+    packet << preferred_mac;
+    packet << network_version;
+    packet << password;
+    packet << token;
+    Send(std::move(packet));
+}
+
+void RoomMember::RoomMemberImpl::HandleRoomInformationPacket(const ENetEvent* event) {
+    Packet packet;
+    packet.Append(event->packet->data, event->packet->dataLength);
+
+    // Ignore the first byte, which is the message id.
+    packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
+
+    RoomInformation info{};
+    packet >> info.name;
+    packet >> info.description;
+    packet >> info.member_slots;
+    packet >> info.port;
+    packet >> info.preferred_game;
+    packet >> info.host_username;
+    room_information.name = info.name;
+    room_information.description = info.description;
+    room_information.member_slots = info.member_slots;
+    room_information.port = info.port;
+    room_information.preferred_game = info.preferred_game;
+    room_information.host_username = info.host_username;
+
+    u32 num_members;
+    packet >> num_members;
+    member_information.resize(num_members);
+
+    for (auto& member : member_information) {
+        packet >> member.nickname;
+        packet >> member.mac_address;
+        packet >> member.game_info.name;
+        packet >> member.game_info.id;
+        packet >> member.username;
+        packet >> member.display_name;
+        packet >> member.avatar_url;
+
+        {
+            std::lock_guard lock(username_mutex);
+            if (member.nickname == nickname) {
+                username = member.username;
+            }
+        }
+    }
+    Invoke(room_information);
+}
+
+void RoomMember::RoomMemberImpl::HandleJoinPacket(const ENetEvent* event) {
+    Packet packet;
+    packet.Append(event->packet->data, event->packet->dataLength);
+
+    // Ignore the first byte, which is the message id.
+    packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
+
+    // Parse the MAC Address from the packet
+    packet >> mac_address;
+}
+
+void RoomMember::RoomMemberImpl::HandleWifiPackets(const ENetEvent* event) {
+    WifiPacket wifi_packet{};
+    Packet packet;
+    packet.Append(event->packet->data, event->packet->dataLength);
+
+    // Ignore the first byte, which is the message id.
+    packet.IgnoreBytes(sizeof(u8)); // Ignore the message type
+
+    // Parse the WifiPacket from the packet
+    u8 frame_type;
+    packet >> frame_type;
+    WifiPacket::PacketType type = static_cast<WifiPacket::PacketType>(frame_type);
+
+    wifi_packet.type = type;
+    packet >> wifi_packet.channel;
+    packet >> wifi_packet.transmitter_address;
+    packet >> wifi_packet.destination_address;
+    packet >> wifi_packet.data;
+
+    Invoke<WifiPacket>(wifi_packet);
+}
+
+void RoomMember::RoomMemberImpl::HandleChatPacket(const ENetEvent* event) {
+    Packet packet;
+    packet.Append(event->packet->data, event->packet->dataLength);
+
+    // Ignore the first byte, which is the message id.
+    packet.IgnoreBytes(sizeof(u8));
+
+    ChatEntry chat_entry{};
+    packet >> chat_entry.nickname;
+    packet >> chat_entry.username;
+    packet >> chat_entry.message;
+    Invoke<ChatEntry>(chat_entry);
+}
+
+void RoomMember::RoomMemberImpl::HandleStatusMessagePacket(const ENetEvent* event) {
+    Packet packet;
+    packet.Append(event->packet->data, event->packet->dataLength);
+
+    // Ignore the first byte, which is the message id.
+    packet.IgnoreBytes(sizeof(u8));
+
+    StatusMessageEntry status_message_entry{};
+    u8 type{};
+    packet >> type;
+    status_message_entry.type = static_cast<StatusMessageTypes>(type);
+    packet >> status_message_entry.nickname;
+    packet >> status_message_entry.username;
+    Invoke<StatusMessageEntry>(status_message_entry);
+}
+
+void RoomMember::RoomMemberImpl::HandleModBanListResponsePacket(const ENetEvent* event) {
+    Packet packet;
+    packet.Append(event->packet->data, event->packet->dataLength);
+
+    // Ignore the first byte, which is the message id.
+    packet.IgnoreBytes(sizeof(u8));
+
+    Room::BanList ban_list = {};
+    packet >> ban_list.first;
+    packet >> ban_list.second;
+    Invoke<Room::BanList>(ban_list);
+}
+
+void RoomMember::RoomMemberImpl::Disconnect() {
+    member_information.clear();
+    room_information.member_slots = 0;
+    room_information.name.clear();
+
+    if (!server)
+        return;
+    enet_peer_disconnect(server, 0);
+
+    ENetEvent event;
+    while (enet_host_service(client, &event, ConnectionTimeoutMs) > 0) {
+        switch (event.type) {
+        case ENET_EVENT_TYPE_RECEIVE:
+            enet_packet_destroy(event.packet); // Ignore all incoming data
+            break;
+        case ENET_EVENT_TYPE_DISCONNECT:
+            server = nullptr;
+            return;
+        case ENET_EVENT_TYPE_NONE:
+        case ENET_EVENT_TYPE_CONNECT:
+            break;
+        }
+    }
+    // didn't disconnect gracefully force disconnect
+    enet_peer_reset(server);
+    server = nullptr;
+}
+
+template <>
+RoomMember::RoomMemberImpl::CallbackSet<WifiPacket>& RoomMember::RoomMemberImpl::Callbacks::Get() {
+    return callback_set_wifi_packet;
+}
+
+template <>
+RoomMember::RoomMemberImpl::CallbackSet<RoomMember::State>&
+RoomMember::RoomMemberImpl::Callbacks::Get() {
+    return callback_set_state;
+}
+
+template <>
+RoomMember::RoomMemberImpl::CallbackSet<RoomMember::Error>&
+RoomMember::RoomMemberImpl::Callbacks::Get() {
+    return callback_set_error;
+}
+
+template <>
+RoomMember::RoomMemberImpl::CallbackSet<RoomInformation>&
+RoomMember::RoomMemberImpl::Callbacks::Get() {
+    return callback_set_room_information;
+}
+
+template <>
+RoomMember::RoomMemberImpl::CallbackSet<ChatEntry>& RoomMember::RoomMemberImpl::Callbacks::Get() {
+    return callback_set_chat_messages;
+}
+
+template <>
+RoomMember::RoomMemberImpl::CallbackSet<StatusMessageEntry>&
+RoomMember::RoomMemberImpl::Callbacks::Get() {
+    return callback_set_status_messages;
+}
+
+template <>
+RoomMember::RoomMemberImpl::CallbackSet<Room::BanList>&
+RoomMember::RoomMemberImpl::Callbacks::Get() {
+    return callback_set_ban_list;
+}
+
+template <typename T>
+void RoomMember::RoomMemberImpl::Invoke(const T& data) {
+    std::lock_guard lock(callback_mutex);
+    CallbackSet<T> callback_set = callbacks.Get<T>();
+    for (auto const& callback : callback_set)
+        (*callback)(data);
+}
+
+template <typename T>
+RoomMember::CallbackHandle<T> RoomMember::RoomMemberImpl::Bind(
+    std::function<void(const T&)> callback) {
+    std::lock_guard lock(callback_mutex);
+    CallbackHandle<T> handle;
+    handle = std::make_shared<std::function<void(const T&)>>(callback);
+    callbacks.Get<T>().insert(handle);
+    return handle;
+}
+
+// RoomMember
+RoomMember::RoomMember() : room_member_impl{std::make_unique<RoomMemberImpl>()} {}
+
+RoomMember::~RoomMember() {
+    ASSERT_MSG(!IsConnected(), "RoomMember is being destroyed while connected");
+    if (room_member_impl->loop_thread) {
+        Leave();
+    }
+}
+
+RoomMember::State RoomMember::GetState() const {
+    return room_member_impl->state;
+}
+
+const RoomMember::MemberList& RoomMember::GetMemberInformation() const {
+    return room_member_impl->member_information;
+}
+
+const std::string& RoomMember::GetNickname() const {
+    return room_member_impl->nickname;
+}
+
+const std::string& RoomMember::GetUsername() const {
+    std::lock_guard lock(room_member_impl->username_mutex);
+    return room_member_impl->username;
+}
+
+const MacAddress& RoomMember::GetMacAddress() const {
+    ASSERT_MSG(IsConnected(), "Tried to get MAC address while not connected");
+    return room_member_impl->mac_address;
+}
+
+RoomInformation RoomMember::GetRoomInformation() const {
+    return room_member_impl->room_information;
+}
+
+void RoomMember::Join(const std::string& nick, const std::string& console_id_hash,
+                      const char* server_addr, u16 server_port, u16 client_port,
+                      const MacAddress& preferred_mac, const std::string& password,
+                      const std::string& token) {
+    // If the member is connected, kill the connection first
+    if (room_member_impl->loop_thread && room_member_impl->loop_thread->joinable()) {
+        Leave();
+    }
+    // If the thread isn't running but the ptr still exists, reset it
+    else if (room_member_impl->loop_thread) {
+        room_member_impl->loop_thread.reset();
+    }
+
+    if (!room_member_impl->client) {
+        room_member_impl->client = enet_host_create(nullptr, 1, NumChannels, 0, 0);
+        ASSERT_MSG(room_member_impl->client != nullptr, "Could not create client");
+    }
+
+    room_member_impl->SetState(State::Joining);
+
+    ENetAddress address{};
+    enet_address_set_host(&address, server_addr);
+    address.port = server_port;
+    room_member_impl->server =
+        enet_host_connect(room_member_impl->client, &address, NumChannels, 0);
+
+    if (!room_member_impl->server) {
+        room_member_impl->SetState(State::Idle);
+        room_member_impl->SetError(Error::UnknownError);
+        return;
+    }
+
+    ENetEvent event{};
+    int net = enet_host_service(room_member_impl->client, &event, ConnectionTimeoutMs);
+    if (net > 0 && event.type == ENET_EVENT_TYPE_CONNECT) {
+        room_member_impl->nickname = nick;
+        room_member_impl->StartLoop();
+        room_member_impl->SendJoinRequest(nick, console_id_hash, preferred_mac, password, token);
+        SendGameInfo(room_member_impl->current_game_info);
+    } else {
+        enet_peer_disconnect(room_member_impl->server, 0);
+        room_member_impl->SetState(State::Idle);
+        room_member_impl->SetError(Error::CouldNotConnect);
+    }
+}
+
+bool RoomMember::IsConnected() const {
+    return room_member_impl->IsConnected();
+}
+
+void RoomMember::SendWifiPacket(const WifiPacket& wifi_packet) {
+    Packet packet;
+    packet << static_cast<u8>(IdWifiPacket);
+    packet << static_cast<u8>(wifi_packet.type);
+    packet << wifi_packet.channel;
+    packet << wifi_packet.transmitter_address;
+    packet << wifi_packet.destination_address;
+    packet << wifi_packet.data;
+    room_member_impl->Send(std::move(packet));
+}
+
+void RoomMember::SendChatMessage(const std::string& message) {
+    Packet packet;
+    packet << static_cast<u8>(IdChatMessage);
+    packet << message;
+    room_member_impl->Send(std::move(packet));
+}
+
+void RoomMember::SendGameInfo(const GameInfo& game_info) {
+    room_member_impl->current_game_info = game_info;
+    if (!IsConnected())
+        return;
+
+    Packet packet;
+    packet << static_cast<u8>(IdSetGameInfo);
+    packet << game_info.name;
+    packet << game_info.id;
+    room_member_impl->Send(std::move(packet));
+}
+
+void RoomMember::SendModerationRequest(RoomMessageTypes type, const std::string& nickname) {
+    ASSERT_MSG(type == IdModKick || type == IdModBan || type == IdModUnban,
+               "type is not a moderation request");
+    if (!IsConnected())
+        return;
+
+    Packet packet;
+    packet << static_cast<u8>(type);
+    packet << nickname;
+    room_member_impl->Send(std::move(packet));
+}
+
+void RoomMember::RequestBanList() {
+    if (!IsConnected())
+        return;
+
+    Packet packet;
+    packet << static_cast<u8>(IdModGetBanList);
+    room_member_impl->Send(std::move(packet));
+}
+
+RoomMember::CallbackHandle<RoomMember::State> RoomMember::BindOnStateChanged(
+    std::function<void(const RoomMember::State&)> callback) {
+    return room_member_impl->Bind(callback);
+}
+
+RoomMember::CallbackHandle<RoomMember::Error> RoomMember::BindOnError(
+    std::function<void(const RoomMember::Error&)> callback) {
+    return room_member_impl->Bind(callback);
+}
+
+RoomMember::CallbackHandle<WifiPacket> RoomMember::BindOnWifiPacketReceived(
+    std::function<void(const WifiPacket&)> callback) {
+    return room_member_impl->Bind(callback);
+}
+
+RoomMember::CallbackHandle<RoomInformation> RoomMember::BindOnRoomInformationChanged(
+    std::function<void(const RoomInformation&)> callback) {
+    return room_member_impl->Bind(callback);
+}
+
+RoomMember::CallbackHandle<ChatEntry> RoomMember::BindOnChatMessageRecieved(
+    std::function<void(const ChatEntry&)> callback) {
+    return room_member_impl->Bind(callback);
+}
+
+RoomMember::CallbackHandle<StatusMessageEntry> RoomMember::BindOnStatusMessageReceived(
+    std::function<void(const StatusMessageEntry&)> callback) {
+    return room_member_impl->Bind(callback);
+}
+
+RoomMember::CallbackHandle<Room::BanList> RoomMember::BindOnBanListReceived(
+    std::function<void(const Room::BanList&)> callback) {
+    return room_member_impl->Bind(callback);
+}
+
+template <typename T>
+void RoomMember::Unbind(CallbackHandle<T> handle) {
+    std::lock_guard lock(room_member_impl->callback_mutex);
+    room_member_impl->callbacks.Get<T>().erase(handle);
+}
+
+void RoomMember::Leave() {
+    room_member_impl->SetState(State::Idle);
+    room_member_impl->loop_thread->join();
+    room_member_impl->loop_thread.reset();
+
+    enet_host_destroy(room_member_impl->client);
+    room_member_impl->client = nullptr;
+}
+
+template void RoomMember::Unbind(CallbackHandle<WifiPacket>);
+template void RoomMember::Unbind(CallbackHandle<RoomMember::State>);
+template void RoomMember::Unbind(CallbackHandle<RoomMember::Error>);
+template void RoomMember::Unbind(CallbackHandle<RoomInformation>);
+template void RoomMember::Unbind(CallbackHandle<ChatEntry>);
+template void RoomMember::Unbind(CallbackHandle<StatusMessageEntry>);
+template void RoomMember::Unbind(CallbackHandle<Room::BanList>);
+
+} // namespace Network
diff --git a/src/network/room_member.h b/src/network/room_member.h
new file mode 100644
index 000000000..ee1c921d4
--- /dev/null
+++ b/src/network/room_member.h
@@ -0,0 +1,327 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <vector>
+#include <boost/serialization/vector.hpp>
+#include "common/common_types.h"
+#include "network/room.h"
+
+namespace Network {
+
+/// Information about the received WiFi packets.
+/// Acts as our own 802.11 header.
+struct WifiPacket {
+    enum class PacketType : u8 {
+        Beacon,
+        Data,
+        Authentication,
+        AssociationResponse,
+        Deauthentication,
+        NodeMap
+    };
+    PacketType type;      ///< The type of 802.11 frame.
+    std::vector<u8> data; ///< Raw 802.11 frame data, starting at the management frame header
+                          /// for management frames.
+    MacAddress transmitter_address; ///< Mac address of the transmitter.
+    MacAddress destination_address; ///< Mac address of the receiver.
+    u8 channel;                     ///< WiFi channel where this frame was transmitted.
+
+private:
+    template <class Archive>
+    void serialize(Archive& ar, const unsigned int) {
+        ar& type;
+        ar& data;
+        ar& transmitter_address;
+        ar& destination_address;
+        ar& channel;
+    }
+    friend class boost::serialization::access;
+};
+
+/// Represents a chat message.
+struct ChatEntry {
+    std::string nickname; ///< Nickname of the client who sent this message.
+    /// Web services username of the client who sent this message, can be empty.
+    std::string username;
+    std::string message; ///< Body of the message.
+};
+
+/// Represents a system status message.
+struct StatusMessageEntry {
+    StatusMessageTypes type; ///< Type of the message
+    /// Subject of the message. i.e. the user who is joining/leaving/being banned, etc.
+    std::string nickname;
+    std::string username;
+};
+
+/**
+ * This is what a client [person joining a server] would use.
+ * It also has to be used if you host a game yourself (You'd create both, a Room and a
+ * RoomMembership for yourself)
+ */
+class RoomMember final {
+public:
+    enum class State : u8 {
+        Uninitialized, ///< Not initialized
+        Idle,          ///< Default state (i.e. not connected)
+        Joining,       ///< The client is attempting to join a room.
+        Joined,    ///< The client is connected to the room and is ready to send/receive packets.
+        Moderator, ///< The client is connnected to the room and is granted mod permissions.
+    };
+
+    enum class Error : u8 {
+        // Reasons why connection was closed
+        LostConnection, ///< Connection closed
+        HostKicked,     ///< Kicked by the host
+
+        // Reasons why connection was rejected
+        UnknownError,       ///< Some error [permissions to network device missing or something]
+        NameCollision,      ///< Somebody is already using this name
+        MacCollision,       ///< Somebody is already using that mac-address
+        ConsoleIdCollision, ///< Somebody in the room has the same Console ID
+        WrongVersion,       ///< The room version is not the same as for this RoomMember
+        WrongPassword,      ///< The password doesn't match the one from the Room
+        CouldNotConnect,    ///< The room is not responding to a connection attempt
+        RoomIsFull,         ///< Room is already at the maximum number of players
+        HostBanned,         ///< The user is banned by the host
+
+        // Reasons why moderation request failed
+        PermissionDenied, ///< The user does not have mod permissions
+        NoSuchUser,       ///< The nickname the user attempts to kick/ban does not exist
+    };
+
+    struct MemberInformation {
+        std::string nickname;     ///< Nickname of the member.
+        std::string username;     ///< The web services username of the member. Can be empty.
+        std::string display_name; ///< The web services display name of the member. Can be empty.
+        std::string avatar_url;   ///< Url to the member's avatar. Can be empty.
+        GameInfo game_info;     ///< Name of the game they're currently playing, or empty if they're
+                                /// not playing anything.
+        MacAddress mac_address; ///< MAC address associated with this member.
+    };
+    using MemberList = std::vector<MemberInformation>;
+
+    // The handle for the callback functions
+    template <typename T>
+    using CallbackHandle = std::shared_ptr<std::function<void(const T&)>>;
+
+    /**
+     * Unbinds a callback function from the events.
+     * @param handle The connection handle to disconnect
+     */
+    template <typename T>
+    void Unbind(CallbackHandle<T> handle);
+
+    RoomMember();
+    ~RoomMember();
+
+    /**
+     * Returns the status of our connection to the room.
+     */
+    State GetState() const;
+
+    /**
+     * Returns information about the members in the room we're currently connected to.
+     */
+    const MemberList& GetMemberInformation() const;
+
+    /**
+     * Returns the nickname of the RoomMember.
+     */
+    const std::string& GetNickname() const;
+
+    /**
+     * Returns the username of the RoomMember.
+     */
+    const std::string& GetUsername() const;
+
+    /**
+     * Returns the MAC address of the RoomMember.
+     */
+    const MacAddress& GetMacAddress() const;
+
+    /**
+     * Returns information about the room we're currently connected to.
+     */
+    RoomInformation GetRoomInformation() const;
+
+    /**
+     * Returns whether we're connected to a server or not.
+     */
+    bool IsConnected() const;
+
+    /**
+     * Attempts to join a room at the specified address and port, using the specified nickname.
+     * A console ID hash is passed in to check console ID conflicts.
+     * This may fail if the username or console ID is already taken.
+     */
+    void Join(const std::string& nickname, const std::string& console_id_hash,
+              const char* server_addr = "127.0.0.1", u16 server_port = DefaultRoomPort,
+              u16 client_port = 0, const MacAddress& preferred_mac = NoPreferredMac,
+              const std::string& password = "", const std::string& token = "");
+
+    /**
+     * Sends a WiFi packet to the room.
+     * @param packet The WiFi packet to send.
+     */
+    void SendWifiPacket(const WifiPacket& packet);
+
+    /**
+     * Sends a chat message to the room.
+     * @param message The contents of the message.
+     */
+    void SendChatMessage(const std::string& message);
+
+    /**
+     * Sends the current game info to the room.
+     * @param game_info The game information.
+     */
+    void SendGameInfo(const GameInfo& game_info);
+
+    /**
+     * Sends a moderation request to the room.
+     * @param type Moderation request type.
+     * @param nickname The subject of the request. (i.e. the user you want to kick/ban)
+     */
+    void SendModerationRequest(RoomMessageTypes type, const std::string& nickname);
+
+    /**
+     * Attempts to retrieve ban list from the room.
+     * If success, the ban list callback would be called. Otherwise an error would be emitted.
+     */
+    void RequestBanList();
+
+    /**
+     * Binds a function to an event that will be triggered every time the State of the member
+     * changed. The function wil be called every time the event is triggered. The callback function
+     * must not bind or unbind a function. Doing so will cause a deadlock
+     * @param callback The function to call
+     * @return A handle used for removing the function from the registered list
+     */
+    CallbackHandle<State> BindOnStateChanged(std::function<void(const State&)> callback);
+
+    /**
+     * Binds a function to an event that will be triggered every time an error happened. The
+     * function wil be called every time the event is triggered. The callback function must not bind
+     * or unbind a function. Doing so will cause a deadlock
+     * @param callback The function to call
+     * @return A handle used for removing the function from the registered list
+     */
+    CallbackHandle<Error> BindOnError(std::function<void(const Error&)> callback);
+
+    /**
+     * Binds a function to an event that will be triggered every time a WifiPacket is received.
+     * The function wil be called everytime the event is triggered.
+     * The callback function must not bind or unbind a function. Doing so will cause a deadlock
+     * @param callback The function to call
+     * @return A handle used for removing the function from the registered list
+     */
+    CallbackHandle<WifiPacket> BindOnWifiPacketReceived(
+        std::function<void(const WifiPacket&)> callback);
+
+    /**
+     * Binds a function to an event that will be triggered every time the RoomInformation changes.
+     * The function wil be called every time the event is triggered.
+     * The callback function must not bind or unbind a function. Doing so will cause a deadlock
+     * @param callback The function to call
+     * @return A handle used for removing the function from the registered list
+     */
+    CallbackHandle<RoomInformation> BindOnRoomInformationChanged(
+        std::function<void(const RoomInformation&)> callback);
+
+    /**
+     * Binds a function to an event that will be triggered every time a ChatMessage is received.
+     * The function wil be called every time the event is triggered.
+     * The callback function must not bind or unbind a function. Doing so will cause a deadlock
+     * @param callback The function to call
+     * @return A handle used for removing the function from the registered list
+     */
+    CallbackHandle<ChatEntry> BindOnChatMessageRecieved(
+        std::function<void(const ChatEntry&)> callback);
+
+    /**
+     * Binds a function to an event that will be triggered every time a StatusMessage is
+     * received. The function will be called every time the event is triggered. The callback
+     * function must not bind or unbind a function. Doing so will cause a deadlock
+     * @param callback The function to call
+     * @return A handle used for removing the function from the registered list
+     */
+    CallbackHandle<StatusMessageEntry> BindOnStatusMessageReceived(
+        std::function<void(const StatusMessageEntry&)> callback);
+
+    /**
+     * Binds a function to an event that will be triggered every time a requested ban list
+     * received. The function will be called every time the event is triggered. The callback
+     * function must not bind or unbind a function. Doing so will cause a deadlock
+     * @param callback The function to call
+     * @return A handle used for removing the function from the registered list
+     */
+    CallbackHandle<Room::BanList> BindOnBanListReceived(
+        std::function<void(const Room::BanList&)> callback);
+
+    /**
+     * Leaves the current room.
+     */
+    void Leave();
+
+private:
+    class RoomMemberImpl;
+    std::unique_ptr<RoomMemberImpl> room_member_impl;
+};
+
+inline const char* GetStateStr(const RoomMember::State& s) {
+    switch (s) {
+    case RoomMember::State::Uninitialized:
+        return "Uninitialized";
+    case RoomMember::State::Idle:
+        return "Idle";
+    case RoomMember::State::Joining:
+        return "Joining";
+    case RoomMember::State::Joined:
+        return "Joined";
+    case RoomMember::State::Moderator:
+        return "Moderator";
+    }
+    return "Unknown";
+}
+
+inline const char* GetErrorStr(const RoomMember::Error& e) {
+    switch (e) {
+    case RoomMember::Error::LostConnection:
+        return "LostConnection";
+    case RoomMember::Error::HostKicked:
+        return "HostKicked";
+    case RoomMember::Error::UnknownError:
+        return "UnknownError";
+    case RoomMember::Error::NameCollision:
+        return "NameCollision";
+    case RoomMember::Error::MacCollision:
+        return "MaxCollision";
+    case RoomMember::Error::ConsoleIdCollision:
+        return "ConsoleIdCollision";
+    case RoomMember::Error::WrongVersion:
+        return "WrongVersion";
+    case RoomMember::Error::WrongPassword:
+        return "WrongPassword";
+    case RoomMember::Error::CouldNotConnect:
+        return "CouldNotConnect";
+    case RoomMember::Error::RoomIsFull:
+        return "RoomIsFull";
+    case RoomMember::Error::HostBanned:
+        return "HostBanned";
+    case RoomMember::Error::PermissionDenied:
+        return "PermissionDenied";
+    case RoomMember::Error::NoSuchUser:
+        return "NoSuchUser";
+    default:
+        return "Unknown";
+    }
+}
+
+} // namespace Network
diff --git a/src/network/verify_user.cpp b/src/network/verify_user.cpp
new file mode 100644
index 000000000..d9d98e495
--- /dev/null
+++ b/src/network/verify_user.cpp
@@ -0,0 +1,18 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "network/verify_user.h"
+
+namespace Network::VerifyUser {
+
+Backend::~Backend() = default;
+
+NullBackend::~NullBackend() = default;
+
+UserData NullBackend::LoadUserData([[maybe_unused]] const std::string& verify_UID,
+                                   [[maybe_unused]] const std::string& token) {
+    return {};
+}
+
+} // namespace Network::VerifyUser
diff --git a/src/network/verify_user.h b/src/network/verify_user.h
new file mode 100644
index 000000000..01b9877c8
--- /dev/null
+++ b/src/network/verify_user.h
@@ -0,0 +1,46 @@
+// Copyright 2018 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <string>
+#include "common/logging/log.h"
+
+namespace Network::VerifyUser {
+
+struct UserData {
+    std::string username;
+    std::string display_name;
+    std::string avatar_url;
+    bool moderator = false; ///< Whether the user is a Citra Moderator.
+};
+
+/**
+ * A backend used for verifying users and loading user data.
+ */
+class Backend {
+public:
+    virtual ~Backend();
+
+    /**
+     * Verifies the given token and loads the information into a UserData struct.
+     * @param verify_UID A GUID that may be used for verification.
+     * @param token A token that contains user data and verification data. The format and content is
+     * decided by backends.
+     */
+    virtual UserData LoadUserData(const std::string& verify_UID, const std::string& token) = 0;
+};
+
+/**
+ * A null backend where the token is ignored.
+ * No verification is performed here and the function returns an empty UserData.
+ */
+class NullBackend final : public Backend {
+public:
+    ~NullBackend();
+
+    UserData LoadUserData(const std::string& verify_UID, const std::string& token) override;
+};
+
+} // namespace Network::VerifyUser