Files
DECNET/templates/mssql/server.py
anti 55896b0caa Add RFC 5424 syslog logging to all service templates
- decnet/logging/syslog_formatter.py: RFC 5424 formatter (local0 facility,
  decnet@55555 SD element ID, full escaping per §6.3.3)
- decnet/logging/file_handler.py: rotating file handler (10 MB / 5 backups),
  path configurable via DECNET_LOG_FILE env var
- templates/decnet_logging.py: combined syslog_line / write_syslog_file /
  forward_syslog helper distributed to all 22 service template dirs
- All templates/*/server.py: replaced ad-hoc JSON _forward/_log with RFC 5424
  syslog_line + write_syslog_file + forward_syslog
- All templates/*/Dockerfile: COPY decnet_logging.py /opt/
- DecnetConfig: added log_file field; CLI: --log-file flag;
  composer injects DECNET_LOG_FILE env var into service containers
- tests/test_syslog_formatter.py + tests/test_file_handler.py: 25 new tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 04:31:00 -03:00

137 lines
4.5 KiB
Python

#!/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 json
import os
import socket
import struct
from datetime import datetime, timezone
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", "")
# Minimal TDS pre-login response
_PRELOGIN_RESP = bytes([
0x04, 0x01, 0x00, 0x2b, 0x00, 0x00, 0x01, 0x00, # TDS header type=4, status=1, len=43
# VERSION option
0x00, 0x00, 0x1a, 0x00, 0x06,
# ENCRYPTION option (not supported = 0x02)
0x01, 0x00, 0x20, 0x00, 0x01,
# INSTOPT
0x02, 0x00, 0x21, 0x00, 0x01,
# THREADID
0x03, 0x00, 0x22, 0x00, 0x04,
# TERMINATOR
0xff,
# version data: 16.00.1000
0x10, 0x00, 0x03, 0xe8, 0x00, 0x00,
# encryption: NOT_SUP
0x02,
# instance name NUL
0x00,
# thread id
0x00, 0x00, 0x00, 0x01,
])
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("<H", 4 + 1 + 1 + 2 + len(msg_enc) + 1 + 1 + 1 + 1 + 4)
+ struct.pack("<I", 18456) # SQL error number: login failed
+ b"\x01" # state
+ b"\x0e" # class
+ struct.pack("<H", len(message))
+ msg_enc
+ b"\x00" # server name length
+ b"\x00" # proc name length
+ struct.pack("<I", 1) # line number
)
done = b"\xfd\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
payload = token + done
header = 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 len(self._buf) < pkt_len:
break
payload = self._buf[8:pkt_len]
self._buf = self._buf[pkt_len:]
self._handle_packet(pkt_type, payload)
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 "<short_packet>"
offset = struct.unpack("<H", payload[36:38])[0]
length = struct.unpack("<H", payload[38:40])[0]
username = payload[offset:offset + length * 2].decode("utf-16-le", errors="replace")
return username
except Exception:
return "<parse_error>"
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)
async with server:
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main())