fix: resolve nethernet connection, ban deadlock, and nil RemoteAddr crashes
This commit is contained in:
+48
-19
@@ -21,7 +21,7 @@ type BanEntry struct {
|
|||||||
|
|
||||||
// BanManager handles player bans.
|
// BanManager handles player bans.
|
||||||
type BanManager struct {
|
type BanManager struct {
|
||||||
mu sync.Mutex
|
mu sync.RWMutex
|
||||||
bans map[string]BanEntry
|
bans map[string]BanEntry
|
||||||
path string
|
path string
|
||||||
}
|
}
|
||||||
@@ -54,14 +54,15 @@ func (bm *BanManager) Load() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save saves the banned players to bans.json.
|
// save writes bans to disk. Must be called WITHOUT holding bm.mu.
|
||||||
func (bm *BanManager) Save() error {
|
func (bm *BanManager) save() error {
|
||||||
bm.mu.Lock()
|
bm.mu.RLock()
|
||||||
defer bm.mu.Unlock()
|
|
||||||
var list []BanEntry
|
var list []BanEntry
|
||||||
for _, entry := range bm.bans {
|
for _, entry := range bm.bans {
|
||||||
list = append(list, entry)
|
list = append(list, entry)
|
||||||
}
|
}
|
||||||
|
bm.mu.RUnlock()
|
||||||
|
|
||||||
data, err := json.MarshalIndent(list, "", " ")
|
data, err := json.MarshalIndent(list, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -69,14 +70,21 @@ func (bm *BanManager) Save() error {
|
|||||||
return os.WriteFile(bm.path, data, 0644)
|
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.
|
// 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) {
|
func (bm *BanManager) Ban(name string, duration time.Duration, reason string, xuid string, uuidStr string, ip string) {
|
||||||
bm.mu.Lock()
|
bm.mu.Lock()
|
||||||
entry := BanEntry{
|
entry := BanEntry{
|
||||||
Name: name,
|
Name: name,
|
||||||
XUID: xuid,
|
XUID: xuid,
|
||||||
UUID: uuidStr,
|
UUID: uuidStr,
|
||||||
IP: ip,
|
IP: "", // Do NOT store IP to prevent LAN-wide bans
|
||||||
BannedAt: time.Now(),
|
BannedAt: time.Now(),
|
||||||
Reason: reason,
|
Reason: reason,
|
||||||
}
|
}
|
||||||
@@ -85,7 +93,7 @@ func (bm *BanManager) Ban(name string, duration time.Duration, reason string, xu
|
|||||||
}
|
}
|
||||||
bm.bans[strings.ToLower(name)] = entry
|
bm.bans[strings.ToLower(name)] = entry
|
||||||
bm.mu.Unlock()
|
bm.mu.Unlock()
|
||||||
_ = bm.Save()
|
_ = bm.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unban unbans a player by name.
|
// Unban unbans a player by name.
|
||||||
@@ -98,15 +106,28 @@ func (bm *BanManager) Unban(name string) bool {
|
|||||||
}
|
}
|
||||||
bm.mu.Unlock()
|
bm.mu.Unlock()
|
||||||
if exists {
|
if exists {
|
||||||
_ = bm.Save()
|
_ = bm.save()
|
||||||
}
|
}
|
||||||
return exists
|
return exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsBanned checks if a player is banned by name, XUID, UUID, or IP.
|
// UnbanAll removes all bans. Useful for emergency reset.
|
||||||
func (bm *BanManager) IsBanned(name, xuid, uuidStr, ip string) (BanEntry, bool) {
|
func (bm *BanManager) UnbanAll() int {
|
||||||
bm.mu.Lock()
|
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()
|
now := time.Now()
|
||||||
var expiredKeys []string
|
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) {
|
} else if uuidStr != "" && entry.UUID != "" && strings.EqualFold(entry.UUID, uuidStr) {
|
||||||
foundEntry = entry
|
foundEntry = entry
|
||||||
found = true
|
found = true
|
||||||
} else if ip != "" && entry.IP != "" && entry.IP == ip {
|
}
|
||||||
foundEntry = entry
|
// NOTE: IP matching is intentionally removed.
|
||||||
found = true
|
// 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 {
|
if len(expiredKeys) > 0 {
|
||||||
|
bm.mu.Lock()
|
||||||
for _, k := range expiredKeys {
|
for _, k := range expiredKeys {
|
||||||
delete(bm.bans, k)
|
delete(bm.bans, k)
|
||||||
}
|
}
|
||||||
go func() { _ = bm.Save() }()
|
bm.mu.Unlock()
|
||||||
|
go func() { _ = bm.save() }()
|
||||||
}
|
}
|
||||||
|
|
||||||
return foundEntry, found
|
return foundEntry, found
|
||||||
@@ -148,8 +174,7 @@ func (bm *BanManager) IsBanned(name, xuid, uuidStr, ip string) (BanEntry, bool)
|
|||||||
|
|
||||||
// List returns all active bans.
|
// List returns all active bans.
|
||||||
func (bm *BanManager) List() []BanEntry {
|
func (bm *BanManager) List() []BanEntry {
|
||||||
bm.mu.Lock()
|
bm.mu.RLock()
|
||||||
defer bm.mu.Unlock()
|
|
||||||
var active []BanEntry
|
var active []BanEntry
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
var expiredKeys []string
|
var expiredKeys []string
|
||||||
@@ -160,11 +185,15 @@ func (bm *BanManager) List() []BanEntry {
|
|||||||
}
|
}
|
||||||
active = append(active, entry)
|
active = append(active, entry)
|
||||||
}
|
}
|
||||||
|
bm.mu.RUnlock()
|
||||||
|
|
||||||
if len(expiredKeys) > 0 {
|
if len(expiredKeys) > 0 {
|
||||||
|
bm.mu.Lock()
|
||||||
for _, k := range expiredKeys {
|
for _, k := range expiredKeys {
|
||||||
delete(bm.bans, k)
|
delete(bm.bans, k)
|
||||||
}
|
}
|
||||||
go func() { _ = bm.Save() }()
|
bm.mu.Unlock()
|
||||||
|
go func() { _ = bm.save() }()
|
||||||
}
|
}
|
||||||
return active
|
return active
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -533,6 +533,15 @@ func (p *Player) handleCheatCommand(name string, args []string) bool {
|
|||||||
}
|
}
|
||||||
return true
|
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":
|
case "gamemode", "gm":
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
p.Message("Usage: /gamemode <survival/creative/adventure/spectator/0/1/2/3> [player]")
|
p.Message("Usage: /gamemode <survival/creative/adventure/spectator/0/1/2/3> [player]")
|
||||||
|
|||||||
+45
-6
@@ -349,33 +349,64 @@ func (srv *Server) listen(l Listener) {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
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
|
name := c.IdentityData().DisplayName
|
||||||
xuid := c.IdentityData().XUID
|
xuid := c.IdentityData().XUID
|
||||||
uuidStr := c.IdentityData().Identity
|
uuidStr := c.IdentityData().Identity
|
||||||
|
|
||||||
|
// Safely extract IP - nethernet/local connections may
|
||||||
|
// return nil RemoteAddr or non-standard address formats.
|
||||||
ip := ""
|
ip := ""
|
||||||
if addr := c.RemoteAddr(); addr != nil {
|
if addr := c.RemoteAddr(); addr != nil {
|
||||||
if host, _, err := net.SplitHostPort(addr.String()); err == nil {
|
addrStr := addr.String()
|
||||||
|
if addrStr != "" {
|
||||||
|
if host, _, err := net.SplitHostPort(addrStr); err == nil {
|
||||||
ip = host
|
ip = host
|
||||||
} else {
|
} else {
|
||||||
ip = addr.String()
|
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 {
|
if entry, banned := player.Bans.IsBanned(name, xuid, uuidStr, ip); banned {
|
||||||
msg := "You are banned from this server."
|
msg := "You are banned from this server."
|
||||||
|
if entry.Reason != "" {
|
||||||
|
msg += " Reason: " + entry.Reason
|
||||||
|
}
|
||||||
if !entry.ExpiresAt.IsZero() {
|
if !entry.ExpiresAt.IsZero() {
|
||||||
timeLeft := time.Until(entry.ExpiresAt).Round(time.Second)
|
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.WritePacket(&packet.Disconnect{Message: msg})
|
||||||
_ = c.Close()
|
_ = c.Close()
|
||||||
return
|
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.WritePacket(&packet.Disconnect{Message: "Server is full."})
|
||||||
_ = c.Close()
|
_ = c.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg, ok := srv.conf.Allower.Allow(c.RemoteAddr(), c.IdentityData(), c.ClientData()); !ok {
|
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.WritePacket(&packet.Disconnect{HideDisconnectionScreen: msg == "", Message: msg})
|
||||||
_ = c.Close()
|
_ = c.Close()
|
||||||
return
|
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 {
|
if err := conn.StartGameContext(ctx, data); err != nil {
|
||||||
_ = l.Disconnect(conn, "Connection timeout.")
|
_ = 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
|
return
|
||||||
}
|
}
|
||||||
if _, ok := srv.Player(id); ok {
|
if _, ok := srv.Player(id); ok {
|
||||||
_ = l.Disconnect(conn, "Already logged in.")
|
_ = 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
|
return
|
||||||
}
|
}
|
||||||
_ = conn.WritePacket(&packet.ItemRegistry{Items: srv.customItems})
|
_ = conn.WritePacket(&packet.ItemRegistry{Items: srv.customItems})
|
||||||
|
|||||||
@@ -176,7 +176,11 @@ func (conf Config) New(conn Conn) *Session {
|
|||||||
if conf.Log == nil {
|
if conf.Log == nil {
|
||||||
conf.Log = slog.Default()
|
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{}
|
||||||
*s = Session{
|
*s = Session{
|
||||||
|
|||||||
Reference in New Issue
Block a user