A client mod that fixes LAN world discovery on machines with multiple network interfaces (VMs, VPNs, virtual adapters, Docker/WSL bridges, etc.). Built for Fabric, NeoForge, and Forge, across Minecraft 1.14.4 through 1.21.11 plus the newer 26.x releases.
This is a known, long-standing Mojang bug - see MC-98598 ("LAN List does not populate reliably", confirmed, Category: Networking, open since 2016 with ~35 duplicate reports going back to 1.9). This mod works around it client-side until Mojang fixes it upstream.
Minecraft's client-side LAN scanner
(net.minecraft.client.server.LanServerDetection$LanServerDetector) joins its multicast
group with the legacy MulticastSocket#joinGroup(InetAddress) overload, which does not let
you choose a network interface — it silently uses whatever the OS/JVM picks as "default".
On machines with many virtual adapters (VMware/VirtualBox host-only networks, VPN tunnels,
Docker bridges, WSL, etc.) that is frequently not the real LAN adapter, so "Share to LAN"
worlds hosted on other machines never show up in the Multiplayer screen. This class and its
fields have been structurally unchanged since at least 1.14.4, so a single fix works across
every loader and version listed below.
This matches the pattern across MC-98598's
duplicates almost exactly: it works on some machines and not others on the same network,
it comes and goes with OS reinstalls or adapter changes, and direct-connect-by-IP always
works even when the LAN list stays empty (direct connect is ordinary unicast TCP, unaffected
by multicast group membership). That's the signature of an interface-selection bug, not a
protocol regression — the sending side (LanServerPinger) just sends plain UDP broadcast and
isn't affected the same way, which is also why this has stayed hard to pin down: it only
reproduces on machines with more than one active network interface.
A single Mixin (LanServerDetectorMixin) redirects that joinGroup call. Instead of relying
on the default interface, it enumerates all network interfaces, filters out ones that
clearly aren't usable (down, loopback, point-to-point, no multicast support, no IPv4
address), and explicitly joins the LAN discovery multicast group via
joinGroup(SocketAddress, NetworkInterface) on every interface that's left — not just one
"best" pick, and deliberately not filtered by adapter name or type beyond that: a VM
host-only network, VPN, or container bridge can be a legitimate LAN too if that's genuinely
where the player is hosting or joining from. If no interface passes the basic checks, it
falls back to the original vanilla behavior.
See LanServerDetectorMixin
and LanInterfaceSelector.
| Minecraft | Fabric | NeoForge | Forge |
|---|---|---|---|
| 1.14.4 | ✅ | n/a (NeoForge starts at 1.20.2) | ❌ blocked - see below |
| 1.16.5 | ✅ | n/a | ❌ blocked - see below |
| 1.18.2 | ✅ | n/a | ✅ |
| 1.19.4 | ✅ | n/a | ✅ |
| 1.20.4 | ✅ | ✅ | ✅ |
| 1.21.1 | ✅ | ✅ | ✅ |
| 1.21.11 | ✅ | ✅ | ✅ |
| 26.2 (whole 26.x line) | ✅ | ✅ (beta NeoForge build) | not attempted |
Quilt: not built separately. Quilt Loader has a built-in Fabric-compatibility layer (QSL) and can load Fabric mod jars directly; this mod only uses a Mixin and a basic client init hook, well within what that layer supports. Use the Fabric jar for the matching Minecraft version.
Forge 1.14.4 and 1.16.5 are currently blocked, unlike every other target: the Mixin
annotation processor reports Mixin target ... could not be found for
LanServerDetection$LanServerDetector specifically on these two versions' ForgeGradle-6
compile classpath, even though the exact same class resolves fine on 1.18.2-1.20.4 (same FG6
generation) and the class is confirmed present in both versions' official Mojang mappings.
Forcing the Java toolchain to 17 instead of 8 didn't change the outcome, so it isn't a
Java-8-toolchain issue - the cause is still open. 1.12.2 (ForgeGradle 3, a different legacy
apply plugin build style entirely) hasn't been attempted at all. 26.2 hasn't been checked
for Forge (unclear whether a Forge build for the 26.x line exists yet upstream).
Fabric and NeoForge don't support Minecraft versions before 1.14/1.20.2 respectively - no mod-loading hooks exist that early. Forge itself goes back to Minecraft 1.1, but versions before roughly 1.12.2 predate Mixin support in Forge entirely (would need a hand-written ASM coremod) and rely on MCP/SRG mapping infrastructure that mostly no longer exists online - those are out of scope here.
Getting any Forge target working required working around several real bugs/limitations in the current ForgeGradle releases, worth knowing if you touch these builds:
- ForgeGradle 7 (1.21+) rejects Gradle 9.5 outright ("Versions Gradle 9.0 and newer are
not supported yet"), but its own plugin-entry gate separately requires Gradle ≥9.3.0.
The only version that satisfies both of its own contradictory checks is the 9.3.x line -
that's why
forge/pins Gradle 9.3.0 specifically, not the newer 9.5.1 the root build uses. - ForgeGradle 6 (1.12.2-1.20.6) unconditionally refuses any Gradle ≥9.0, so it needs its
own separate Gradle 8.x build entirely - see
forge-legacy-*/. - ForgeGradle 6 on Gradle 8.14.5 crashes (
ObjectInstantiationExceptioninDefaultToolchainSpec) if the Gradle daemon itself runs under a JDK newer than 21 (e.g. JDK 25) - unrelated to which JDK your build actually targets.JAVA_HOMEmust point at a JDK 21 (or older, back to whatever Gradle 8.14.5 supports) when invoking./gradlewin anyforge-legacy-*directory, even though the compiled mod itself may target Java 8. - ForgeGradle 6's Minecraft-artifact generation is not safe to share across projects: if
two ForgeGradle-6-configured projects are evaluated in the same Gradle daemon (e.g. as
subprojects of one multi-project build), the artifact generation for one version silently
fails to produce a resolvable dependency for another. Each FG6 version needs its own fully
separate Gradle root (own
settings.gradle, not just its own subproject) - that's why there's aforge-legacy-<version>/directory per version instead of one sharedforge-legacy/multi-project build. - Related to the above: ForgeGradle 6's global artifact cache under
~/.gradle/caches/forge_gradlecan end up in a state where a different Minecraft version's build leaves it unable to resolve/regenerate an already-built version's artifact. If aforge-legacy-*build fails withCould not find net.minecraftforge:forge:...-...-...-_mapped_official_..., delete~/.gradle/caches/forge_gradleand retry - this was reliable in testing, but means building multiple FG6 versions back-to-back isn't fully safe without clearing that cache between them.
The mixin/mod source is identical across every loader and version above (the LAN scanner
hasn't changed), so it lives once in common/:
common/src/main/java/... shared mod source: mixin, interface selector, LanFinderFix (logger/constants)
common/src/main/resources/ shared lan-finder-fix.mixins.json (templated per version)
fabric/src/main/java/... Fabric entrypoint (ClientModInitializer)
fabric/src/main/resources/ fabric.mod.json (templated)
neoforge/src/main/java/... NeoForge entrypoint (@Mod)
neoforge/src/main/resources/ neoforge.mods.toml (templated)
forge/src/main/java/... Forge entrypoint (@Mod) - shared by both Forge sub-builds
forge/src/main/resources/ mods.toml (templated)
versions/remap.gradle shared Fabric build logic, obfuscated releases (Mojang mappings)
versions/noremap.gradle shared Fabric build logic, unobfuscated releases (26.x, no mappings)
versions/neoforge.gradle shared NeoForge build logic (ModDevGradle)
versions/<mc>-fabric/ one subproject per Fabric target: build.gradle + gradle.properties
versions/<mc>-neoforge/ one subproject per NeoForge target: build.gradle + gradle.properties
Forge is split into two separate Gradle builds, neither part of the root build - see "ForgeGradle quirks" above for why:
forge/ ForgeGradle 7 (1.21+), own settings.gradle/wrapper (Gradle 9.3.0)
forge/versions/<mc>/ one subproject per FG7 target
forge-legacy-<mc>/ ForgeGradle 6 (1.18.2-1.20.4), one fully independent Gradle
root PER version, own settings.gradle/wrapper (Gradle 8.14.5)
- build-logic.gradle and build.gradle live directly in each
Each <version>/build.gradle just declares the right plugin and applies the matching shared
script; gradle.properties next to it (or in the same directory, for forge-legacy-*)
supplies the version-specific values (Minecraft/loader version, Java target, Mixin
compatibility level, dependency version ranges).
./gradlew build # Fabric + NeoForge, every version
cd forge && ./gradlew build # Forge 1.21.1 / 1.21.11 (needs Gradle 9.3.0, auto-fetched by the wrapper)
cd forge-legacy-1.20.4 && JAVA_HOME=<path to a JDK 21 or older> ./gradlew build # Forge 1.18.2/1.19.4/1.20.4, one at a timeEach target produces its own jar, e.g. versions/1.21.11-fabric/build/libs/lan-finder-fix-1.21.11-fabric-1.0.0.jar,
forge/versions/1.21.11/build/libs/lan-finder-fix-1.21.11-forge-1.0.0.jar, or
forge-legacy-1.20.4/build/libs/lan-finder-fix-1.20.4-forge-1.0.0.jar. Install whichever one
matches your game version and loader. To build just one Fabric/NeoForge target:
./gradlew :v1_21_11_fabric:build
./gradlew :v1_21_11_neoforge:build(subproject names replace dots with underscores - see the respective settings.gradle)
- One of the Minecraft versions/loaders listed above
- Fabric Loader >= 0.19.3 / NeoForge / Forge, matching your target
- Fabric API (Fabric only)
- Java 8, 17, 21, or 25 depending on target (see the loader-specific
gradle.propertiesfiles) - but see "ForgeGradle quirks" above:forge-legacy-*builds needJAVA_HOMEset to JDK 21 or older regardless of the target's own Java version, due to a ForgeGradle 6 bug
.github/workflows/release.yml builds every target above
and publishes each jar to both CurseForge
and Modrinth whenever a GitHub Release is
published (or manually via "Run workflow" - use its dry_run input to build and resolve
everything without actually uploading). scripts/publish.py does the
actual uploading, driven by scripts/targets.json (one entry per
loader/version combination, matching the "Supported loaders and versions" table above -
Forge 1.14.4/1.16.5 are deliberately left out since that build is currently broken).
The mod version published is the release's tag name (a leading v is stripped, e.g. tag
v1.0.1 publishes as 1.0.1), and the changelog is the release's description text. Both
get passed straight through to -Pmod_version=... for every Gradle build and to the
CurseForge/Modrinth API calls.
Setup: CURSEFORGE_API_TOKEN, MODRINTH_API_TOKEN, and CURSEFORGE_PROJECT_ID (the
numeric ID CurseForge shows on the project's authors page, e.g.
authors.curseforge.com/#/projects/<id>/general) all exist as repo secrets (Settings →
Secrets and variables → Actions → Secrets) - CurseForge's project ID isn't actually
sensitive, but it's kept alongside the tokens for simplicity. The Modrinth project slug is
hardcoded in the workflow as lan-finder-fix; change it there if that project ends up under
a different slug.