Rename the container-side logging module decnet_logging → syslog_bridge (canonical at templates/syslog_bridge.py, synced into each template by the deployer). Drop the stale per-template copies; setuptools find was picking them up anyway. Swap useradd/USER/chown "decnet" for "logrelay" so no obvious token appears in the rendered container image. Apply the same cloaking pattern to the telnet template that SSH got: syslog pipe moves to /run/systemd/journal/syslog-relay and the relay is cat'd via exec -a "systemd-journal-fwd". rsyslog.d conf rename 99-decnet.conf → 50-journal-forward.conf. SSH capture script: /var/decnet/captured → /var/lib/systemd/coredump (real systemd path), logger tag decnet-capture → systemd-journal. Compose volume updated to match the new in-container quarantine path. SD element ID shifts decnet@55555 → relay@55555; synced across collector, parser, sniffer, prober, formatter, tests, and docs so the host-side pipeline still matches what containers emit.
150 lines
4.7 KiB
Python
150 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
LDAPserver.
|
|
Parses BER-encoded BindRequest messages, logs DN and password, returns an
|
|
invalidCredentials error. Logs all interactions as JSON.
|
|
"""
|
|
|
|
import asyncio
|
|
import os
|
|
from syslog_bridge import syslog_line, write_syslog_file, forward_syslog
|
|
|
|
NODE_NAME = os.environ.get("NODE_NAME", "ldapserver")
|
|
SERVICE_NAME = "ldap"
|
|
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
|
|
|
|
|
|
|
|
|
def _log(event_type: str, severity: int = 6, **kwargs) -> None:
|
|
line = syslog_line(SERVICE_NAME, NODE_NAME, event_type, severity, **kwargs)
|
|
write_syslog_file(line)
|
|
forward_syslog(line, LOG_TARGET)
|
|
|
|
|
|
def _ber_length(data: bytes, pos: int):
|
|
"""Return (length, next_pos)."""
|
|
b = data[pos]
|
|
if b < 0x80:
|
|
return b, pos + 1
|
|
n = b & 0x7f
|
|
length = int.from_bytes(data[pos + 1:pos + 1 + n], "big")
|
|
return length, pos + 1 + n
|
|
|
|
|
|
def _ber_string(data: bytes, pos: int):
|
|
"""Skip tag byte, read BER length, return (string, next_pos)."""
|
|
pos += 1 # skip tag
|
|
length, pos = _ber_length(data, pos)
|
|
return data[pos:pos + length].decode(errors="replace"), pos + length
|
|
|
|
|
|
def _parse_bind_request(msg: bytes):
|
|
"""Best-effort extraction of (dn, password) from a raw LDAPMessage."""
|
|
try:
|
|
pos = 0
|
|
# LDAPMessage SEQUENCE
|
|
assert msg[pos] == 0x30 # nosec B101
|
|
pos += 1
|
|
_, pos = _ber_length(msg, pos)
|
|
# messageID INTEGER
|
|
assert msg[pos] == 0x02 # nosec B101
|
|
pos += 1
|
|
id_len, pos = _ber_length(msg, pos)
|
|
pos += id_len
|
|
# BindRequest [APPLICATION 0]
|
|
assert msg[pos] == 0x60 # nosec B101
|
|
pos += 1
|
|
_, pos = _ber_length(msg, pos)
|
|
# version INTEGER
|
|
assert msg[pos] == 0x02 # nosec B101
|
|
pos += 1
|
|
v_len, pos = _ber_length(msg, pos)
|
|
pos += v_len
|
|
# name LDAPDN (OCTET STRING)
|
|
dn, pos = _ber_string(msg, pos)
|
|
# authentication CHOICE — simple [0] OCTET STRING
|
|
if msg[pos] == 0x80:
|
|
pos += 1
|
|
pw_len, pos = _ber_length(msg, pos)
|
|
password = msg[pos:pos + pw_len].decode(errors="replace")
|
|
else:
|
|
password = "<sasl_or_unknown>" # nosec B105
|
|
return dn, password
|
|
except Exception:
|
|
return "<parse_error>", "<parse_error>"
|
|
|
|
|
|
def _bind_error_response(message_id: int) -> bytes:
|
|
# BindResponse: resultCode=49 (invalidCredentials), matchedDN="", errorMessage=""
|
|
result_code = bytes([0x0a, 0x01, 0x31]) # ENUMERATED 49
|
|
matched_dn = bytes([0x04, 0x00]) # empty OCTET STRING
|
|
error_msg = bytes([0x04, 0x00]) # empty OCTET STRING
|
|
bind_resp_body = result_code + matched_dn + error_msg
|
|
bind_resp = bytes([0x61, len(bind_resp_body)]) + bind_resp_body
|
|
|
|
msg_id_enc = bytes([0x02, 0x01, message_id & 0xff])
|
|
ldap_msg_body = msg_id_enc + bind_resp
|
|
return bytes([0x30, len(ldap_msg_body)]) + ldap_msg_body
|
|
|
|
|
|
class LDAPProtocol(asyncio.Protocol):
|
|
def __init__(self):
|
|
self._transport = None
|
|
self._peer = None
|
|
self._buf = b""
|
|
|
|
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
|
|
self._process()
|
|
|
|
def _process(self):
|
|
while len(self._buf) >= 2:
|
|
if self._buf[0] != 0x30:
|
|
self._buf = b""
|
|
return
|
|
if self._buf[1] < 0x80:
|
|
msg_len = self._buf[1] + 2
|
|
elif self._buf[1] == 0x81:
|
|
if len(self._buf) < 3:
|
|
return
|
|
msg_len = self._buf[2] + 3
|
|
else:
|
|
self._buf = b""
|
|
return
|
|
if len(self._buf) < msg_len:
|
|
return
|
|
msg = self._buf[:msg_len]
|
|
self._buf = self._buf[msg_len:]
|
|
self._handle_message(msg)
|
|
|
|
def _handle_message(self, msg: bytes):
|
|
# Extract messageID for the response
|
|
try:
|
|
message_id = msg[4] if len(msg) > 4 else 1
|
|
except Exception:
|
|
message_id = 1
|
|
dn, password = _parse_bind_request(msg)
|
|
_log("bind", src=self._peer[0], dn=dn, password=password)
|
|
self._transport.write(_bind_error_response(message_id))
|
|
|
|
def connection_lost(self, exc):
|
|
_log("disconnect", src=self._peer[0] if self._peer else "?")
|
|
|
|
|
|
async def main():
|
|
_log("startup", msg=f"LDAP server starting as {NODE_NAME}")
|
|
loop = asyncio.get_running_loop()
|
|
server = await loop.create_server(LDAPProtocol, "0.0.0.0", 389) # nosec B104
|
|
async with server:
|
|
await server.serve_forever()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|