diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index dbd9d464..ffba61e6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -4,11 +4,11 @@ name: "CodeQL" on: - # Scan only for push on the primary branch for now - push: - branches: [ main ] - pull_request: - branches: [ main ] + workflow_run: + workflows: ["Maven Build"] + branches: [main] + types: + - completed jobs: # job i @@ -20,6 +20,8 @@ jobs: # Environment runs-on: ubuntu-latest + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: actions: read contents: read @@ -35,24 +37,25 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - # Setup Java - - name: Set up JDK - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: 19 - cache: 'maven' - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} - # Auto build attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 + # Setup Java + - name: Set up JDK + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version-file: '.java-version' + cache: 'maven' + + # Manually start the autobuild process, because autobuild always selects Java 8 as build toolchain, but + # we are doing cross-crompiling from a newer Java version + - name: Build with Maven + # Extracted from autobuild + run: mvn package -f "pom.xml" --batch-mode -V -e -Dfindbugs.skip -Dcheckstyle.skip -Dpmd.skip=true -Dspotbugs.skip -Denforcer.skip -Dmaven.javadoc.skip -DskipTests -Dmaven.test.skip.exec -Dlicense.skip=true -Drat.skip=true -Dspotless.check.skip=true -t /home/runner/.m2/toolchains.xml - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index e30e67f7..f8798a15 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -21,7 +21,7 @@ jobs: # Environment image - always use the newest OS runs-on: ubuntu-latest permissions: - contents: read + contents: write # Run steps steps: @@ -33,7 +33,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: 19 + java-version-file: '.java-version' cache: 'maven' # Build and test (included in package) @@ -41,3 +41,7 @@ jobs: # Run non-interactive, package (with compile+test), # ignore snapshot updates, because they are likely to have breaking changes, enforce checksums run: mvn test --batch-mode --threads 2.0C --no-snapshot-updates --strict-checksums --file pom.xml + + - name: Update dependency graph + if: ${{ github.event_name == 'push' }} + uses: advanced-security/maven-dependency-submission-action@v3.0.2 diff --git a/.java-version b/.java-version new file mode 100644 index 00000000..98d9bcb7 --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +17 diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/InetUtils.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/InetUtils.java new file mode 100644 index 00000000..78600a4f --- /dev/null +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/InetUtils.java @@ -0,0 +1,63 @@ +/* + * SPDX-License-Identifier: MIT + * + * The MIT License (MIT) + * + * Copyright (c) 2015-2023 games647 and contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.github.games647.fastlogin.bukkit; + +import java.net.InetAddress; + +public final class InetUtils { + + private InetUtils() { + // Utility + } + + /** + * Verifies if the given IP address is from the local network + * + * @param address IP address + * @return true if address is from local network or even from the device itself (loopback) + */ + public static boolean isLocalAddress(InetAddress address) { + // Loopback addresses like 127.0.* (IPv4) or [::1] (IPv6) + return address.isLoopbackAddress() + // Example: 10.0.0.0, 172.16.0.0, 192.168.0.0, fec0::/10 (deprecated) + // Ref: https://en.wikipedia.org/wiki/IP_address#Private_addresses + || address.isSiteLocalAddress() + // Example: 169.254.0.0/16, fe80::/10 + // Ref: https://en.wikipedia.org/wiki/IP_address#Address_autoconfiguration + || address.isLinkLocalAddress() + // non deprecated unique site-local that java doesn't check yet -> fc00::/7 + || isIPv6UniqueSiteLocal(address); + } + + private static boolean isIPv6UniqueSiteLocal(InetAddress address) { + // ref: https://en.wikipedia.org/wiki/Unique_local_address + + // currently undefined but could be used in the near future fc00::/8 + return (address.getAddress()[0] & 0xFF) == 0xFC + // in use for unique site-local fd00::/8 + || (address.getAddress()[0] & 0xFF) == 0xFD; + } +} diff --git a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java index ef0bda01..3b8ba923 100644 --- a/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java +++ b/bukkit/src/main/java/com/github/games647/fastlogin/bukkit/listener/protocollib/VerifyResponseTask.java @@ -46,6 +46,7 @@ import com.github.games647.craftapi.model.skin.SkinProperty; import com.github.games647.craftapi.resolver.MojangResolver; import com.github.games647.fastlogin.bukkit.BukkitLoginSession; import com.github.games647.fastlogin.bukkit.FastLoginBukkit; +import com.github.games647.fastlogin.bukkit.InetUtils; import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey; import lombok.val; import org.bukkit.entity.Player; @@ -71,10 +72,17 @@ public class VerifyResponseTask implements Runnable { private static final String ENCRYPTION_CLASS_NAME = "MinecraftEncryption"; private static final Class ENCRYPTION_CLASS; + private static final String ADDRESS_VERIFY_WARNING = "This indicates the use of reverse-proxy like HAProxy, " + + "TCPShield, BungeeCord, Velocity, etc. " + + "By default (configurable in the config) this plugin requests Mojang to verify the connecting IP " + + "to this server with the one used to log into Minecraft to prevent MITM attacks. In " + + "order to work this security feature, the actual client IP needs to be forwarding " + + "(keyword IP forwarding). This process will also be useful for other server " + + "features like IP banning, so that it doesn't ban the proxy IP."; static { ENCRYPTION_CLASS = MinecraftReflection.getMinecraftClass( - "util." + ENCRYPTION_CLASS_NAME, ENCRYPTION_CLASS_NAME + "util." + ENCRYPTION_CLASS_NAME, ENCRYPTION_CLASS_NAME ); } @@ -149,10 +157,27 @@ public class VerifyResponseTask implements Runnable { } else { //user tried to fake an authentication disconnect( - "invalid-session", - "GameProfile {} ({}) tried to log in with an invalid session. ServerId: {}", - session.getRequestUsername(), socketAddress, serverId + "invalid-session", + "Session server rejected incoming connection for GameProfile {} ({}). Possible reasons are" + + "1) Client IP address contacting Mojang and server during server join were different " + + "(Do you use a reverse proxy? -> Enable IP forwarding, " + + "or disable the feature in the config). " + + "2) Player is offline, but tried to bypass the authentication" + + "3) Client uses an outdated username for connecting (Fix: Restart client)", + requestedUsername, address ); + + if (InetUtils.isLocalAddress(address)) { + plugin.getLog().warn( + "The incoming request for player {} uses a local IP address", + requestedUsername + ); + plugin.getLog().warn(ADDRESS_VERIFY_WARNING); + } else { + plugin.getLog().warn("If you think this is an error, please verify that the incoming " + + "IP address {} is not associated with a server hosting company.", address); + plugin.getLog().warn(ADDRESS_VERIFY_WARNING); + } } } catch (IOException ioEx) { disconnect("error-kick", "Failed to connect to session server", ioEx); @@ -217,15 +242,15 @@ public class VerifyResponseTask implements Runnable { try { // Try to get the old (pre MC 1.16.4) encryption method encryptMethod = FuzzyReflection.fromClass(networkManagerClass) - .getMethodByParameters("a", SecretKey.class); + .getMethodByParameters("a", SecretKey.class); } catch (IllegalArgumentException exception) { // Get the new encryption method encryptMethod = FuzzyReflection.fromClass(networkManagerClass) - .getMethodByParameters("a", Cipher.class, Cipher.class); + .getMethodByParameters("a", Cipher.class, Cipher.class); // Get the needed Cipher helper method (used to generate ciphers from login key) cipherMethod = FuzzyReflection.fromClass(ENCRYPTION_CLASS) - .getMethodByParameters("a", int.class, Key.class); + .getMethodByParameters("a", int.class, Key.class); } } @@ -276,7 +301,7 @@ public class VerifyResponseTask implements Runnable { EquivalentConverter converter = BukkitConverters.getWrappedPublicKeyDataConverter(); val wrappedKey = Optional.ofNullable(clientKey).map(key -> - new WrappedProfileKeyData(clientKey.expiry(), clientKey.key(), clientKey.signature()) + new WrappedProfileKeyData(clientKey.expiry(), clientKey.key(), clientKey.signature()) ); startPacket.getOptionals(converter).write(0, wrappedKey);