Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b208f77
fix(compression): consume unrecognized handshake frames once
pjscruggs Oct 27, 2026
f838fa0
build: update the toolchain for Minecraft 26
pjscruggs Oct 27, 2026
2350810
fix(fabric): adapt encryption and protocol transition hooks
pjscruggs Oct 27, 2026
8875077
feat(fabric): add deobfuscated Minecraft 26 artifacts
pjscruggs Oct 27, 2026
76d2caf
fix(proxy): add and regenerate Minecraft 26 channel mappings
pjscruggs Oct 27, 2026
6b03951
Release SYNC FrameData in SynchronizationLayer.channelRead
netherguy4 Oct 31, 2026
e8205cf
fix(sync): merge frame release fix from PR #85
pjscruggs Oct 31, 2026
93f9fc3
refactor(fabric): separate modern sources and official mappings
pjscruggs Nov 5, 2026
d9eca3b
build(fabric): decouple artifact and release configuration
pjscruggs Nov 5, 2026
1d31d14
feat(encryption): support independent inbound and outbound ciphers
pjscruggs Nov 5, 2026
b89e955
feat(fabric): support 26.4 Snapshot 7 in the modern artifact
pjscruggs Nov 5, 2026
c636a53
feat(protocol): add 26.4 Snapshot 7 channel mappings
pjscruggs Nov 5, 2026
5bc72cc
build(protocol): include Snapshot 7 in mapping generation
pjscruggs Nov 5, 2026
8cbc1e7
refactor(fabric): remove obsolete modern mixin scaffolding
pjscruggs Nov 5, 2026
fd933f4
refactor(fabric): centralize modern version checks
pjscruggs Nov 5, 2026
c0c9ffc
docs(compat): explain version and transport invariants
pjscruggs Nov 5, 2026
3781b21
fix(ci): keep generated mapping checks reproducible
pjscruggs Nov 5, 2026
5d0958d
test(ci): exercise Linux server transports across modern versions
pjscruggs Nov 5, 2026
6510fdb
fix(ci): validate snapshot status display names and protocols
pjscruggs Nov 5, 2026
bff9d35
fix(ci): invalidate stale mapping launchers and require fresh output
pjscruggs Nov 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions .github/scripts/server-smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import argparse
import json
import queue
import socket
import struct
import subprocess
import sys
import threading
import time
import urllib.request
from pathlib import Path
import shutil


MAGIC = bytes.fromhex('00ffff00fefefefefdfdfdfd12345678')
PROTOCOLS = {'26.1': 775, '26.1.1': 775, '26.1.2': 775, '26.2': 776, '26.3': 777,
'26.4-snapshot-7': 1073742169}


def varint(value):
value &= 0xffffffff
result = bytearray()
while value > 127:
result.append((value & 127) | 128)
value >>= 7
return bytes(result + bytes([value]))


def read_varint(stream):
value = 0
for shift in range(0, 35, 7):
part = stream.read(1)
if not part:
raise EOFError('Incomplete VarInt')
value |= (part[0] & 127) << shift
if part[0] < 128:
return value
raise ValueError('VarInt exceeds five bytes')


def check_tcp(port, version):
host = b'127.0.0.1'
handshake = b'\x00' + varint(PROTOCOLS[version]) + varint(len(host)) + host + struct.pack('>H', port) + b'\x01'
with socket.create_connection(('127.0.0.1', port), timeout=10) as connection:
connection.sendall(varint(len(handshake)) + handshake + b'\x01\x00')
stream = connection.makefile('rb')
length = read_varint(stream)
assert 0 < length < 1048576, length
assert read_varint(stream) == 0
size = read_varint(stream)
assert 0 < size < length
status = json.loads(stream.read(size))
assert status['version']['name'] == version.replace('-snapshot-', ' Snapshot '), status
assert status['version']['protocol'] == PROTOCOLS[version], status


def check_udp(port):
# Open Connection Request 1 negotiates the RakNet transport before Minecraft login.
request = (b'\x05' + MAGIC + b'\x09').ljust(1200, b'\x00')
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as connection:
connection.settimeout(10)
connection.connect(('127.0.0.1', port))
connection.send(request)
reply = connection.recv(8192)
assert len(reply) == 28 and reply[0] == 6, reply.hex()
assert reply[1:17] == MAGIC and reply[25] == 0, reply.hex()
assert 128 <= struct.unpack('>H', reply[26:28])[0] <= 1200


def start_server(folder, log, events):
process = subprocess.Popen(['java', '-Xms256M', '-Xmx1G', '-jar', 'fabric-server-launch.jar', 'nogui'],
cwd=folder, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace')

def collect():
with log.open('w', encoding='utf-8') as output:
for line in process.stdout:
output.write(line)
output.flush()
events.put(line)

reader = threading.Thread(target=collect)
reader.start()
return process, reader


def check_server(folder, version, native):
port = 25871
(folder / 'eula.txt').write_text('eula=true\n')
(folder / 'server.properties').write_text(
'server-ip=127.0.0.1\nserver-port=25871\nonline-mode=false\n'
'view-distance=2\nsimulation-distance=2\nspawn-chunk-radius=0\n'
'level-type=minecraft:flat\nmax-players=2\n'
'use-native-transport=' + str(native).lower() + '\nlevel-name=world-' + str(native).lower() + '\n')
log = folder / ('native.log' if native else 'nio.log')
events = queue.Queue()
process, reader = start_server(folder, log, events)
try:
deadline = time.monotonic() + 180
while time.monotonic() < deadline:
if process.poll() is not None:
raise RuntimeError('Server exited before readiness: ' + str(process.returncode))
try:
if 'Done (' in events.get(timeout=1):
break
except queue.Empty:
pass
else:
raise TimeoutError('Server startup exceeded 180 seconds')
for attempt in range(10):
try:
check_tcp(port, version)
break
except (EOFError, OSError):
if attempt == 9:
raise
time.sleep(0.5)
check_udp(port)
dump = subprocess.check_output(['jcmd', str(process.pid), 'Thread.print'], text=True)
(folder / ('native-threads.log' if native else 'nio-threads.log')).write_text(dump)
transport = 'Epoll' if native and sys.platform.startswith('linux') else 'NIO'
assert 'Netty Server ' + transport + ' Raknetify' in dump, transport
process.stdin.write('stop\n')
process.stdin.flush()
assert process.wait(timeout=45) == 0
reader.join(timeout=10)
text = log.read_text()
for failure in ('Critical injection failure', 'Mixin apply failed', 'NoClassDefFoundError', 'LEAK:'):
assert failure not in text, failure
print('PASS', version, transport, 'startup, TCP status, RakNet UDP response, shutdown', flush=True)
finally:
if process.poll() is None:
try:
process.stdin.write('stop\n')
process.stdin.flush()
process.wait(timeout=45)
except (OSError, subprocess.TimeoutExpired):
process.kill()
process.wait(timeout=10)
reader.join(timeout=10)


def main():
parser = argparse.ArgumentParser()
parser.add_argument('--jar', type=Path, required=True)
parser.add_argument('--work-dir', type=Path, required=True)
parser.add_argument('--versions', nargs='+', default=['26.1', '26.1.1', '26.1.2', '26.2', '26.3', '26.4-snapshot-7'])
args = parser.parse_args()
for version in args.versions:
folder = args.work_dir / version
(folder / 'mods').mkdir(parents=True, exist_ok=True)
shutil.copy2(args.jar, folder / 'mods' / args.jar.name)
launcher = folder / 'fabric-server-launch.jar'
if not launcher.exists():
url = f'https://meta.fabricmc.net/v2/versions/loader/{version}/0.20.1/1.1.1/server/jar'
with urllib.request.urlopen(url, timeout=60) as response:
launcher.write_bytes(response.read())
check_server(folder, version, True)
check_server(folder, version, False)


if __name__ == '__main__':
main()
12 changes: 10 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
matrix:
# Use these Java versions
java: [
21, # Current Java LTS & minimum supported by Minecraft
25, # Required by Minecraft 26.x
]
# and run on both Linux and Windows
os: [ubuntu-24.04]
Expand Down Expand Up @@ -42,6 +42,14 @@ jobs:
${{ runner.os }}-gradle-
- name: build
run: ./gradlew build
- name: Linux server transports
run: python3 .github/scripts/server-smoke.py --jar fabric-26/build/libs/*-all.jar --work-dir "$RUNNER_TEMP/raknetify-smoke"
- name: capture server smoke logs
if: always()
uses: actions/upload-artifact@v4
with:
name: Linux-server-smoke-logs
path: ${{ runner.temp }}/raknetify-smoke/**/*.log
- name: upload to modrinth and curseforge
run: ./gradlew modrinth publishMainPublicationToCurseForge
if: github.ref == 'refs/heads/master'
Expand All @@ -51,7 +59,7 @@ jobs:
GITHUB_EVENT_RAW_PATH: ${{ github.event_path }}
continue-on-error: true
- name: capture build artifacts
if: ${{ runner.os == 'Linux' && matrix.java == '21' }} # Only upload artifacts built from latest java on one OS
if: ${{ runner.os == 'Linux' && matrix.java == '25' }} # Only upload artifacts built from latest java on one OS
uses: actions/upload-artifact@v4
with:
name: Artifacts
Expand Down
5 changes: 2 additions & 3 deletions .github/workflows/checkMappings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
matrix:
# Use these Java versions
java: [
21, # Current Java LTS & minimum supported by Minecraft
25, # Required by Minecraft 26.x
]
# and run on both Linux and Windows
os: [ubuntu-24.04]
Expand Down Expand Up @@ -49,8 +49,7 @@ jobs:
run: ./genMappings.sh
- name: fail if mappings changed
run: |
git update-index --refresh
if ! git diff-index --quiet HEAD --; then
if ! git diff --exit-code --; then
echo "Mappings changed, please commit them"
git diff
git diff --staged
Expand Down
2 changes: 1 addition & 1 deletion Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pipeline {
}
stage('Build') {
tools {
jdk "OpenJDK 21"
jdk "OpenJDK 25"
}
steps {
withMaven(
Expand Down
17 changes: 15 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
plugins {
id 'fabric-loom' version '1.13-SNAPSHOT' apply false
id 'fabric-loom' version '1.17.21' apply false
id 'net.fabricmc.fabric-loom' version '1.17.21' apply false
id 'com.gradleup.shadow' version '9.2.1' apply false
id 'dev.yumi.gradle.licenser' version '2.1.1' apply false
id 'dev.yumi.gradle.licenser' version '4.0.0' apply false
id 'com.modrinth.minotaur' version '2.+' apply false
id 'io.github.themrmilchmann.curseforge-publish' version '0.8.0' apply false
}

ext.remapJarTaskType = net.fabricmc.loom.task.RemapJarTask
ext.curseforgeAlpha = io.github.themrmilchmann.gradle.publish.curseforge.ReleaseType.ALPHA
ext.curseforgeMarkdown = io.github.themrmilchmann.gradle.publish.curseforge.ChangelogFormat.MARKDOWN

subprojects {
apply plugin: 'java'
apply plugin: 'java-library'
Expand All @@ -18,8 +23,10 @@ subprojects {
}

version = project.mod_version + "." + getVersionSuffix()
if (project.name == 'fabric-26') version += '.mc26'

license {
include '**/*.java'
rule(rootProject.file('HEADER.txt'))
}

Expand Down Expand Up @@ -62,6 +69,12 @@ subprojects {
versionType = "alpha" // This is the default -- can also be `beta` or `alpha`
// uploadFile = remapShadowJar // With Loom, this MUST be set to `remapJar` instead of `jar`!
gameVersions = ["1.18", "1.18.1", "1.18.2", "1.19", "1.19.1", "1.19.2", "1.19.3", "1.19.4", "1.20", "1.20.1", "1.20.2", "1.20.3", "1.20.4", "1.20.5", "1.20.6", "1.21", "1.21.1", "1.21.2", "1.21.3", "1.21.4", "1.21.5", "1.21.6", "1.21.7", "1.21.8", "1.21.9", "1.21.10", "1.21.11"]
def modernVersions = ['26.1', '26.1.1', '26.1.2', '26.2', '26.3']
if (project.name == 'fabric-26') {
gameVersions = modernVersions
} else if (project.name != 'fabric') {
gameVersions.addAll(modernVersions)
}
// loaders = ["fabric"]
changelog = com.ishland.buildscript.ParseGItHubActionChangelog.getChangelog()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
compressedFrameData.release();
if (payload != null) payload.release();
}
return;
} else if (compressedFrameData.getPacketId() == compressedPacketId && compressedFrameData.getReliability().isReliable && compressedFrameData.getReliability().isOrdered && !compressedFrameData.getReliability().isSequenced && inflaters[compressedFrameData.getOrderChannel()] != null) {
final int orderChannel = compressedFrameData.getOrderChannel();
final Inflater inflater = inflaters[orderChannel];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,23 @@ public MultiChannellingEncryption(Cipher decryption, Cipher encryption) {
this.encryption = new PacketEncryptionManager(Objects.requireNonNull(encryption));
}

// A null manager passes that direction through while the handshake enables the other direction independently.
private MultiChannellingEncryption(PacketEncryptionManager decryption, PacketEncryptionManager encryption) {
this.decryption = decryption;
this.encryption = encryption;
}

public static MultiChannellingEncryption inbound(Cipher cipher) {
return new MultiChannellingEncryption(new PacketEncryptionManager(Objects.requireNonNull(cipher)), null);
}

public static MultiChannellingEncryption outbound(Cipher cipher) {
return new MultiChannellingEncryption(null, new PacketEncryptionManager(Objects.requireNonNull(cipher)));
}

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof FrameData data) {
if (encryption != null && msg instanceof FrameData data) {
data.touch();
final ByteBuf buf = data.createData().skipBytes(1);
ByteBuf res = null;
Expand All @@ -86,7 +100,7 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof FrameData data) {
if (decryption != null && msg instanceof FrameData data) {
data.touch();
final ByteBuf buf = data.createData().skipBytes(1);
ByteBuf res = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
FIELD_RELIABILITY_LAST_RECEIVED_SEQ_ID.set(this.reliabilityHandler, seqId);
} finally {
byteBuf.release();
packet.release();
}
}
return;
Expand Down
Loading