Skip to content

Conversation

@GodFuper
Copy link
Contributor

@GodFuper GodFuper commented Dec 23, 2025

изображение изображение

Fixs:
Fix pingwrapper for windows with a language other than "English".
Fixing the TCP parsing of FAF servers.

Features:
A complete rewrite of the Ice4J connection code for Peer2Peer.
Legacy code has been removed from the IceAdapter without loss of functionality.
Ice4J has been updated to the latest version.
Deleted/changed the order of HOST+STUN+RELAY -> STUN+RELAY -> RELAY -> always RELAY.
Now HOST/STUN/RELAY alternate in a cycle, which has a positive effect on connections for users who do not have RELAY working.

The Peer implementation is divided into blocks/modules with the ability to disable.
The Peer connection does not depend on modules, but modules can make their own changes to the connection if necessary.
The connection is implemented in services.

Support connection for the previous version of IceAdapter.

Summary by CodeRabbit

  • New Features

    • Added ICE server management UI window with live status monitoring and server enable/disable controls.
    • Enhanced peer connection UI with strategy selection and real-time connectivity information.
    • Improved telemetry messaging structure for better diagnostics.
  • Refactor

    • Redesigned peer and ICE connection management architecture.
    • Restructured debug window and logging components.
  • Chores

    • Updated dependencies: JavaFX, SLF4J, ice4j, and JUnit 5.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 23, 2025

📝 Walkthrough

Walkthrough

This pull request refactors the ICE adapter from a monolithic architecture to a modular, service-oriented design. Major changes include: introducing UI layers with JavaFX windows and controllers, replacing static debug components with instance-based services, restructuring peer management into a plugin-based module system, establishing abstraction boundaries via service interfaces, and updating build configurations to support the new architecture across multiple submodules.

Changes

Cohort / File(s) Summary
Build Configuration
build.gradle, client/build.gradle, ice-adapter/build.gradle, server/build.gradle
Added OpenJFX plugin (0.1.0) and JavaFX configuration across modules; updated dependencies: ice4j (3.0-66-g1c60acc → 3.2-12-gc2cbf61), slf4j-api (1.7.36 → 2.0.13), added commons-lang3, JUnit 5, logback-classic; enabled JUnit Platform in ice-adapter.
Client & TestClient
client/src/main/java/client/*, client/TestClient.java, client/ice/ICEAdapter.java
Uncommented ice-adapter dependency; switched JJsonRpc source (Geosearchef → faforever); modified GDPR flag default (skipGDRP=true); added --game-id 100 to ICE adapter command line.
Core Architecture Refactoring
ice-adapter/src/main/java/com/faforever/iceadapter/IceAdapter.java
Converted INSTANCE from private static to public volatile; added Lombok @Data; introduced TelemetryDebugger wiring; refactored GameSession lifecycle (createGameSession now synchronized, returns instance); added null-safe public accessors (getId, getGameId, getLogin, getTelemetryServer, getPingCount, getAcceptableLatency); replaced GAME_SESSION static access with getGameSessionSafe(); updated sendToGpgNet with null guards.
Debug System Overhaul
ice-adapter/src/main/java/com/faforever/iceadapter/debug/Debug.java, DebugFacade.java, DebugWindow.java, DebugWindowController.java, TextAreaLogAppender.java
Removed RPC_PORT, ENABLE_DEBUG_WINDOW_LOG_TEXT_AREA, TelemetryDebugger field; eliminated reflection-based DebugWindow launching; replaced with conditional JavaFX UI initialization via IceWindow/InfoWindow; added DebugFacade.close(); completely removed DebugWindow and DebugWindowController classes (eliminates entire JavaFX debug UI).
Service Layer Abstractions
ice-adapter/src/main/java/com/faforever/iceadapter/services/{ConnectService, IceAsync, IceTrigger, UIAdapter}.java
Introduced service interfaces: ConnectService (ICE event callbacks), IceAsync (async task execution), UIAdapter (UI-facing API). Implemented: IceAsyncImpl (executor-based async), UIAdapterImpl (state exposure via observable lists), ConnectServiceHandler (dispatcher), ConnectServiceCommon (shared lifecycle logic), ConnectServiceControlledImpl/NotControlledImpl (peer-controlled variants).
Peer Management Restructure
ice-adapter/src/main/java/com/faforever/iceadapter/ice/peer/Peer.java, PeerEventListener.java, PeerModule.java, IceGameSession.java, GameSession.java
Removed old Peer.java (ice path); introduced new Peer.java (ice/peer path) with modular architecture, event bus, and lifecycle hooks. Added PeerEventListener interface, PeerModule enum (coordinates module instantiation with factory functions), IceGameSession interface (contract for game session behavior). Refactored GameSession: now implements IceGameSession, accepts RPCService/IceOptions via constructor, manages ConnectService variants, wired IceServerChecker.
Peer Module System
ice-adapter/src/main/java/com/faforever/iceadapter/ice/peer/modules/*
Introduced modular peer system: AllowCombination enum (candidate filters), EventBusModule (event dispatch), FaToPeerModule/PeerToFaModule (FA forwarding), PeerToPeerSenderModule/ListenerModule (ICE data relay), RttCalculateModule (latency computation), AutoSettingAllowCandidates/ChangeIceStrategyModule (automatic configuration), FASocketModule (FA UDP socket), PeerConnectivityCheckerModule (echo-based connectivity checks).
ICE Configuration & Utilities
ice-adapter/src/main/java/com/faforever/iceadapter/ice/{IceServer, IceServerChecker, IceState, ModuleBase, CandidatesMessage}.java
Added IceServer enhancements: TypeServer enum, static PUBLIC_STUN_SERVERS, createPublicServers(), mapperFromMap(); IceServerChecker (scheduled periodic latency checks); ModuleBase interface (lifecycle contract); CandidatesMessage.toStrCandidates(); IceState.toString() override.
Data Transfer Objects
ice-adapter/src/main/java/com/faforever/iceadapter/dto/{PeerView, IceServerView}.java
New view models: PeerView (wraps Peer with JavaFX properties, PeerEventListener implementation, AdditionalInfo inner class), IceServerView (observable properties: type, transport, address, rtt, enabled).
GPGNet & RPC Updates
ice-adapter/src/main/java/com/faforever/iceadapter/gpgnet/GPGNetServer.java, FaDataInputStream.java, FaDataOutputStream.java, RPCHandler.java, RPCService.java
Refactored GPGNetServer: public class, added AtomicReference currentClient, virtual-thread executor, GPGNetClient inner class, init(IceAdapter, RPCService) signature. FaDataInputStream/OutputStream: added string field support, null checks, stream state tracking, error logging. RPCService: added @Data/@requiredargsconstructor, host tracking, NOT_CONNECTION sentinel, InfoWindow.showWindow() integration, replaced Arrays.asList with List.of().
UI Layer
ice-adapter/src/main/java/com/faforever/iceadapter/ui/{IceWindow, InfoWindow, IceServerWindow}.java
New JavaFX entry points: IceWindow (main adapter window), IceServerWindow (ICE server manager), InfoWindow (moved from debug, refactored lifecycle to JavaFX start() method).
UI Controllers
ice-adapter/src/main/java/com/faforever/iceadapter/ui/controller/{WindowController, IceServerController, InfoWindowController}.java
New controllers: WindowController (main UI logic, peer table, strategy/combination controls), IceServerController (STUN/TURN server table with live toggle), InfoWindowController (moved from debug package, updated to launch IceWindow).
Telemetry Messages
ice-adapter/src/main/java/com/faforever/iceadapter/telemetry/{OutgoingMessageV1, ConnectToPeer, DisconnectFromPeer, RegisterAsPeer, UpdateCoturnList, UpdateGameState, UpdateGpgnetState, UpdatePeerConnectivity, UpdatePeerState}.java
Added getType() override to all record implementations, returning simple class name.
Utility Classes
ice-adapter/src/main/java/com/faforever/iceadapter/util/{DatagramSocketUtils, ExecutorHolder, IceUtils, LockUtil, PingUtil, PingWrapper, TrayIcon, CandidateUtil}.java
New utilities: DatagramSocketUtils (MAX_SIZE_PACKET, resizeBuffer, isStunPacket, bytesToHex), IceUtils (getFirstComponent), ExecutorHolder (cached executors), PingUtil (latency caching), LockUtil.executeWithLock(Callable), CandidateUtil.infoCandidate(). Updated PingWrapper (English output via cmd chcp 437), TrayIcon (LogoUtils integration, InfoWindow instead of async DebugWindow).
Auxiliary Classes
ice-adapter/src/main/java/com/faforever/iceadapter/{IceOptions, LogoUtils, UiStarter, AgentSuccessMonitor, PeerConnectionSuccessMonitor}.java
IceOptions: @Getter@Data, added manualCombinationConnection/manualStrategyConnection/additionalInfoPeer; LogoUtils (new utility for logo loading); UiStarter (test entry point); AgentSuccessMonitor/PeerConnectionSuccessMonitor (monitoring interfaces/implementations).
Resources & Configuration
ice-adapter/src/main/resources/{iceWindow.fxml, IceServerTable.fxml, infoWindow.fxml, debugWindow.fxml, logback.xml}
Added new FXML layouts (iceWindow, IceServerTable), updated infoWindow controller path, removed debugWindow.fxml, updated logback.xml (added FILE appender pattern refinement, removed TEXTAREA appender), added server/src/main/resources/logback.xml.
Removed Classes
ice-adapter/src/main/java/com/faforever/iceadapter/ice/{Peer.java (old), PeerIceModule.java, PeerConnectivityCheckerModule.java, PeerTurnRefreshModule.java}
Deleted old Peer (ice path), PeerIceModule (ICE agent lifecycle), PeerConnectivityCheckerModule, PeerTurnRefreshModule (reflection-based TURN refresh). Functionality migrated to new modular Peer and peer modules.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client App
    participant IA as IceAdapter
    participant GS as GameSession
    participant CS as ConnectService
    participant Peer as Peer
    participant Mod as PeerModule<br/>(EventBus, FA, ICE)
    participant ICE as ICE4J Agent

    Client->>IA: connectToPeer(login, id, offer, combo)
    IA->>GS: createGameSession()
    GS->>Peer: new Peer(id, login)
    Peer->>Peer: init()
    Peer->>Mod: initModules()
    loop Per PeerModule
        Mod->>Mod: createModule(Peer)
        Mod->>Peer: register listener
    end
    Peer->>Peer: startModules()
    
    GS->>CS: onChangeIceState(Peer, NEW, GATHERING)
    CS->>ICE: createAgent()
    CS->>ICE: gatherCandidates()
    
    Note over ICE: STUN/TURN harvest
    
    CS->>CS: awaiting candidates (timeout 5s)
    
    alt RPC receives remote candidates
        Client->>GS: onIceMessageFromRPC(candidates)
        GS->>CS: onMessageFromRPC(Peer, message)
        CS->>ICE: unpackCandidates()
        CS->>CS: onChangeIceState(CHECKING)
    end
    
    CS->>CS: checking(Peer)
    ICE->>ICE: connectivity checks
    ICE->>ICE: pair selected
    
    CS->>CS: onChangeIceState(CONNECTED)
    CS->>Peer: onConnected()
    Peer->>Mod: fire onConnected event
    Mod->>Mod: start modules
    
    Peer->>Peer: ready for data forwarding
Loading
sequenceDiagram
    participant FA as Forwarder (FA)
    participant P2P as PeerToPeerListener
    participant Peer as Peer
    participant F2P as FaToPeerModule
    participant ICE as ICE Component
    participant P2F as PeerToFaModule

    FA->>P2P: UDP datagram (peer data)
    P2P->>Peer: onSendToPeer(data)
    Peer->>F2P: trigger event
    F2P->>ICE: send via component
    
    ICE->>P2P: receive data
    P2P->>P2P: parse COMMAND_FA prefix
    P2P->>Peer: onSendToFaSocket(data)
    Peer->>P2F: trigger event
    P2F->>FA: DatagramSocket.send(localhost:port)
    
    Note over P2P,P2F: Echo-based connectivity<br/>checking also wired<br/>via same path
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

The changes constitute a fundamental architectural refactoring affecting core subsystems (peer lifecycle, ICE connection handling, debug/UI framework), introducing new service abstractions, modular peer plugin system, and UI layer. The diff spans 60+ files with diverse patterns: new class introductions, multi-file deletions, signature changes across service boundaries, and intricate state machine rewiring in ICE connection flows. High logic density in ConnectServiceCommon, GameSession, and Peer classes, plus heterogeneous file types (Java classes, FXML, Gradle configs, resources) demand careful reasoning per file and cross-file dependency validation.

Poem

🐰 Hop, hop, the old debug window's gone,
Modular peers now dance the refactor song!
From static chains to services so free,
The ICE flows cleaner—connectivity with glee!
JavaFX windows bloom in the UI place,
Where once was chaos, now structured grace. 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.30% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Full rework ice adapter' accurately summarizes the main change—a complete rewrite of the ICE adapter codebase—and is the primary focus across the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
ice-adapter/src/main/java/com/faforever/iceadapter/gpgnet/FaDataInputStream.java (1)

85-99: Add upper bound validation for string size.

The method validates that size is non-negative (lines 88-90) but doesn't enforce an upper limit. A malicious or corrupted stream could specify an extremely large size, causing memory exhaustion when allocating the byte array (line 96).

🔎 Proposed fix

Add a constant and validation:

 public class FaDataInputStream extends InputStream {
 
     private static final int MAX_CHUNK_SIZE = 10;
+    private static final int MAX_STRING_SIZE = 1048576; // 1MB
     private static final int FIELD_TYPE_INT = 0;
     private static final int FIELD_TYPE_STRING = 1;
 public String readString() throws IOException {
     int size = readInt();
 
     if (size < 0) {
         throw new IOException("Invalid string length: " + size);
     }
 
+    if (size > MAX_STRING_SIZE) {
+        throw new IOException("String too large: " + size);
+    }
+
     if (size == 0) {
         return "";
     }
ice-adapter/src/main/java/com/faforever/iceadapter/util/PingWrapper.java (1)

40-61: Blocking I/O on ForkJoinPool.commonPool() can cause thread starvation.

The supplyAsync() call without an explicit executor uses ForkJoinPool.commonPool(), which is optimized for CPU-bound tasks. The process.waitFor() on line 43 is a blocking I/O operation that will occupy a thread from the common pool, potentially starving other tasks.

🔎 Proposed fix to use a dedicated executor

Add a dedicated executor for I/O operations:

+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
 @Slf4j
 public class PingWrapper {
+    private static final ExecutorService IO_EXECUTOR = Executors.newCachedThreadPool(
+        r -> {
+            Thread t = new Thread(r, "ping-worker");
+            t.setDaemon(true);
+            return t;
+        }
+    );
+
     static final Pattern WINDOWS_OUTPUT_PATTERN = Pattern.compile("Average = (\\d+)ms", Pattern.MULTILINE);

Then use it:

-            return CompletableFuture.supplyAsync(
+            return CompletableFuture.supplyAsync(
                     () -> {
                         try {
                             process.waitFor();
                             InputStreamReader reader = new InputStreamReader(process.getInputStream());
                             String output = CharStreams.toString(reader);
                             reader.close();
 
                             Matcher m = output_pattern.matcher(output);
 
                             if (m.find()) {
                                 double result = Double.parseDouble(m.group(1));
                                 log.debug("Pinged {} with an RTT of {}", address, result);
                                 return result;
                             } else {
                                 log.warn("Failed to ping {}: output='{}'", address, output);
                                 throw new RuntimeException("Failed to contact the host or parse ping output");
                             }
                         } catch (InterruptedException | IOException | RuntimeException e) {
                             throw new CompletionException(e);
                         }
-                    });
+                    }, IO_EXECUTOR);
ice-adapter/src/main/java/com/faforever/iceadapter/rpc/RPCService.java (1)

55-59: Potential NPE on InfoWindow.INSTANCE.showWindow().

After Debug.init(), InfoWindow.INSTANCE might still be null if initialization fails. The code assumes INSTANCE is set after the if block.

🔎 Proposed fix
                     if (InfoWindow.INSTANCE == null) {
                         Debug.ENABLE_INFO_WINDOW = true;
                         Debug.init();
                     }
-                    InfoWindow.INSTANCE.showWindow();
+                    if (InfoWindow.INSTANCE != null) {
+                        InfoWindow.INSTANCE.showWindow();
+                    }
🟠 Major comments (20)
client/src/main/java/client/TestClient.java-83-83 (1)

83-83: Fix the GDPR dialog logic: the default skip renders the feature unreachable.

Changing skipGDRP to true by default makes the GDPR dialog impossible to display, since line 119-121 only shows it when skipGDRP is false. This breaks the ability to test or review the GDPR consent flow. Additionally:

  • The --skip argument (lines 99-101) is now redundant: it sets skipGDRP=true when it's already true by default.
  • There is no command-line option to show the GDPR dialog (e.g., --show-gdpr).
  • Typos throughout: "GDRP" (lines 92) should be "GDPR"; variable name skipGDRP should be skipGDPR.

Either change the default to skipGDRP = false to make the dialog show by default (with --skip to bypass it), or remove the GDPR dialog feature entirely if it's not needed. Also fix the typos.

ice-adapter/src/main/java/com/faforever/iceadapter/util/DatagramSocketUtils.java-14-23 (1)

14-23: Socket buffer size is dangerously small and will cause packet loss.

The method sets both receive and send socket buffers to MAX_SIZE_PACKET (1472 bytes), which is the maximum UDP packet payload size. However, socket buffers should be much larger—typically 64KB to 256KB or more—to allow the OS to queue multiple packets while the application processes them. Setting buffers to just 1472 bytes means only a single packet can be buffered, causing packet drops under any moderate load.

The confusion appears to be between:

  • Packet size (MAX_SIZE_PACKET = 1472 bytes) ✓ correct for individual datagram buffers
  • Socket buffer size (should be 64KB+) ✗ currently set to 1472 bytes
🔎 Recommended fix: Use appropriate socket buffer sizes
 public void resizeBuffer(DatagramSocket socket) {
     try {
-        socket.setReceiveBufferSize(MAX_SIZE_PACKET);
-        socket.setSendBufferSize(MAX_SIZE_PACKET);
+        // Set socket buffers to reasonable sizes for network traffic
+        // Typical values: 64KB-256KB depending on expected load
+        socket.setReceiveBufferSize(256 * 1024);  // 256 KB
+        socket.setSendBufferSize(256 * 1024);     // 256 KB
     } catch (SocketException e) {
         if (!socket.isClosed()) {
-            log.error("Failed to resize socket buffer on {}", MAX_SIZE_PACKET, e);
+            log.error("Failed to resize socket buffer", e);
         }
     }
 }

Alternatively, if you want to keep the buffer size configurable, define separate constants:

public static final int SOCKET_BUFFER_SIZE = 256 * 1024; // 256 KB
ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerConnectionSuccessMonitor.java-93-98 (1)

93-98: Missing listener cleanup in shutdown().

The shutdown() method terminates the scheduler and cancels the timeout task, but does not unregister the listeners added in start() (lines 48-49). This means the agent and mediaStream will retain references to this monitor even after shutdown, preventing garbage collection.

This issue is addressed by the cleanup() method suggested in the earlier comment for start().

🔎 Proposed fix
     public void shutdown() {
+        cleanup(); // Unregister listeners and cancel timeout
         scheduler.shutdown();
-        if (scheduledFuture != null) {
-            scheduledFuture.cancel(true);
-        }
     }

Where cleanup() is as defined in the earlier suggestion.

Committable suggestion skipped: line range outside the PR's diff.

ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerConnectionSuccessMonitor.java-17-23 (1)

17-23: Resource leak: scheduler created eagerly at field initialization.

The ScheduledExecutorService is created when the instance is constructed (line 18), which means:

  1. If start() is never called, the scheduler thread still exists until shutdown() is called
  2. If shutdown() is not called (e.g., due to an exception or oversight), the scheduler thread leaks

Additionally, Executors.newScheduledThreadPool(1) is less explicit than Executors.newSingleThreadScheduledExecutor() for a single-threaded scheduler.

🔎 Proposed fix: lazy initialization
-    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
+    private ScheduledExecutorService scheduler;
     private ScheduledFuture<?> scheduledFuture;
     private CompletableFuture<Boolean> future;
     private String name = "";
     private final long timeoutMs;
     private final Peer peer;

     @Override
     public CompletableFuture<Boolean> start() {
         future = new CompletableFuture<>();
+        scheduler = Executors.newSingleThreadScheduledExecutor();

Alternatively, consider using a shared scheduler passed via constructor or using the peer's executor service if available.

Committable suggestion skipped: line range outside the PR's diff.

ice-adapter/src/main/java/com/faforever/iceadapter/gpgnet/FaDataOutputStream.java-30-30 (1)

30-30: Fix inconsistent closed-state behavior and race conditions.

The implementation has several issues:

  1. Inconsistent behavior: flush() silently returns when closed (lines 77-79), while write() (line 41) and writeMessage() (line 65) throw IOException. This inconsistency can mask errors.

  2. Race condition in write(): Line 41 checks closed before acquiring the lock. Another thread could close the stream between the check and lock acquisition, leading to operations on a closed stream.

  3. Insufficient synchronization: The volatile keyword on closed (line 30) ensures visibility but doesn't prevent the check-then-act race in write() and the lock/unlock window in other methods.

🔎 Proposed fix
 @Override
 public void write(int b) throws IOException {
-    if (closed) throw new IOException("Stream is closed");
     writer.lock();
     try {
+        if (closed) throw new IOException("Stream is closed");
         outputStream.write(b);
     } finally {
         writer.unlock();
     }
 }

 @Override
 public void flush() throws IOException {
-    if (closed) {
-        return;
-    }
     writer.lock();
     try {
+        if (closed) throw new IOException("Stream is closed");
         outputStream.flush();
     } finally {
         writer.unlock();
     }
 }

Also applies to: 40-48, 58-72, 76-86, 89-98

ice-adapter/src/main/java/com/faforever/iceadapter/UiStarter.java-1-22 (1)

1-22: Document purpose and add initialization verification.

This class appears to be a test/development harness but has several issues:

  1. Missing documentation: No class-level Javadoc or comments explaining this is for testing/development purposes.

  2. Race condition risk: Lines 19-20 assume IceAdapter.INSTANCE is initialized synchronously by IceAdapter.main(). If initialization is asynchronous or deferred, this will result in NPE.

  3. No state verification: onJoinGame() is called immediately without verifying the adapter is ready to accept game events.

  4. Unclear location: This appears to be test/development code but is located in src/main/java instead of src/test/java.

🔎 Suggested improvements
 package com.faforever.iceadapter;
 
+/**
+ * Test/development harness for launching the IceAdapter with a UI.
+ * This class is for development purposes only and should not be used in production.
+ */
 public class UiStarter {
 
     public static void main(String[] args) {
         String[] stubArgs = {
                 "--id=12345",
                 "--game-id=67890",
                 "--login=testUser",
                 "--gpgnet-port=5000",
                 "--rpc-port=5001",
                 "--lobby-port=5002",
                 "--debug-window=true",
                 "--info-window=true"
         };
 
         IceAdapter.main(stubArgs);
 
         IceAdapter adapter = IceAdapter.INSTANCE;
+        if (adapter == null) {
+            throw new IllegalStateException("IceAdapter not initialized");
+        }
+        
+        // Wait for adapter to be fully initialized
+        // TODO: Add proper readiness check
         adapter.onJoinGame("Player2", 123);
     }
 }

Consider moving to src/test/java if this is purely for testing.

ice-adapter/src/main/java/com/faforever/iceadapter/ice/ModuleBase.java-3-21 (1)

3-21: Reconsider nullable Boolean return types.

The isRunning() and isEnabled() methods return Boolean (wrapper type) with a default of null (lines 14-20). This design is problematic:

  1. NPE risk: Callers using these methods in conditionals or auto-unboxing contexts will encounter NPE if they don't explicitly check for null.
  2. Unclear semantics: What does null mean? "Not applicable", "unknown", or "not implemented"? Without documentation, this is ambiguous.
  3. Inconsistent with naming: Methods prefixed with is* typically return boolean primitives, not nullable wrappers.

Consider alternatives:

  • Use boolean primitives with meaningful defaults (e.g., false).
  • Use Optional<Boolean> to make the tri-state explicit.
  • Add comprehensive Javadoc explaining the null semantics.
🔎 Proposed fix option 1 (boolean primitive)
-    default Boolean isRunning() {
-        return null;
+    default boolean isRunning() {
+        return false;
     }
 
-    default Boolean isEnabled() {
-        return null;
+    default boolean isEnabled() {
+        return true;
     }
🔎 Proposed fix option 2 (Optional for explicit tri-state)
+import java.util.Optional;
+
 public interface ModuleBase {
 
     default void start() {
     }
 
     default void stop() {
     }
 
     default void init() {
     }
 
-    default Boolean isRunning() {
-        return null;
+    default Optional<Boolean> isRunning() {
+        return Optional.empty();
     }
 
-    default Boolean isEnabled() {
-        return null;
+    default Optional<Boolean> isEnabled() {
+        return Optional.empty();
     }
 
 }
ice-adapter/src/main/java/com/faforever/iceadapter/ice/peer/modules/fa/FaToPeerModule.java-24-24 (1)

24-24: ExecutorService is never shut down, causing resource leak.

The executor field is created as a new VirtualThreadPerTaskExecutor but stop() never calls executor.shutdown(). This will prevent clean shutdown of the executor.

🔎 Proposed fix
     public void stop() {
         LockUtil.executeWithLock(peer.getLock(LOCK_MODULE), this::stopListeners);
+        executor.shutdown();
     }

     private void stopListeners() {
-        if (peer.isClosing() && listener != null && !listener.isDone()) {
+        if (listener != null && !listener.isDone()) {
             listener.cancel(true);
             listener = null;
         }
     }

Also applies to: 42-50

ice-adapter/src/main/java/com/faforever/iceadapter/ice/IceServerChecker.java-33-40 (1)

33-40: Uncaught exceptions will silently stop the scheduled task.

If checkerThread() throws any exception, ScheduledExecutorService will suppress it and stop future executions without notification. Wrap the body in try-catch to log errors and allow continued execution.

🔎 Proposed fix
     private void checkerThread() {
+        try {
             gameSession.getIceServers().stream()
                     .filter(IceServer::isTurn)
                     .filter(IceServer::isAuto)
                     .forEach(server -> {
                         server.setEnabled(server.hasAcceptableLatency(options.getAcceptableLatency()));
                     });
+        } catch (Exception e) {
+            log.error("Error in ICE server check", e);
+        }
     }
ice-adapter/src/main/java/com/faforever/iceadapter/ui/IceWindow.java-39-52 (1)

39-52: NPE if FXML loading fails: new Scene(null) will throw.

If loader.load() throws IOException, root remains null. The subsequent new Scene(root) on line 52 will throw NullPointerException. Either return early after catching the exception or propagate the error.

🔎 Proposed fix
         try {
             FXMLLoader loader = new FXMLLoader(getClass().getResource("/iceWindow.fxml"));
             root = loader.load();

             controller = loader.getController();
             controller.setAdapter(new UIAdapterImpl(IceAdapter.INSTANCE));
             controller.initialize();
         } catch (IOException e) {
             log.error("Could not load debugger window fxml", e);
+            return; // Prevent NPE from new Scene(null)
         }
ice-adapter/src/main/java/com/faforever/iceadapter/ice/peer/modules/other/PeerConnectivityCheckerModule.java-29-29 (1)

29-29: Resource leak: ScheduledExecutorService is never shut down.

The scheduledExecutorService created at line 29 is never terminated. The stop() method only cancels the scheduled future but doesn't shut down the executor. This leaks a thread per peer.

🔎 Proposed fix
     @Override
     public void stop() {
         if (!peer.isLocalOffer()) {
             return;
         }
 
         LockUtil.executeWithLock(peer.getLock(LOCK_CHECKER_MODULE), () -> {
             if (scheduledFuture != null) {
                 scheduledFuture.cancel(true);
                 scheduledFuture = null;
             }
+            scheduledExecutorService.shutdownNow();
         });
     }

Also applies to: 74-85

ice-adapter/src/main/java/com/faforever/iceadapter/ui/controller/IceServerController.java-42-42 (1)

42-42: Resource leak: updateScheduler is never shut down.

The ScheduledExecutorService created in startPeriodicUpdates() is never terminated. This will prevent the JVM from shutting down cleanly and leak threads. Add a dispose() method similar to WindowController.

🔎 Proposed fix
     private UIAdapter adapter;
     private ScheduledExecutorService updateScheduler;
+
+    public void dispose() {
+        if (updateScheduler != null && !updateScheduler.isShutdown()) {
+            updateScheduler.shutdown();
+        }
+    }

Also applies to: 105-110

ice-adapter/src/main/java/com/faforever/iceadapter/ui/controller/WindowController.java-211-221 (1)

211-221: Listener leak: value listeners accumulate on combo boxes.

Each time setSelectedPeer is called, new listeners are added to allowCombinationComboBox and connectionStrategyComboBox without removing previous ones. This causes listener accumulation and potential memory leaks when users repeatedly select different peers.

🔎 Proposed fix

Store the listener references as fields and remove them before adding new ones:

+    private javafx.beans.value.ChangeListener<AllowCombination> combinationListener;
+    private javafx.beans.value.ChangeListener<IceAgentStrategy> strategyListener;
+
     private void setSelectedPeer(PeerView peer) {
         // ... existing code ...
 
+        // Remove previous listeners
+        if (combinationListener != null) {
+            allowCombinationComboBox.valueProperty().removeListener(combinationListener);
+        }
+        if (strategyListener != null) {
+            connectionStrategyComboBox.valueProperty().removeListener(strategyListener);
+        }
+
-        allowCombinationComboBox.valueProperty().addListener((observable, oldValue, newValue) -> {
+        combinationListener = (observable, oldValue, newValue) -> {
             if (!Objects.equals(oldValue, newValue)) {
                 adapter.setAllowCombination(peer, newValue);
             }
-        });
+        };
+        allowCombinationComboBox.valueProperty().addListener(combinationListener);
 
-        connectionStrategyComboBox.valueProperty().addListener((observable, oldValue, newValue) -> {
+        strategyListener = (observable, oldValue, newValue) -> {
             if (!Objects.equals(oldValue, newValue)) {
                 adapter.setStrategy(peer, newValue);
             }
-        });
+        };
+        connectionStrategyComboBox.valueProperty().addListener(strategyListener);
ice-adapter/src/main/java/com/faforever/iceadapter/services/impl/UIAdapterImpl.java-153-163 (1)

153-163: Potential listener memory leak: PeerView registered but never unregistered.

When toPeerInfo is called, the PeerView is registered as an event listener on the Peer (line 156). However, when peers are removed from the session (lines 113-117), the corresponding PeerView is removed from uiPeers but never unregistered from the Peer's listener list.

Consider unregistering the listener when removing peers:

uiPeers.keySet().stream()
    .filter(id -> !ids.contains(id))
    .forEach(id -> {
        PeerView view = uiPeers.remove(id);
        // Unregister from peer if peer still exists
        getGameSession()
            .flatMap(s -> s.getPeer(id))
            .ifPresent(p -> p.removeEventListener(view));
    });
ice-adapter/src/main/java/com/faforever/iceadapter/dto/PeerView.java-46-59 (1)

46-59: Thread safety concern: JavaFX properties modified outside FX Application Thread.

The event callbacks (onIceStateChange, onAgentChange, onLastPacketReceived) delegate to update(Peer), which modifies JavaFX properties. These callbacks are invoked asynchronously from EventBusModule via virtual threads (not the FX Application Thread). JavaFX properties should only be modified on the FX Application Thread to avoid potential race conditions or UI binding issues.

Consider wrapping property updates in Platform.runLater():

public void update(Peer peer) {
    Platform.runLater(() -> {
        getConnected().set(peer.isConnected());
        // ... rest of property updates
    });
}

Also applies to: 61-89

ice-adapter/src/main/java/com/faforever/iceadapter/ice/peer/modules/ice/PeerToPeerListenerModule.java-44-44 (1)

44-44: Lock acquisition wraps blocking receive loop.

The createListener method is called inside LockUtil.executeWithLock(), meaning the lock is held for the entire duration of the blocking receive loop. This could block other operations needing the same lock (e.g., stop()).

Consider restructuring to release the lock after setup:

Thread.ofVirtual().name(threadComponentListenerName()).start(() -> createListener(component));

And move lock usage to only protect socket access/mutation if needed.

ice-adapter/src/main/java/com/faforever/iceadapter/services/impl/UIAdapterImpl.java-124-130 (1)

124-130: Creates new ObservableList on each call.

getPeerInfoList() returns a new FXCollections.observableArrayList() each time it's called. This breaks JavaFX bindings since the list identity changes. If UI components bind to this list, they won't see updates.

Consider maintaining a persistent ObservableList field that is updated in place.

ice-adapter/src/main/java/com/faforever/iceadapter/ice/GameSession.java-38-38 (1)

38-38: Static mutable list iceServers is not thread-safe.

The static iceServers list is modified by setIceServers() (clear, addAll) without synchronization, while it may be read concurrently by getIceServers() or IceServerChecker. This can cause ConcurrentModificationException or inconsistent reads.

🔎 Proposed fix - use synchronized list or copy-on-write
-    private static final List<IceServer> iceServers = createIceServers();
+    private static final List<IceServer> iceServers = Collections.synchronizedList(new ArrayList<>());
+
+    static {
+        addDefaultIceServers(iceServers);
+    }

     public static void setIceServers(List<Map<String, Object>> iceServersData) {
-        iceServers.clear();
-        addDefaultIceServers(iceServers);
-        ...
-        iceServers.addAll(pair.getFirst());
+        synchronized (iceServers) {
+            iceServers.clear();
+            addDefaultIceServers(iceServers);
+            ...
+            iceServers.addAll(pair.getFirst());
+        }

Alternatively, use CopyOnWriteArrayList if reads vastly outnumber writes.

Also applies to: 165-180

ice-adapter/src/main/java/com/faforever/iceadapter/rpc/RPCService.java-68-72 (1)

68-72: Potential NPE when getPeerOrWait() returns null.

getPeerOrWait() can return null (line 118), but the callers at lines 70, 76, 86, 95-97, and 103 invoke methods on the result without null-checking, which will cause a NullPointerException.

🔎 Proposed fix
     public void onConnectionStateChanged(String newState) {
         if (!skipRPCMessages) {
-            getPeerOrWait().sendNotification("onConnectionStateChanged", List.of(newState));
+            JJsonPeer peer = getPeerOrWait();
+            if (peer != null) {
+                peer.sendNotification("onConnectionStateChanged", List.of(newState));
+            }
         }
     }

Apply similar null checks to the other methods.

ice-adapter/src/main/java/com/faforever/iceadapter/ice/peer/Peer.java-259-273 (1)

259-273: Race condition in close() method.

The check-then-act pattern on closing (line 260-266) is not atomic. Two threads could both pass the if (closing) check before either sets closing = true, causing duplicate close operations.

🔎 Proposed fix using synchronized or AtomicBoolean
+    private final AtomicBoolean closingFlag = new AtomicBoolean(false);
+
     public void close() {
-        if (closing) {
+        if (!closingFlag.compareAndSet(false, true)) {
             return;
         }
 
         log.info("Closing peer for player {}", getPeerIdentifier());
 
-        closing = true;
-        event(bus -> bus.onClose(this, closing));
+        event(bus -> bus.onClose(this, true));
         for (ModuleBase module : modules.values()) {
             module.stop();
         }
 
         log.info("Peer closed: {}", getPeerIdentifier());
     }

Note: Also update the isClosing() method to use closingFlag.get().

Committable suggestion skipped: line range outside the PR's diff.

🟡 Minor comments (24)
ice-adapter/src/main/java/com/faforever/iceadapter/util/DatagramSocketUtils.java-25-36 (1)

25-36: STUN packet type validation contains incorrect and obsolete type codes.

The method checks several STUN message types, but some are invalid per RFC 5389:

  • 0x0000: Invalid—method 0 is not defined in any STUN RFC
  • 0x0002: Invalid—should be 0x0011 for Binding Indication (RFC 5389)
  • 0x0115 / 0x0116: Obsolete—these method values (0x045) are from RFC 3489, not RFC 5389

RFC 5389 defines valid Binding method types with correct class bits:

  • 0x0001: Binding Request
  • 0x0101: Binding Success Response
  • 0x0111: Binding Error Response
  • 0x0011: Binding Indication

Update the type checks to match RFC 5389 specifications.

ice-adapter/src/main/java/com/faforever/iceadapter/util/ExecutorHolder.java-10-12 (1)

10-12: Clarify or verify the Ice4J virtual thread constraint in javadoc.

The javadoc states Ice4J is "not allowed to use virtual threads in Java 21," but this appears to be an unverified assumption rather than an established constraint. Ice4J is a standard JVM library that runs on Java 21; no official project documentation explicitly forbids virtual threads. The actual issue is that virtual thread compatibility is untested and depends on Ice4J's internal use of ThreadLocals, synchronized blocks, or native operations.

Either cite a specific Ice4J issue/version requirement for this constraint, or revise the javadoc to reflect that virtual thread compatibility is untested and requires validation.

ice-adapter/src/main/java/com/faforever/iceadapter/util/ExecutorHolder.java-16-17 (1)

16-17: Add explicit shutdown for executors if the application needs graceful cleanup.

While the executors are never explicitly shut down, this has minimal practical impact since the application always calls System.exit() which terminates the JVM immediately. However, for cleaner shutdown semantics and to avoid abruptly terminating threads, consider adding a shutdown() method to ExecutorHolder that properly closes both executors before application exit.

ice-adapter/src/main/java/com/faforever/iceadapter/IceOptions.java-52-55 (1)

52-55: Description appears to be copy-pasted from --ping-count.

The description for --acceptable-latency says "number of times to ping each turn server to determine latency", which is the description for --ping-count. This should describe what the acceptable latency threshold is used for.

Suggested fix
     @Option(names = "--acceptable-latency",
             defaultValue = "250.0",
-            description = "number of times to ping each turn server to determine latency")
+            description = "maximum acceptable latency threshold in milliseconds")
     private double acceptableLatency;
client/src/main/java/client/ice/ICEAdapter.java-223-223 (1)

223-223: Hardcoded game-id is inconsistent with other dynamic parameters.

The --game-id is set to a hardcoded value 100, while --id and --login are dynamically sourced from TestClient.playerID and TestClient.username. Add a comment explaining if this is intentional test scaffolding, or make it configurable to match the pattern of other parameters.

ice-adapter/src/main/java/com/faforever/iceadapter/ice/IceServerChecker.java-23-31 (1)

23-31: Race condition in start(): double scheduling possible.

If start() is called concurrently from multiple threads, both may see scheduledFuture == null and schedule duplicate tasks. Consider synchronizing access or using AtomicReference<ScheduledFuture<?>> with CAS.

🔎 Proposed fix with synchronization
+    private final Object lock = new Object();
+
     public void start() {
-        if (scheduledFuture != null) {
-            return;
+        synchronized (lock) {
+            if (scheduledFuture != null) {
+                return;
+            }
+            scheduledFuture = scheduler.scheduleAtFixedRate(this::checkerThread,
+                    0,
+                    INTERVAL,
+                    TimeUnit.MINUTES);
         }
-        scheduledFuture = scheduler.scheduleAtFixedRate(this::checkerThread,
-                0,
-                INTERVAL,
-                TimeUnit.MINUTES);
     }

Committable suggestion skipped: line range outside the PR's diff.

ice-adapter/src/main/java/com/faforever/iceadapter/ice/peer/modules/other/AutoSettingAllowCandidates.java-35-43 (1)

35-43: Race condition in combination cycling logic.

The getAndIncrement() followed by a conditional reset can cause multiple threads to cycle incorrectly if called concurrently. Use updateAndGet with modulo arithmetic for atomic cycling.

🔎 Suggested fix
     private void changeCombination() {
-        int id = index.getAndIncrement();
-        if (id >= combinations.size()) {
-            index.set(0);
-            id = 0;
-        }
+        int id = index.updateAndGet(i -> (i + 1) % combinations.size());
         AllowCombination combination = combinations.get(id);
         peer.setCombination(combination);
     }

Note: This changes the starting index behavior slightly (starts at 1 instead of 0), so adjust if the first combination must be index 0:

int id = index.getAndUpdate(i -> (i + 1) % combinations.size());
ice-adapter/src/main/java/com/faforever/iceadapter/ui/IceServerWindow.java-74-81 (1)

74-81: Race condition in launch() singleton initialization.

Multiple concurrent calls to launch() could pass the INSTANCE == null check before any of them completes start(), potentially creating multiple windows. Consider synchronizing the check-and-create logic.

🔎 Suggested fix
     public static void launch() {
         log.info("Launching IceServerTableApp window.");
-        if (INSTANCE == null) {
-            runOnUIThread(() -> new IceServerWindow().start(new Stage()));
-        } else {
-            INSTANCE.showWindow();
-        }
+        runOnUIThread(() -> {
+            if (INSTANCE == null) {
+                new IceServerWindow().start(new Stage());
+            } else {
+                INSTANCE.showWindow();
+            }
+        });
     }
ice-adapter/build.gradle-34-34 (1)

34-34: Test ice4j 3.2 compatibility thoroughly before merging.

The upgrade from the custom ice4j fork (3.0-66-g1c60acc) to the official Jitsi mainline (3.2-12-gc2cbf61) is a significant migration. Ensure the existing test suite (particularly IceTest.java) passes and validate ICE connectivity functionality in realistic scenarios, as the codebase has multiple dependencies on ice4j APIs across ConnectServiceCommon, ConnectServiceControlled/NotControlled, and related classes.

ice-adapter/src/main/java/com/faforever/iceadapter/ice/peer/PeerModule.java-62-66 (1)

62-66: Redundant comparator: compares the same field twice.

The thenComparing(PeerModule::getPriority) adds no value since it's comparing the same field that was already used in comparingInt. This appears to be a copy-paste error.

🔎 Proposed fix
     @Override
     public int compare(PeerModule o1, PeerModule o2) {
-        return Comparator.comparingInt(PeerModule::getPriority)
-                .thenComparing(PeerModule::getPriority).compare(o1, o2);
+        return Integer.compare(o1.getPriority(), o2.getPriority());
     }
ice-adapter/src/main/java/com/faforever/iceadapter/ice/peer/modules/other/PeerConnectivityCheckerModule.java-106-107 (1)

106-107: Bug: Log message uses wrong variable.

The log message prints lastPacketReceived (a timestamp) instead of sinceLastReal (the elapsed time). This will display confusing values like "No traffic for 1703372400000 ms".

🔎 Proposed fix
         if (sinceLastReal > TIMEOUT_BEFORE_LOST_CONNECT) {
-            log.warn("{} No traffic (echo or game) for {} ms (> {} ms timeout). Closing connection.",
-                    peer.getPeerIdentifier(), lastPacketReceived, TIMEOUT_BEFORE_LOST_CONNECT);
+            log.warn("{} No traffic (echo or game) for {} ms (> {} ms timeout). Closing connection.",
+                    peer.getPeerIdentifier(), sinceLastReal, TIMEOUT_BEFORE_LOST_CONNECT);
             peer.lostConnect();
             return;
         }
ice-adapter/src/main/java/com/faforever/iceadapter/ui/controller/IceServerController.java-64-68 (1)

64-68: Potential IndexOutOfBoundsException when accessing table items.

The getIndex() call inside the CheckBox action handler can throw an exception if the table items are modified concurrently (e.g., during a periodic update). Consider adding a bounds check or using the cell's current item instead.

🔎 Proposed fix
             {
                 checkBox.setOnAction(event -> {
-                    IceServerView item = getTableView().getItems().get(getIndex());
-                    adapter.setEnabledIceServer(item, checkBox.isSelected());
+                    int index = getIndex();
+                    if (index >= 0 && index < getTableView().getItems().size()) {
+                        IceServerView item = getTableView().getItems().get(index);
+                        adapter.setEnabledIceServer(item, checkBox.isSelected());
+                    }
                 });
             }
ice-adapter/src/main/java/com/faforever/iceadapter/ui/controller/WindowController.java-248-264 (1)

248-264: Inconsistent threading: label updates outside Platform.runLater.

The label updates (lines 253-263) are performed directly while the peer table update is wrapped in Platform.runLater. Since updateAllInfo is called from a background scheduler thread, all UI updates should be within Platform.runLater to avoid potential threading issues.

🔎 Proposed fix
     private void updateAllInfo() {
         if (adapter == null) {
             return;
         }
 
-        versionLabel.setText("Version: %s".formatted(adapter.getVersion()));
-        userLabel.setText("User: %s(%s)".formatted(adapter.getUsername(), adapter.getUserId()));
-        rpcPortLabel.setText("RPC_PORT: %s".formatted(adapter.getRpcPort()));
-        gpgnetPortLabel.setText("GPGNET_PORT: %s".formatted(adapter.getGpgNetPort()));
-        lobbyPortLabel.setText("LOBBY_PORT: %s".formatted(adapter.getLobbyPort()));
-
-        rpcServerStatus.setText("RPCServer: %s".formatted(adapter.getRpcServerStatus()));
-        rpcClientStatus.setText("RPCClient: %s".formatted(adapter.getRpcClientStatus()));
-        gpgnetServerStatus.setText("GPGNetServer: %s".formatted(adapter.getGpgNetServerStatus()));
-        gpgnetClientStatus.setText("GPGNetClient: %s".formatted(adapter.getGpgNetClientStatus()));
-        gameState.setText("GameState: %s".formatted(adapter.getGameState()));
-
         Platform.runLater(() -> {
+            versionLabel.setText("Version: %s".formatted(adapter.getVersion()));
+            userLabel.setText("User: %s(%s)".formatted(adapter.getUsername(), adapter.getUserId()));
+            rpcPortLabel.setText("RPC_PORT: %s".formatted(adapter.getRpcPort()));
+            gpgnetPortLabel.setText("GPGNET_PORT: %s".formatted(adapter.getGpgNetPort()));
+            lobbyPortLabel.setText("LOBBY_PORT: %s".formatted(adapter.getLobbyPort()));
+
+            rpcServerStatus.setText("RPCServer: %s".formatted(adapter.getRpcServerStatus()));
+            rpcClientStatus.setText("RPCClient: %s".formatted(adapter.getRpcClientStatus()));
+            gpgnetServerStatus.setText("GPGNetServer: %s".formatted(adapter.getGpgNetServerStatus()));
+            gpgnetClientStatus.setText("GPGNetClient: %s".formatted(adapter.getGpgNetClientStatus()));
+            gameState.setText("GameState: %s".formatted(adapter.getGameState()));
+
             PeerView currentlySelected = peerTable.getSelectionModel().getSelectedItem();
             // ... rest of code
         });
     }

Committable suggestion skipped: line range outside the PR's diff.

ice-adapter/src/main/java/com/faforever/iceadapter/ui/InfoWindow.java-76-83 (1)

76-83: Race condition in launch() singleton initialization.

The check if (INSTANCE == null) and subsequent creation is not thread-safe. If launch() is called concurrently from multiple threads, multiple InfoWindow instances could be created.

Consider synchronizing or using a holder pattern:

🔎 Proposed fix
     public static void launch() {
         log.info("Launching info window.");
-        if (INSTANCE == null) {
-            runOnUIThread(() -> new InfoWindow().start(new Stage()));
-        } else {
-            INSTANCE.showWindow();
-        }
+        runOnUIThread(() -> {
+            if (INSTANCE == null) {
+                new InfoWindow().start(new Stage());
+            } else {
+                INSTANCE.showWindow();
+            }
+        });
     }

This ensures the null check and creation happen on the same thread.

ice-adapter/src/main/java/com/faforever/iceadapter/services/impl/ConnectServiceControlledImpl.java-72-78 (1)

72-78: Missing closing check before scheduling re-init.

tryReInitState schedules a delayed state transition to NEW but doesn't check if the peer is closing. This could cause issues if the peer closes before the delay completes.

🔎 Proposed fix
     private void tryReInitState(Peer peer, IceState oldState) {
+        if (peer.isClosing()) {
+            return;
+        }
         if (oldState == CONNECTED) {
-            iceAsync.runAsyncDelay(peer, () -> peer.setIceState(NEW), 1000);
+            iceAsync.runAsyncDelay(peer, () -> {
+                if (!peer.isClosing()) peer.setIceState(NEW);
+            }, 1000);
         } else {
-            iceAsync.runAsyncDelay(peer, () -> peer.setIceState(NEW), 5000);
+            iceAsync.runAsyncDelay(peer, () -> {
+                if (!peer.isClosing()) peer.setIceState(NEW);
+            }, 5000);
         }
     }
ice-adapter/src/main/java/com/faforever/iceadapter/ice/peer/modules/other/FASocketModule.java-41-55 (1)

41-55: Logging mismatch and redundant @SneakyThrows.

  1. Line 49 logs peer.getLocalPort() but the actual port used is the resolved port variable. If localPort was null, the log will be misleading.

  2. @SneakyThrows(SocketException.class) is unnecessary since the exception is caught and explicitly rethrown within the method.

🔎 Proposed fix
-    @SneakyThrows(SocketException.class)
     private DatagramSocket initForwarding(int port, Integer localPort) {
         try {
             if (localPort != null) {
                 port = localPort;
             }
             DatagramSocket socket = new DatagramSocket(port);
             DatagramSocketUtils.resizeBuffer(socket);
-            log.debug("Now forwarding data to peer {} on port {}", peer.getPeerIdentifier(), peer.getLocalPort());
+            log.debug("Now forwarding data to peer {} on port {}", peer.getPeerIdentifier(), port);
             return socket;
         } catch (SocketException e) {
             log.error("Could not create socket for peer: {}", peer.getPeerIdentifier(), e);
             throw e;
         }
     }
ice-adapter/src/main/java/com/faforever/iceadapter/ice/peer/modules/EventBusModule.java-94-102 (1)

94-102: safeAsyncCall is not actually "safe" - missing exception handling.

The method name suggests exception safety, but exceptions thrown by listeners will propagate uncaught in virtual threads. Consider wrapping the call in a try-catch:

🔎 Proposed fix
     private void safeAsyncCall(PeerEventListener listener, String methodName, Runnable call) {
-        asyncVirtual(methodName, peer, call);
+        asyncVirtual(methodName, peer, () -> {
+            try {
+                call.run();
+            } catch (Exception e) {
+                log.error("Error in listener {} during {}", listener.getClass().getSimpleName(), methodName, e);
+            }
+        });
     }
ice-adapter/src/main/java/com/faforever/iceadapter/ui/InfoWindow.java-30-42 (1)

30-42: INSTANCE set before FXML loading could leave partial state on failure.

If loader.load() throws an exception (line 38), INSTANCE is already set to this (line 31), leaving a partially initialized singleton. Subsequent launch() calls would try to show a broken window.

Consider setting INSTANCE after successful initialization:

🔎 Proposed fix
     public void start(Stage stage) {
-        INSTANCE = this;
         this.stage = stage;
         LogoUtils.getLogoFx().ifPresent(logo -> {
             stage.getIcons().add(logo);
         });
         try {
             FXMLLoader loader = new FXMLLoader(getClass().getResource("/infoWindow.fxml"));
             root = loader.load();
             controller = loader.getController();
         } catch (IOException e) {
             log.error("Could not load debugger window fxml", e);
+            return; // Don't set INSTANCE on failure
         }

         setUserAgentStylesheet(STYLESHEET_MODENA);
         // ... rest of setup
+        INSTANCE = this;

Committable suggestion skipped: line range outside the PR's diff.

ice-adapter/src/main/java/com/faforever/iceadapter/services/impl/ConnectServiceControlledImpl.java-102-111 (1)

102-111: Use peer's AllowCombination settings instead of hardcoded true values.

Both ConnectServiceControlledImpl and ConnectServiceNotControlledImpl call unpackCandidates with all three boolean parameters hardcoded to true. This is inconsistent with ConnectServiceCommon, which filters outgoing candidates using peer.getCombination(). Apply the same combination-based filtering when unpacking incoming candidates for consistency.

ice-adapter/src/main/java/com/faforever/iceadapter/IceAdapter.java-178-204 (1)

178-204: close(int status) accesses instance fields without synchronization.

The method reads INSTANCE into a local variable, but subsequent access to instance.gpgNetServer and instance.rpcService is not synchronized. If another thread modifies these fields concurrently, inconsistent state could result. Consider synchronizing or making these fields final.

ice-adapter/src/main/java/com/faforever/iceadapter/services/impl/ConnectServiceCommon.java-268-281 (1)

268-281: Potential ConcurrentModificationException when iterating and removing streams.

The loop iterates agent.getStreams() while calling removeStream() and removeComponent() inside. Depending on the implementation of getStreams(), this could throw ConcurrentModificationException.

🔎 Proposed fix - copy collection before iterating
     protected void closeAgent(Agent agent) {
         log.info("Close agent");
         try {
-            for (IceMediaStream stream : agent.getStreams()) {
-                for (Component streamComponent : stream.getComponents()) {
+            for (IceMediaStream stream : new ArrayList<>(agent.getStreams())) {
+                for (Component streamComponent : new ArrayList<>(stream.getComponents())) {
                     stream.removeComponent(streamComponent);
                 }
                 agent.removeStream(stream);
             }
             agent.free();
         } catch (Exception e) {
             log.warn("Error freeing existing agent", e);
         }
     }
ice-adapter/src/main/java/com/faforever/iceadapter/ice/IceServer.java-85-89 (1)

85-89: Unchecked cast and incorrect fallback key.

Line 86 performs an unchecked cast to List<String>. Line 88 falls back to iceServerData.get("url") (singular) but the enclosing if checks for "urls" (plural), which means the fallback will never retrieve a value when "urls" key exists.

🔎 Proposed fix
             if (iceServerData.containsKey("urls")) {
                 List<String> urls;
                 Object urlsData = iceServerData.get("urls");
                 if (urlsData instanceof List) {
-                    urls = (List<String>) urlsData;
+                    @SuppressWarnings("unchecked")
+                    List<String> castUrls = (List<String>) urlsData;
+                    urls = castUrls;
                 } else {
-                    urls = Collections.singletonList((String) iceServerData.get("url"));
+                    urls = Collections.singletonList((String) urlsData);
                 }
ice-adapter/src/main/java/com/faforever/iceadapter/services/impl/ConnectServiceCommon.java-206-221 (1)

206-221: Double timeout may cause extended waits.

PeerConnectionSuccessMonitor already has a TIMEOUT_ON_CHECKING timeout (line 209), and then future.get() at line 214 adds another TIMEOUT_ON_CHECKING wait. In the worst case, this could wait up to 30 seconds instead of the intended 15.

🔎 Proposed fix - use longer timeout for get() as safety net
         try {
-            return future.get(TIMEOUT_ON_CHECKING, TimeUnit.MILLISECONDS);
+            // Use a slightly longer timeout as a safety net; monitor handles the actual timeout
+            return future.get(TIMEOUT_ON_CHECKING + 1000, TimeUnit.MILLISECONDS);
         } catch (Exception e) {
ice-adapter/src/main/java/com/faforever/iceadapter/ice/IceServer.java-115-120 (1)

115-120: Default case logs warning but still creates IceServer with TURN type.

When an unknown scheme is encountered, the code logs a warning but proceeds to create an IceServer with type = TypeServer.TURN (initialized at line 115). Consider skipping the entry entirely or explicitly setting a default type after logging.

🔎 Proposed fix to skip unknown schemes
                         TypeServer type = TypeServer.TURN;
                         switch (uri.getScheme()) {
                             case STUN -> type = TypeServer.STUN;
                             case TURNS, TURN -> type = TypeServer.TURN;
-                            default -> log.warn("Invalid ICE server protocol: {}", uri);
+                            default -> {
+                                log.warn("Invalid ICE server protocol: {}", uri);
+                                return; // Skip this URI
+                            }
                         }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +253 to 259

// perform the potentially blocking and cross-module shutdown outside of the lock to avoid deadlocks
if (currentClient.get() != null) {
iceAdapter.onFAShutdown();
debug().gpgnetConnectedDisconnected();
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical logic bug: FA shutdown never triggered.

After currentClient.getAndSet(null) at line 246, the check at line 255 if (currentClient.get() != null) will always be false because you just set it to null. The FA shutdown and debug notification will never execute.

🔎 Proposed fix
     private void onGpgnetConnectionLost() {
         log.info("GPGNet connection lost");
 
+        boolean hadClient = false;
         // remove and close the client under lock
         LockUtil.executeWithLock(lockSocket, () -> {
             GPGNetClient prevClient = currentClient.getAndSet(null);
             if (prevClient != null) {
+                hadClient = true;
                 prevClient.close();
-                // create a fresh lobby future if necessary is handled per-client
                 rpcService.onConnectionStateChanged("Disconnected");
             }
         });
 
         // perform the potentially blocking and cross-module shutdown outside of the lock to avoid deadlocks
-        if (currentClient.get() != null) {
+        if (hadClient) {
             iceAdapter.onFAShutdown();
             debug().gpgnetConnectedDisconnected();
         }
     }

Note: You'll need to use an AtomicBoolean or similar since the variable is accessed from within a lambda.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In ice-adapter/src/main/java/com/faforever/iceadapter/gpgnet/GPGNetServer.java
around lines 253 to 259, the code checks currentClient.get() after calling
currentClient.getAndSet(null), so the condition is always false and FA shutdown
never runs; fix by capturing the previous client state before clearing (e.g.,
Client previous = currentClient.getAndSet(null)) and then if (previous != null)
call iceAdapter.onFAShutdown() and debug().gpgnetConnectedDisconnected();
alternatively, if you need a boolean flag for lambda/thread use, replace or
accompany with an AtomicBoolean to record whether a non-null client was present
and check that flag after the atomic set.

Comment on lines +26 to +54
public CompletableFuture<Boolean> start() {
future = new CompletableFuture<>();
if (peer == null) {
log.error("Peer is null. Aborting.");
future.complete(false);
return future;
}
Agent agent = peer.getAgent();
if (agent == null) {
log.error("Agent is null. Aborting.");
future.complete(false);
return future;
}
IceMediaStream mediaStream = peer.getMediaStream();
if (mediaStream == null) {
log.error("Media stream is null. Aborting.");
future.complete(false);
return future;
}

name = peer.getPeerIdentifier();

agent.addStateChangeListener(this);
mediaStream.addPairChangeListener(this);

scheduledFuture = scheduler.schedule(this::agentTimeoutFailed, timeoutMs, TimeUnit.MILLISECONDS);

return future;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Memory leak: listeners never unregistered; start() not idempotent.

Two critical issues:

  1. Listener leak (lines 48-49): The monitor registers itself as a listener on both agent and mediaStream, but these listeners are never unregistered—not on completion, failure, timeout, or even in shutdown(). If the agent/mediaStream outlive this monitor, these listeners will leak memory.

  2. Not idempotent: If start() is called multiple times, a new future is created each time (line 27), losing the reference to previous futures and their callbacks. Additionally, listeners are registered multiple times without deregistration.

🔎 Proposed fix
+    private Agent agent;
+    private IceMediaStream mediaStream;
+
     @Override
     public CompletableFuture<Boolean> start() {
+        if (future != null && !future.isDone()) {
+            log.warn("start() called while already running. Returning existing future.");
+            return future;
+        }
+        
         future = new CompletableFuture<>();
         if (peer == null) {
             log.error("Peer is null. Aborting.");
             future.complete(false);
             return future;
         }
-        Agent agent = peer.getAgent();
+        agent = peer.getAgent();
         if (agent == null) {
             log.error("Agent is null. Aborting.");
             future.complete(false);
             return future;
         }
-        IceMediaStream mediaStream = peer.getMediaStream();
+        mediaStream = peer.getMediaStream();
         if (mediaStream == null) {
             log.error("Media stream is null. Aborting.");
             future.complete(false);
             return future;
         }

         name = peer.getPeerIdentifier();

         agent.addStateChangeListener(this);
         mediaStream.addPairChangeListener(this);

         scheduledFuture = scheduler.schedule(this::agentTimeoutFailed, timeoutMs, TimeUnit.MILLISECONDS);

+        future.whenComplete((result, ex) -> cleanup());
         return future;
     }
+
+    private void cleanup() {
+        if (scheduledFuture != null && !scheduledFuture.isDone()) {
+            scheduledFuture.cancel(false);
+        }
+        if (agent != null) {
+            agent.removeStateChangeListener(this);
+        }
+        if (mediaStream != null) {
+            mediaStream.removePairChangeListener(this);
+        }
+    }

Also update shutdown() to call cleanup().

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
ice-adapter/src/main/java/com/faforever/iceadapter/ice/PeerConnectionSuccessMonitor.java
around lines 26 to 54, start() currently recreates the CompletableFuture and
re-registers listeners every call and never removes listeners, causing leaks and
non-idempotency; make start() idempotent by returning the existing future if
already started, avoid registering listeners a second time, and only create the
future once. Implement a private cleanup() that unregisters this from agent and
mediaStream, cancels scheduledFuture, and nulls out/cleans state; call cleanup()
on success, failure, timeout and from shutdown(). Ensure scheduledFuture is
cancelled when finished and the future is completed exactly once.


public class CandidateUtil {

public static int candidateIDFactory = 0;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Race condition on shared mutable static field.

candidateIDFactory is a static mutable field incremented without synchronization (line 34). If packCandidates is called concurrently from multiple threads, this will cause race conditions leading to duplicate candidate IDs or lost increments.

🔎 Proposed fix using AtomicInteger

At line 15, replace the field declaration:

-    public static int candidateIDFactory = 0;
+    private static final AtomicInteger candidateIDFactory = new AtomicInteger(0);

At line 34, update the usage:

-                String.valueOf(candidateIDFactory++),
+                String.valueOf(candidateIDFactory.getAndIncrement()),

Add the import at the top of the file:

import java.util.concurrent.atomic.AtomicInteger;

Also applies to: 34-34

🤖 Prompt for AI Agents
In ice-adapter/src/main/java/com/faforever/iceadapter/util/CandidateUtil.java
around lines 15 and 34, the static mutable int candidateIDFactory is incremented
without synchronization causing race conditions; replace the declaration with a
static AtomicInteger (add import java.util.concurrent.atomic.AtomicInteger), and
at the usage site replace the plain increment with atomic getAndIncrement() (or
incrementAndGet() as appropriate) so candidate IDs are produced atomically and
safely under concurrent calls.

Comment on lines +114 to +153
public static String infoCandidate(CandidatePair pair) {
if (pair == null) {
return null;
}
return """
Local Candidate:
Type: %s
Transport: %s
Address: %s:%d
Priority: %d
Foundation: %s
Remote Candidate:
Type: %s
Transport: %s
Address: %s:%d
Priority: %d
Foundation: %s
Priority: %d
Nominated: %s
State: %s
""".formatted(
pair.getLocalCandidate().getType(),
pair.getLocalCandidate().getTransport(),
pair.getLocalCandidate().getTransportAddress().getHostAddress(),
pair.getLocalCandidate().getTransportAddress().getPort(),
pair.getLocalCandidate().getPriority(),
pair.getLocalCandidate().getFoundation(),
pair.getRemoteCandidate().getType(),
pair.getLocalCandidate().getTransport(),
pair.getRemoteCandidate().getTransportAddress().getHostAddress(),
pair.getRemoteCandidate().getTransportAddress().getPort(),
pair.getRemoteCandidate().getPriority(),
pair.getRemoteCandidate().getFoundation(),
pair.getPriority(),
pair.isNominated(),
pair.getState()
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix copy-paste error: displaying wrong transport for remote candidate.

Line 144 incorrectly uses pair.getLocalCandidate().getTransport() in the Remote Candidate section. It should use pair.getRemoteCandidate().getTransport() instead.

🔎 Proposed fix
                 pair.getRemoteCandidate().getType(),
-                pair.getLocalCandidate().getTransport(),
+                pair.getRemoteCandidate().getTransport(),
                 pair.getRemoteCandidate().getTransportAddress().getHostAddress(),
🤖 Prompt for AI Agents
In ice-adapter/src/main/java/com/faforever/iceadapter/util/CandidateUtil.java
around lines 114 to 153, there is a copy-paste bug in the formatted argument
list: the Remote Candidate's Transport value is passed as
pair.getLocalCandidate().getTransport() instead of
pair.getRemoteCandidate().getTransport(); replace that specific argument with
pair.getRemoteCandidate().getTransport() so the Remote Candidate section shows
the correct transport while leaving all other format placeholders and arguments
unchanged.

Comment on lines 30 to +33
if (System.getProperty("os.name").startsWith("Windows")) {
process = new ProcessBuilder("ping", "-n", count.toString(), address).start();
// Force English output using code page 437
String command = String.format("chcp 437 > NUL && ping -n %d %s", count, address);
process = new ProcessBuilder("cmd", "/c", command).start();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Potential command injection vulnerability.

The address parameter is directly interpolated into the shell command string without validation or sanitization. A malicious address like example.com & malicious_command could execute arbitrary commands.

🔎 Proposed fix to prevent command injection
             if (System.getProperty("os.name").startsWith("Windows")) {
                 // Force English output using code page 437
-                String command = String.format("chcp 437 > NUL && ping -n %d %s", count, address);
-                process = new ProcessBuilder("cmd", "/c", command).start();
+                process = new ProcessBuilder("cmd", "/c", "chcp", "437", ">", "NUL", "&&", "ping", "-n", count.toString(), address).start();
                 output_pattern = WINDOWS_OUTPUT_PATTERN;

Even better, separate the commands:

             if (System.getProperty("os.name").startsWith("Windows")) {
                 // Force English output using code page 437
-                String command = String.format("chcp 437 > NUL && ping -n %d %s", count, address);
-                process = new ProcessBuilder("cmd", "/c", command).start();
+                // Set code page in environment instead
+                ProcessBuilder pb = new ProcessBuilder("ping", "-n", count.toString(), address);
+                pb.environment().put("CHCP", "437");
+                process = pb.start();
                 output_pattern = WINDOWS_OUTPUT_PATTERN;

Note: The second approach may require testing to confirm code page handling works as expected.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In ice-adapter/src/main/java/com/faforever/iceadapter/util/PingWrapper.java
around lines 30-33, the address variable is being interpolated into a shell
command which allows command injection; fix by (1) never passing a single
concatenated command string to the shell with untrusted input — run chcp and
ping as separate ProcessBuilder invocations or invoke ping directly without cmd
/c using ProcessBuilder with each argument as a separate element (no shell
parsing), and (2) validate/sanitize the address beforehand (restrict to a safe
regex for IPv4/IPv6 or hostname characters) and reject or throw on invalid
values so only validated addresses reach the ProcessBuilder.

Comment on lines +18 to +20
implementation("ch.qos.logback:logback-classic:1.5.6")
implementation("org.slf4j:slf4j-api:2.0.13")

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for latest versions of logging dependencies

echo "=== Checking logback-classic latest version ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:ch.qos.logback+AND+a:logback-classic&rows=1&wt=json" | jq -r '.response.docs[0].latestVersion' 2>/dev/null || echo "Failed to fetch"

echo ""
echo "=== Checking slf4j-api latest version ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:org.slf4j+AND+a:slf4j-api&rows=1&wt=json" | jq -r '.response.docs[0].latestVersion' 2>/dev/null || echo "Failed to fetch"

echo ""
echo "=== Checking recent versions of logback-classic ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:ch.qos.logback+AND+a:logback-classic&rows=5&wt=json" | jq -r '.response.docs[] | .v' 2>/dev/null | head -10

echo ""
echo "=== Checking recent versions of slf4j-api ==="
curl -s "https://search.maven.org/solrsearch/select?q=g:org.slf4j+AND+a:slf4j-api&rows=5&wt=json" | jq -r '.response.docs[] | .v' 2>/dev/null | head -10

Repository: FAForever/java-ice-adapter

Length of output: 196


🌐 Web query:

logback-classic latest version 2025 Maven Central

💡 Result:

The latest logback-classic on Maven Central is ch.qos.logback:logback-classic:1.5.22 (published Dec 11, 2025). [1][2]

Sources:

  • Maven Central directory for logback-classic (shows 1.5.22). [1]
  • Logback project download/setup pages. [2]

🌐 Web query:

slf4j-api latest version 2025 Maven Central

💡 Result:

Latest stable slf4j-api on Maven Central: 2.0.17 (released Feb 25, 2025). [1][2]

Sources:
[1] SLF4J download page (stable: 2.0.17).
[2] Maven Central repository metadata for org.slf4j:slf4j-api.


🌐 Web query:

logback-classic 1.5.6 security vulnerabilities CVE

💡 Result:

Short answer: Yes — logback 1.5.6 is vulnerable to CVE-2025-11226 (arbitrary code execution in logback-core) and is in the affected range for that issue. Older serialization/DoS CVEs (CVE-2023-6378 / CVE-2023-6481) affected earlier 1.2/1.3/1.4 series but are separate. [1][2][3][4]

Details/sources:

  • CVE-2025-11226 (ACE in conditional configuration processing) — affects logback-core up to and including 1.5.18 (so 1.5.6 is vulnerable). [NVD]. [1][2]
  • CVE-2023-6378 and CVE-2023-6481 — serialization/DoS issues in receiver components affecting 1.2/1.3/1.4 series (separately disclosed in 2023). [3][4]
  • Maven listing for logback-classic 1.5.6 (shows known vulnerability references). [5]

References:
[1] NVD / CVE-2025-11226. (nvd.nist.gov)
[2] GitHub Advisory / CVE-2025-11226. (github.com)
[3] GitHub Advisory / CVE-2023-6481 (serialization/DoS). (github.com)
[4] OSV / logback security bulletins (CVE-2023-6378, CVE-2023-6481). (osv.dev)
[5] Maven Repository — logback-classic 1.5.6 metadata. (mvnrepository.com)

If you want, I can (a) list exact affected version ranges per CVE, (b) show mitigation/workaround and fixed versions, or (c) produce a patch/upgrade plan.

Citations:


🌐 Web query:

slf4j-api 2.0.13 security vulnerabilities CVE

💡 Result:

Short answer: I found no published CVE that specifically names slf4j-api 2.0.13. [1][2]

Notes and relevant references:

  • SLF4J release notes for 2.0.13 (no CVE listed for slf4j-api 2.0.13). [1]
  • Historical CVE affecting SLF4J components: CVE‑2018‑8088 (slf4j-ext) — fixed in later 1.7.x and in the 2.0.x series. This CVE is not specific to slf4j-api 2.0.13. [2]
  • Many CVEs in the Java logging ecosystem (e.g., Log4j-related CVEs, Jackson issues) can affect applications that use SLF4J depending on the bound implementation; check your logging binding (log4j, logback, etc.) and transitive dependencies. See SLF4J guidance on Log4Shell and ecosystem advisories. [3][4]

If you want, I can:

  • run a targeted NVD/CVE feed lookup for a specific date range or CVE IDs, or
  • scan your project's dependency tree to show whether any vulnerable logging implementations or transitive libraries are present.

Sources:
[1] SLF4J news / release 2.0.13.
[2] GitLab advisory / CVE‑2018‑8088 (slf4j‑ext).
[3] SLF4J guidance on Log4Shell and related risks.
[4] Snyk package advisories for SLF4J/log4j-slf4j related packages.


🌐 Web query:

logback-classic 1.5.6 slf4j-api 2.0.13 compatibility

💡 Result:

Short answer: Yes — logback-classic 1.5.6 (part of Logback 1.5.x) is intended to work with SLF4J 2.0.x, so it is compatible with slf4j-api 2.0.13. Use matching SLF4J major/minor (2.0) artifacts and avoid mixing different SLF4J major/minor versions. [1][2][3]

Sources:

  • Logback dependencies / version matrix (Logback 1.5.x → SLF4J 2.0). [1]
  • SLF4J FAQ / compatibility notes (providers must match SLF4J major.minor; 2.0.x details). [2]
  • Maven Central listing for slf4j-api 2.0.13. [3]

Upgrade logging dependencies immediately due to CVE-2025-11226.

The logback-classic version 1.5.6 is vulnerable to CVE-2025-11226 (arbitrary code execution in conditional configuration processing). Upgrade to logback-classic 1.5.19 or later (current: 1.5.22). Also update slf4j-api to 2.0.17 (from 2.0.13). Both dependencies are compatible with each other.

🤖 Prompt for AI Agents
In server/build.gradle around lines 18 to 20, the project depends on vulnerable
logback-classic 1.5.6 and an older slf4j-api (2.0.13); update logback-classic to
a non-vulnerable release (>=1.5.19) — e.g. 1.5.22 — and bump slf4j-api to 2.0.17
so the two are compatible, then run a build and/or `./gradlew dependencies` to
verify the resolved versions and ensure no dependency conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant