#!/usr/bin/env python3 """Prueft, ob Java- und Bedrock-Endpunkt eines Crossplay-Servers antworten. python scripts/check-crossplay.py # localhost python scripts/check-crossplay.py minecraft.djnizz.at # Live-Server Der Java-Test spricht das Server-List-Ping-Protokoll, der Bedrock-Test einen RakNet Unconnected Ping - also genau das, was die echten Clients tun. """ import json import os import socket import struct import sys import time HOST = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1" JAVA_PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 25565 BEDROCK_PORT = int(sys.argv[3]) if len(sys.argv) > 3 else 19132 TIMEOUT = 10 def _varint(n): out = b"" while True: b = n & 0x7F n >>= 7 out += bytes([b | (0x80 if n else 0)]) if not n: return out def _read_varint(sock): n = 0 for i in range(5): b = sock.recv(1) if not b: raise EOFError("Verbindung vorzeitig geschlossen") n |= (b[0] & 0x7F) << (7 * i) if not b[0] & 0x80: return n raise ValueError("VarInt zu lang") def java_ping(host, port): s = socket.create_connection((host, port), timeout=TIMEOUT) try: host_b = host.encode() # Handshake: Protokollversion ist beim Status-Ping beliebig payload = (b"\x00" + _varint(772) + _varint(len(host_b)) + host_b + struct.pack(">H", port) + _varint(1)) s.sendall(_varint(len(payload)) + payload) s.sendall(_varint(1) + b"\x00") # Status Request _read_varint(s) # Paketlaenge _read_varint(s) # Paket-ID n = _read_varint(s) # JSON-Laenge data = b"" while len(data) < n: chunk = s.recv(n - len(data)) if not chunk: break data += chunk return json.loads(data.decode("utf-8")) finally: s.close() RAKNET_MAGIC = bytes([0x00, 0xFF, 0xFF, 0x00, 0xFE, 0xFE, 0xFE, 0xFE, 0xFD, 0xFD, 0xFD, 0xFD, 0x12, 0x34, 0x56, 0x78]) def bedrock_ping(host, port): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.settimeout(TIMEOUT) try: pkt = (b"\x01" + struct.pack(">Q", int(time.time() * 1000)) + RAKNET_MAGIC + os.urandom(8)) s.sendto(pkt, (host, port)) data, _ = s.recvfrom(4096) if data[0] != 0x1C: raise ValueError(f"unerwartete Antwort-ID 0x{data[0]:02x}") strlen = struct.unpack(">H", data[33:35])[0] return data[35:35 + strlen].decode("utf-8", "replace") finally: s.close() def main(): print(f"Ziel: {HOST} (Java {JAVA_PORT}/TCP, Bedrock {BEDROCK_PORT}/UDP)\n") failed = False print("=== Java Edition (TCP) ===") try: j = java_ping(HOST, JAVA_PORT) version = j.get("version", {}) desc = j.get("description") if isinstance(desc, dict): desc = desc.get("text", "") or "".join( e.get("text", "") for e in desc.get("extra", [])) players = j.get("players", {}) print(f" OK {version.get('name')} (Protokoll {version.get('protocol')})") print(f" MOTD {desc!r}") print(f" Spieler {players.get('online')}/{players.get('max')}") except Exception as e: failed = True print(f" FEHLER {type(e).__name__}: {e}") print(" -> Security List (TCP 25565), Instanz-Firewall, Container laeuft?") print("\n=== Bedrock Edition (UDP, RakNet) ===") try: fields = bedrock_ping(HOST, BEDROCK_PORT).split(";") labels = ["Edition", "MOTD Zeile 1", "Protokoll", "Version", "Spieler", "Max. Spieler", "Server-GUID", "MOTD Zeile 2", "Gamemode"] print(" OK") for label, value in zip(labels, fields): print(f" {label:<13} {value}") except Exception as e: failed = True print(f" FEHLER {type(e).__name__}: {e}") print(" -> Security List (UDP 19132!), Geyser gestartet?") print() if failed: print("Mindestens ein Endpunkt antwortet nicht.") sys.exit(1) print("Beide Editionen erreichbar.") if __name__ == "__main__": main()