#!/usr/bin/env python3 """ MSSQL (TDS)server. Reads TDS pre-login and login7 packets, extracts username, responds with a login failed error. Logs auth attempts as JSON. """ import asyncio import os import struct from decnet_logging import syslog_line, write_syslog_file, forward_syslog NODE_NAME = os.environ.get("NODE_NAME", "dbserver") SERVICE_NAME = "mssql" LOG_TARGET = os.environ.get("LOG_TARGET", "") _PRELOGIN_RESP = bytes([ 0x04, 0x01, 0x00, 0x2f, 0x00, 0x00, 0x01, 0x00, # TDS header type=4, status=1, len=47 # 0. VERSION option 0x00, 0x00, 0x1a, 0x00, 0x06, # 1. ENCRYPTION option 0x01, 0x00, 0x20, 0x00, 0x01, # 2. INSTOPT 0x02, 0x00, 0x21, 0x00, 0x01, # 3. THREADID 0x03, 0x00, 0x22, 0x00, 0x04, # 4. MARS 0x04, 0x00, 0x26, 0x00, 0x01, # TERMINATOR 0xff, # version data: 14.0.2000 0x0e, 0x00, 0x07, 0xd0, 0x00, 0x00, # encryption: NOT_SUP 0x02, # instopt 0x00, # thread id 0x00, 0x00, 0x00, 0x00, # mars 0x00, ]) def _log(event_type: str, severity: int = 6, **kwargs) -> None: line = syslog_line(SERVICE_NAME, NODE_NAME, event_type, severity, **kwargs) print(line, flush=True) write_syslog_file(line) forward_syslog(line, LOG_TARGET) def _tds_error_packet(message: str) -> bytes: msg_enc = message.encode("utf-16-le") # Token type 0xAA = ERROR, followed by length, error number, state, class, msg_len, msg token = ( b"\xaa" + struct.pack("BBHBBBB", 0x04, 0x01, len(payload) + 8, 0x00, 0x00, 0x01, 0x00) return header + payload class MSSQLProtocol(asyncio.Protocol): def __init__(self): self._transport = None self._peer = None self._buf = b"" self._prelogin_done = False def connection_made(self, transport): self._transport = transport self._peer = transport.get_extra_info("peername", ("?", 0)) _log("connect", src=self._peer[0], src_port=self._peer[1]) def data_received(self, data): self._buf += data while len(self._buf) >= 8: pkt_type = self._buf[0] pkt_len = struct.unpack(">H", self._buf[2:4])[0] if pkt_len < 8: _log("unknown_packet", src=self._peer[0], pkt_type=hex(pkt_type)) self._transport.close() self._buf = b"" return if len(self._buf) < pkt_len: break payload = self._buf[8:pkt_len] self._buf = self._buf[pkt_len:] self._handle_packet(pkt_type, payload) if self._transport.is_closing(): self._buf = b"" break def _handle_packet(self, pkt_type: int, payload: bytes): if pkt_type == 0x12: # Pre-login self._transport.write(_PRELOGIN_RESP) self._prelogin_done = True elif pkt_type == 0x10: # Login7 username = self._parse_login7_username(payload) _log("auth", src=self._peer[0], username=username) self._transport.write(_tds_error_packet("Login failed for user.")) self._transport.close() else: _log("unknown_packet", src=self._peer[0], pkt_type=hex(pkt_type)) self._transport.close() def _parse_login7_username(self, payload: bytes) -> str: try: # Login7 layout: fixed header 36 bytes, then offsets # Username offset at bytes 36-37, length at 38-39 if len(payload) < 40: return "" offset = struct.unpack("" def connection_lost(self, exc): _log("disconnect", src=self._peer[0] if self._peer else "?") async def main(): _log("startup", msg=f"MSSQL server starting as {NODE_NAME}") loop = asyncio.get_running_loop() server = await loop.create_server(MSSQLProtocol, "0.0.0.0", 1433) # nosec B104 async with server: await server.serve_forever() if __name__ == "__main__": asyncio.run(main())