diff --git a/src/network/room.cpp b/src/network/room.cpp
index ee62df220..394e8644f 100644
--- a/src/network/room.cpp
+++ b/src/network/room.cpp
@@ -48,6 +48,10 @@ public:
     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()
         : random_gen(std::random_device()()), NintendoOUI{0x00, 0x1F, 0x32, 0x00, 0x00, 0x00} {}
 
@@ -68,6 +72,30 @@ public:
      */
     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.
      */
@@ -85,6 +113,11 @@ public:
      */
     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.
      */
@@ -122,6 +155,32 @@ public:
      */
     void SendJoinSuccess(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,
      */
@@ -202,6 +261,19 @@ void Room::RoomImpl::ServerLoop() {
                 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;
@@ -296,6 +368,29 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) {
     }
     member.user_data = verify_backend->LoadUserData(uid, token);
 
+    {
+        std::lock_guard<std::mutex> 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);
+        std::string 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);
 
@@ -309,6 +404,153 @@ void Room::RoomImpl::HandleJoinRequest(const ENetEvent* event) {
     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;
+    {
+        std::lock_guard<std::mutex> 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;
+
+        enet_peer_disconnect(target_member->peer, 0);
+        members.erase(target_member);
+    }
+
+    // Announce the change to all clients.
+    SendStatusMessage(IdMemberKicked, nickname, username);
+    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;
+    std::string ip;
+
+    {
+        std::lock_guard<std::mutex> 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<std::mutex> 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);
+    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<std::mutex> 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.
@@ -336,6 +578,22 @@ bool Room::RoomImpl::IsValidConsoleId(const std::string& console_id_hash) const
     });
 }
 
+bool Room::RoomImpl::HasModPermission(const ENetPeer* client) const {
+    if (room_information.host_username.empty())
+        return false; // This room does not support moderation
+    std::lock_guard<std::mutex> 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 (sending_member->user_data.username != room_information.host_username) {
+        return false;
+    }
+    return true;
+}
+
 void Room::RoomImpl::SendNameCollision(ENetPeer* client) {
     Packet packet;
     packet << static_cast<u8>(IdNameCollision);
@@ -407,6 +665,61 @@ void Room::RoomImpl::SendJoinSuccess(ENetPeer* client, MacAddress mac_address) {
     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<std::mutex> 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);
@@ -450,6 +763,7 @@ void Room::RoomImpl::BroadcastRoomInformation() {
     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());
     {
@@ -625,8 +939,10 @@ 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& preferred_game,
-                  u64 preferred_game_id, std::unique_ptr<VerifyUser::Backend> verify_backend) {
+                  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) {
     ENetAddress address;
     address.host = ENET_HOST_ANY;
     if (!server_address.empty()) {
@@ -648,8 +964,11 @@ bool Room::Create(const std::string& name, const std::string& description,
     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->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;
@@ -668,6 +987,11 @@ std::string Room::GetVerifyUID() const {
     return room_impl->verify_UID;
 }
 
+Room::BanList Room::GetBanList() const {
+    std::lock_guard<std::mutex> 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<std::mutex> lock(room_impl->member_mutex);
diff --git a/src/network/room.h b/src/network/room.h
index a3d93eea9..3181e84d7 100644
--- a/src/network/room.h
+++ b/src/network/room.h
@@ -31,6 +31,7 @@ struct RoomInformation {
     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
 };
 
 struct GameInfo {
@@ -62,12 +63,26 @@ enum RoomMessageTypes : u8 {
     IdRoomIsFull,
     IdConsoleIdCollision,
     IdStatusMessage,
+    IdHostKicked,
+    IdHostBanned,
+    /// Moderation requests
+    IdModKick,
+    IdModBan,
+    IdModUnban,
+    IdModGetBanList,
+    // Moderation responses
+    IdModBanListResponse,
+    IdModPermissionDenied,
+    IdModNoSuchUser,
 };
 
 /// Types of system status messages
 enum StatusMessageTypes : u8 {
-    IdMemberJoin = 1, ///< Member joining
-    IdMemberLeave,    ///< Member leaving
+    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.
@@ -115,6 +130,11 @@ public:
      */
     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.
@@ -123,14 +143,21 @@ public:
                 const std::string& server = "", u16 server_port = DefaultRoomPort,
                 const std::string& password = "",
                 const u32 max_connections = MaxConcurrentConnections,
-                const std::string& preferred_game = "", u64 preferred_game_id = 0,
-                std::unique_ptr<VerifyUser::Backend> verify_backend = nullptr);
+                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 = {});
 
     /**
      * 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
      */