diff --git a/src/lobby.rs b/src/lobby.rs
index b2c97a1..241c159 100644
--- a/src/lobby.rs
+++ b/src/lobby.rs
@@ -588,11 +588,15 @@ async fn connect_to_server_no_timeout(
         .await
         .map_err(|_| ())?;
 
-    let (mut reader, mut writer) = outbound.split();
-
-    let tmp_client = Client::default();
+    // Construct temporary server client
+    let tmp_client = match outbound.local_addr() {
+        Ok(addr) => Client::new(addr),
+        Err(_) => Client::dummy(),
+    };
     tmp_client.set_state(ClientState::Login);
 
+    let (mut reader, mut writer) = outbound.split();
+
     // Handshake packet
     packet::write_packet(
         Handshake {
diff --git a/src/mc/ban.rs b/src/mc/ban.rs
index bae461b..cbfeb48 100644
--- a/src/mc/ban.rs
+++ b/src/mc/ban.rs
@@ -17,6 +17,14 @@ pub struct BannedIps {
 }
 
 impl BannedIps {
+    /// Get ban entry if IP if it exists.
+    ///
+    /// This uses the latest known `banned-ips.json` contents if known.
+    /// If this feature is disabled, this will always return false.
+    pub fn get(&self, ip: &IpAddr) -> Option<BannedIp> {
+        self.ips.get(ip).cloned()
+    }
+
     /// Check whether the given IP is banned.
     ///
     /// This uses the latest known `banned-ips.json` contents if known.
@@ -27,7 +35,7 @@ impl BannedIps {
 }
 
 /// A banned IP entry.
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Deserialize, Clone)]
 pub struct BannedIp {
     /// Banned IP.
     pub ip: IpAddr,
diff --git a/src/monitor.rs b/src/monitor.rs
index 39dd270..85f5ac3 100644
--- a/src/monitor.rs
+++ b/src/monitor.rs
@@ -98,7 +98,7 @@ async fn fetch_status(config: &Config, addr: SocketAddr) -> Result<ServerStatus,
     let mut stream = TcpStream::connect(addr).await.map_err(|_| ())?;
 
     // Dummy client
-    let client = Client::default();
+    let client = Client::dummy();
 
     send_handshake(&client, &mut stream, config, addr).await?;
     request_status(&client, &mut stream).await?;
@@ -110,7 +110,7 @@ async fn do_ping(config: &Config, addr: SocketAddr) -> Result<(), ()> {
     let mut stream = TcpStream::connect(addr).await.map_err(|_| ())?;
 
     // Dummy client
-    let client = Client::default();
+    let client = Client::dummy();
 
     send_handshake(&client, &mut stream, config, addr).await?;
     let token = send_ping(&client, &mut stream).await?;
diff --git a/src/proto/client.rs b/src/proto/client.rs
index 584696b..4d82eb2 100644
--- a/src/proto/client.rs
+++ b/src/proto/client.rs
@@ -1,3 +1,4 @@
+use std::net::SocketAddr;
 use std::sync::atomic::{AtomicI32, Ordering};
 use std::sync::Mutex;
 
@@ -6,6 +7,9 @@ use std::sync::Mutex;
 /// Note: this does not keep track of encryption states.
 #[derive(Debug)]
 pub struct Client {
+    /// Client peer address.
+    pub peer: SocketAddr,
+
     /// Current client state.
     pub state: Mutex<ClientState>,
 
@@ -16,6 +20,20 @@ pub struct Client {
 }
 
 impl Client {
+    /// Construct new client with given peer address.
+    pub fn new(peer: SocketAddr) -> Self {
+        Self {
+            peer,
+            state: Default::default(),
+            compression: AtomicI32::new(-1),
+        }
+    }
+
+    /// Construct dummy client.
+    pub fn dummy() -> Self {
+        Self::new("0.0.0.0:0".parse().unwrap())
+    }
+
     /// Get client state.
     pub fn state(&self) -> ClientState {
         *self.state.lock().unwrap()
@@ -44,15 +62,6 @@ impl Client {
     }
 }
 
-impl Default for Client {
-    fn default() -> Self {
-        Self {
-            state: Default::default(),
-            compression: AtomicI32::new(-1),
-        }
-    }
-}
-
 /// Protocol state a client may be in.
 ///
 /// Note: this does not include the `play` state, because this is never used anymore when a client
diff --git a/src/server.rs b/src/server.rs
index af7b90c..842feb1 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -13,7 +13,7 @@ use tokio::sync::{Mutex, RwLock, RwLockReadGuard};
 use tokio::time;
 
 use crate::config::Config;
-use crate::mc::ban::BannedIps;
+use crate::mc::ban::{BannedIp, BannedIps};
 use crate::os;
 
 /// Server cooldown after the process quit.
@@ -320,6 +320,11 @@ impl Server {
         self.banned_ips.read().await.is_banned(ip)
     }
 
+    /// Get user ban entry.
+    pub async fn ban_entry(&self, ip: &IpAddr) -> Option<BannedIp> {
+        self.banned_ips.read().await.get(ip)
+    }
+
     /// Check whether the given IP is banned.
     ///
     /// This uses the latest known `banned-ips.json` contents if known.
diff --git a/src/service/server.rs b/src/service/server.rs
index 7d341f5..a810cda 100644
--- a/src/service/server.rs
+++ b/src/service/server.rs
@@ -70,48 +70,33 @@ pub async fn service(config: Arc<Config>) -> Result<(), ()> {
 /// Route inbound TCP stream to correct service, spawning a new task.
 #[inline]
 fn route(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
-    // Check ban
-    if !check_ban(&inbound, &server) {
-        return;
-    }
-
-    // Route connection through proper channel
-    let should_proxy = server.state() == server::State::Started && !config.lockout.enabled;
-    if should_proxy {
-        route_proxy(inbound, config)
-    } else {
-        route_status(inbound, config, server)
-    }
-}
-
-/// Check whether user IP is banned.
-///
-/// Returns `true` if user is still allowed to connect.
-fn check_ban(inbound: &TcpStream, server: &Server) -> bool {
     // Get user peer address
     let peer = match inbound.peer_addr() {
         Ok(peer) => peer,
         Err(err) => {
-            warn!(target: "lazymc", "Connection from unknown peer, disconnecting: {}", err);
-            return false;
+            warn!(target: "lazymc", "Connection from unknown peer address, disconnecting: {}", err);
+            return;
         }
     };
 
-    // Check if user is banned
-    let is_banned = server.is_banned_ip_blocking(&peer.ip());
-    if is_banned {
-        warn!(target: "lazymc", "Connection from banned IP {}, disconnecting", peer);
-        return false;
-    }
+    // Check ban state
+    let banned = server.is_banned_ip_blocking(&peer.ip());
 
-    true
+    // Route connection through proper channel
+    let should_proxy =
+        !banned && server.state() == server::State::Started && !config.lockout.enabled;
+    if should_proxy {
+        route_proxy(inbound, config)
+    } else {
+        route_status(inbound, config, server, peer)
+    }
 }
 
 /// Route inbound TCP stream to status server, spawning a new task.
 #[inline]
-fn route_status(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>) {
+fn route_status(inbound: TcpStream, config: Arc<Config>, server: Arc<Server>, peer: SocketAddr) {
     // When server is not online, spawn a status server
-    let client = Client::default();
+    let client = Client::new(peer);
     let service = status::serve(client, inbound, config, server).map(|r| {
         if let Err(err) = r {
             warn!(target: "lazymc", "Failed to serve status: {:?}", err);
diff --git a/src/status.rs b/src/status.rs
index ceaf07d..b9e3cac 100644
--- a/src/status.rs
+++ b/src/status.rs
@@ -127,6 +127,15 @@ pub async fn serve(
                 break;
             }
 
+            // Kick if client is banned
+            if let Some(ban) = server.ban_entry(&client.peer.ip()).await {
+                if ban.is_banned() {
+                    warn!(target: "lazymc", "Login from banned IP {} ({}), disconnecting", client.peer.ip(), &ban.reason);
+                    action::kick(&client, &ban.reason, &mut writer).await?;
+                    break;
+                }
+            }
+
             // Start server if not starting yet
             Server::start(config.clone(), server.clone(), username).await;