feat: send Login Disconnect with custom message on blocked join

Previously a blocked join just dropped the socket, so the MC client
showed 'Internal Exception: SocketException: Connection reset'.

Now when next_state=2 (login), the proxy sends a proper Login
Disconnect (0x00) packet containing a JSON chat component, and the
client displays the message on its disconnect screen.

- block_message added to config (default Korean message); editable
  in Settings UI as a textarea
- build_login_disconnect() encodes (varint length)+(0x00)+(JSON str)
- Status/ping (next_state=1) still silently dropped so the proxy
  presence is not announced to scanners
- Backward-compat: load_config() backfills block_message on old files
This commit is contained in:
2026-05-23 17:10:49 +09:00
parent 4b0d790748
commit 75c4242365
7 changed files with 79 additions and 4 deletions

View File

@@ -19,6 +19,7 @@ DEFAULT_CONFIG = {
"allowed_domains": [
{"domain": "mc.tkrmagid.kr", "enabled": True, "note": "메인 도메인"}
],
"block_message": "이 서버는 허용된 도메인에서만 접속 가능합니다.",
}
_lock = threading.Lock()

View File

@@ -10,6 +10,7 @@ asyncio 기반 TCP 프록시. 동작 순서:
from __future__ import annotations
import asyncio
import json
import logging
import os
import sqlite3
@@ -19,6 +20,39 @@ from pathlib import Path
import config as cfg_mod
from handshake import HandshakeError, parse_handshake, read_handshake_bytes
DEFAULT_BLOCK_MESSAGE = "이 서버는 허용된 도메인에서만 접속 가능합니다."
def _encode_varint(n: int) -> bytes:
out = bytearray()
while True:
b = n & 0x7F
n >>= 7
if n:
out.append(b | 0x80)
else:
out.append(b)
break
return bytes(out)
def build_login_disconnect(message: str) -> bytes:
"""Login Disconnect (clientbound, login state, packet id 0x00).
Body: JSON chat component as length-prefixed UTF-8 string.
클라이언트는 이 패킷을 받으면 "서버에서 연결을 거부했습니다" 화면 대신
여기 담긴 chat 컴포넌트를 그대로 보여준다.
"""
chat_json = json.dumps(
{"text": message, "color": "red"}, ensure_ascii=False
).encode("utf-8")
body = (
_encode_varint(0x00)
+ _encode_varint(len(chat_json))
+ chat_json
)
return _encode_varint(len(body)) + body
LOG_DB = Path(os.environ.get("MC_LOG_DB", "/data/logs.db"))
RESTART_SIGNAL = Path(os.environ.get("MC_RESTART_SIGNAL", "/data/restart.signal"))
@@ -184,6 +218,15 @@ async def handle_client(
log.info(
"BLOCK %s domain=%r next_state=%d", client_ip, domain, hs.next_state
)
# next_state=2 (login) 인 경우 Login Disconnect 패킷으로 메시지 전달,
# next_state=1 (status/ping) 은 그냥 끊는다 (프록시 존재 자체를 굳이 노출 X).
if hs.next_state == 2:
try:
msg = state.cfg.get("block_message") or DEFAULT_BLOCK_MESSAGE
client_writer.write(build_login_disconnect(msg))
await client_writer.drain()
except (OSError, ConnectionResetError, BrokenPipeError):
pass
client_writer.close()
return