diff --git a/server/player/ban.go b/server/player/ban.go index cf914af..95447cc 100644 --- a/server/player/ban.go +++ b/server/player/ban.go @@ -21,7 +21,7 @@ type BanEntry struct { // BanManager handles player bans. type BanManager struct { - mu sync.Mutex + mu sync.RWMutex bans map[string]BanEntry path string } @@ -54,14 +54,15 @@ func (bm *BanManager) Load() error { return nil } -// Save saves the banned players to bans.json. -func (bm *BanManager) Save() error { - bm.mu.Lock() - defer bm.mu.Unlock() +// save writes bans to disk. Must be called WITHOUT holding bm.mu. +func (bm *BanManager) save() error { + bm.mu.RLock() var list []BanEntry for _, entry := range bm.bans { list = append(list, entry) } + bm.mu.RUnlock() + data, err := json.MarshalIndent(list, "", " ") if err != nil { return err @@ -69,23 +70,30 @@ func (bm *BanManager) Save() error { return os.WriteFile(bm.path, data, 0644) } +// Save saves the banned players to bans.json. +func (bm *BanManager) Save() error { + return bm.save() +} + // Ban bans a player by name, with optional XUID, UUID, and IP. +// IP bans are intentionally NOT stored to prevent blocking all +// players on the same local network (LAN/nethernet). func (bm *BanManager) Ban(name string, duration time.Duration, reason string, xuid string, uuidStr string, ip string) { bm.mu.Lock() entry := BanEntry{ - Name: name, - XUID: xuid, - UUID: uuidStr, - IP: ip, - BannedAt: time.Now(), - Reason: reason, + Name: name, + XUID: xuid, + UUID: uuidStr, + IP: "", // Do NOT store IP to prevent LAN-wide bans + BannedAt: time.Now(), + Reason: reason, } if duration > 0 { entry.ExpiresAt = time.Now().Add(duration) } bm.bans[strings.ToLower(name)] = entry bm.mu.Unlock() - _ = bm.Save() + _ = bm.save() } // Unban unbans a player by name. @@ -98,15 +106,28 @@ func (bm *BanManager) Unban(name string) bool { } bm.mu.Unlock() if exists { - _ = bm.Save() + _ = bm.save() } return exists } -// IsBanned checks if a player is banned by name, XUID, UUID, or IP. -func (bm *BanManager) IsBanned(name, xuid, uuidStr, ip string) (BanEntry, bool) { +// UnbanAll removes all bans. Useful for emergency reset. +func (bm *BanManager) UnbanAll() int { bm.mu.Lock() - defer bm.mu.Unlock() + count := len(bm.bans) + bm.bans = make(map[string]BanEntry) + bm.mu.Unlock() + if count > 0 { + _ = bm.save() + } + return count +} + +// IsBanned checks if a player is banned by name, XUID, or UUID. +// IP-based checking is intentionally excluded to prevent false +// positives on local networks where all players share the same IP. +func (bm *BanManager) IsBanned(name, xuid, uuidStr, ip string) (BanEntry, bool) { + bm.mu.RLock() now := time.Now() var expiredKeys []string @@ -129,18 +150,23 @@ func (bm *BanManager) IsBanned(name, xuid, uuidStr, ip string) (BanEntry, bool) } else if uuidStr != "" && entry.UUID != "" && strings.EqualFold(entry.UUID, uuidStr) { foundEntry = entry found = true - } else if ip != "" && entry.IP != "" && entry.IP == ip { - foundEntry = entry - found = true } + // NOTE: IP matching is intentionally removed. + // On local networks (LAN/nethernet), all players share the + // same IP address. Matching by IP would block ALL players + // from the same network when only one should be banned. } } + bm.mu.RUnlock() + // Clean up expired entries with a separate write lock. if len(expiredKeys) > 0 { + bm.mu.Lock() for _, k := range expiredKeys { delete(bm.bans, k) } - go func() { _ = bm.Save() }() + bm.mu.Unlock() + go func() { _ = bm.save() }() } return foundEntry, found @@ -148,8 +174,7 @@ func (bm *BanManager) IsBanned(name, xuid, uuidStr, ip string) (BanEntry, bool) // List returns all active bans. func (bm *BanManager) List() []BanEntry { - bm.mu.Lock() - defer bm.mu.Unlock() + bm.mu.RLock() var active []BanEntry now := time.Now() var expiredKeys []string @@ -160,11 +185,15 @@ func (bm *BanManager) List() []BanEntry { } active = append(active, entry) } + bm.mu.RUnlock() + if len(expiredKeys) > 0 { + bm.mu.Lock() for _, k := range expiredKeys { delete(bm.bans, k) } - go func() { _ = bm.Save() }() + bm.mu.Unlock() + go func() { _ = bm.save() }() } return active } diff --git a/server/player/player.go b/server/player/player.go index 5efaa20..c5d2097 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -533,6 +533,15 @@ func (p *Player) handleCheatCommand(name string, args []string) bool { } return true + case "unbanall": + count := Bans.UnbanAll() + if count > 0 { + p.Message(fmt.Sprintf("Successfully unbanned all %d players.", count)) + } else { + p.Message("No players were banned.") + } + return true + case "gamemode", "gm": if len(args) < 1 { p.Message("Usage: /gamemode [player]") diff --git a/server/server.go b/server/server.go index 33a4772..0b22e47 100644 --- a/server/server.go +++ b/server/server.go @@ -349,33 +349,64 @@ func (srv *Server) listen(l Listener) { wg.Add(1) go func() { defer wg.Done() + defer func() { + if r := recover(); r != nil { + srv.conf.Log.Error(fmt.Sprintf("panic handling connection: %v", r)) + _ = c.Close() + } + }() + name := c.IdentityData().DisplayName xuid := c.IdentityData().XUID uuidStr := c.IdentityData().Identity + + // Safely extract IP - nethernet/local connections may + // return nil RemoteAddr or non-standard address formats. ip := "" if addr := c.RemoteAddr(); addr != nil { - if host, _, err := net.SplitHostPort(addr.String()); err == nil { - ip = host - } else { - ip = addr.String() + addrStr := addr.String() + if addrStr != "" { + if host, _, err := net.SplitHostPort(addrStr); err == nil { + ip = host + } else { + ip = addrStr + } } } + + srv.conf.Log.Debug("Player connecting...", "name", name, "xuid", xuid, "ip", ip) + + // Check if player is banned (by name, XUID, or UUID only - not IP). if entry, banned := player.Bans.IsBanned(name, xuid, uuidStr, ip); banned { msg := "You are banned from this server." + if entry.Reason != "" { + msg += " Reason: " + entry.Reason + } if !entry.ExpiresAt.IsZero() { timeLeft := time.Until(entry.ExpiresAt).Round(time.Second) - msg = fmt.Sprintf("You are banned from this server. Remaining time: %v", timeLeft) + if timeLeft > 0 { + msg += fmt.Sprintf(" Remaining: %v", timeLeft) + } } + srv.conf.Log.Debug("Player banned, rejecting.", "name", name) _ = c.WritePacket(&packet.Disconnect{Message: msg}) _ = c.Close() return } - if srv.PlayerCount() >= 14 && !strings.EqualFold(name, "XTarnaWijaya") && !strings.EqualFold(name, "TarnaWijaya") { + + // Use the configured MaxPlayerCount instead of a hardcoded value. + // Admins bypass the player limit. + if srv.PlayerCount() >= srv.MaxPlayerCount() && + !strings.EqualFold(name, "XTarnaWijaya") && + !strings.EqualFold(name, "TarnaWijaya") { + srv.conf.Log.Debug("Server full, rejecting.", "name", name) _ = c.WritePacket(&packet.Disconnect{Message: "Server is full."}) _ = c.Close() return } + if msg, ok := srv.conf.Allower.Allow(c.RemoteAddr(), c.IdentityData(), c.ClientData()); !ok { + srv.conf.Log.Debug("Player not allowed.", "name", name, "msg", msg) _ = c.WritePacket(&packet.Disconnect{HideDisconnectionScreen: msg == "", Message: msg}) _ = c.Close() return @@ -469,12 +500,20 @@ func (srv *Server) finaliseConn(ctx context.Context, conn session.Conn, l Listen if err := conn.StartGameContext(ctx, data); err != nil { _ = l.Disconnect(conn, "Connection timeout.") - srv.conf.Log.Debug("spawn failed: "+err.Error(), "raddr", conn.RemoteAddr()) + raddr := "nethernet" + if addr := conn.RemoteAddr(); addr != nil { + raddr = addr.String() + } + srv.conf.Log.Debug("spawn failed: "+err.Error(), "raddr", raddr) return } if _, ok := srv.Player(id); ok { _ = l.Disconnect(conn, "Already logged in.") - srv.conf.Log.Debug("spawn failed: already logged in", "raddr", conn.RemoteAddr()) + raddr := "nethernet" + if addr := conn.RemoteAddr(); addr != nil { + raddr = addr.String() + } + srv.conf.Log.Debug("spawn failed: already logged in", "raddr", raddr) return } _ = conn.WritePacket(&packet.ItemRegistry{Items: srv.customItems}) diff --git a/server/session/session.go b/server/session/session.go index cdd1a69..a9081b7 100644 --- a/server/session/session.go +++ b/server/session/session.go @@ -176,7 +176,11 @@ func (conf Config) New(conn Conn) *Session { if conf.Log == nil { conf.Log = slog.Default() } - conf.Log = conf.Log.With("name", conn.IdentityData().DisplayName, "uuid", conn.IdentityData().Identity, "raddr", conn.RemoteAddr().String()) + raddr := "nethernet" + if addr := conn.RemoteAddr(); addr != nil { + raddr = addr.String() + } + conf.Log = conf.Log.With("name", conn.IdentityData().DisplayName, "uuid", conn.IdentityData().Identity, "raddr", raddr) s := &Session{} *s = Session{