diff --git a/nullmailer/defaults/main.yml b/nullmailer/defaults/main.yml new file mode 100644 index 0000000000000000000000000000000000000000..4adfed4e01a1571d576f2d23626f3ba7ef61cc62 --- /dev/null +++ b/nullmailer/defaults/main.yml @@ -0,0 +1,3 @@ +--- + +nullmailer_enable_queued: false diff --git a/nullmailer/files/nullmailer-queuec.py b/nullmailer/files/nullmailer-queuec.py new file mode 100644 index 0000000000000000000000000000000000000000..72b4aa7f4089450e568693739dacc964fdb97179 --- /dev/null +++ b/nullmailer/files/nullmailer-queuec.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +import socket +import sys + +SERVER_SOCKET = '/run/nullmailer/queued.sock' + +# reference implementation from docs.python.org; will be in v3.9 +if not hasattr(socket.socket, 'send_fds'): + import array + def send_fds(sock, msg, fds): + ancdata = [(socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", fds).tobytes())] + return sock.sendmsg([msg], ancdata) + socket.socket.send_fds = send_fds + +sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) +try: + sock.connect(SERVER_SOCKET) +except socket.error as msg: + print(msg) + sys.exit(255) + +ret = 255 +try: + sock.send_fds((1).to_bytes(4, 'big'), [sys.stdin.fileno()]) + ret = int.from_bytes(sock.recv(1024), 'big') +finally: + sock.close() + +sys.exit(ret) diff --git a/nullmailer/files/nullmailer-queued.py b/nullmailer/files/nullmailer-queued.py new file mode 100644 index 0000000000000000000000000000000000000000..ccaa190f3a3b737dd7fb90278238259987d87d30 --- /dev/null +++ b/nullmailer/files/nullmailer-queued.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 + +import os +import socket +import socketserver +import struct +import subprocess +import sys + +SERVER_SOCKET = '/run/nullmailer/queued.sock' +QUEUE_BINARY = '/usr/sbin/nullmailer-queue' + +# from: https://github.com/ganeti/ganeti/blob/4d8c16a0739c93400471023b3cf9adf73a037b8f/lib/netutils.py#L51 +_STRUCT_UCRED = "iII" # pid, uid, gid +_STRUCT_UCRED_SIZE = struct.calcsize(_STRUCT_UCRED) +def get_peer_credentials(sock): + peercred = sock.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, _STRUCT_UCRED_SIZE) + return struct.unpack(_STRUCT_UCRED, peercred) + +# reference implementation from docs.python.org; will be in v3.9 +if not hasattr(socket.socket, 'recv_fds'): + import array + def recv_fds(sock, msglen, maxfds): + fds = array.array("i") # Array of ints + msg, ancdata, flags, addr = sock.recvmsg(msglen, socket.CMSG_LEN(maxfds * fds.itemsize)) + for cmsg_level, cmsg_type, cmsg_data in ancdata: + if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS: + # Append data, ignoring any truncated integers at the end. + fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)]) + return msg, list(fds) + socket.socket.recv_fds = recv_fds + +def drop_privs(uid, gid): + try: + os.setgid(gid) + os.setuid(uid) + except OSError as e: + print(f'could not drop privs to user {uid}, group {gid}, because {e}') + sys.exit(1) + +class QueueingHandler(socketserver.BaseRequestHandler): + def handle(self): + print(f'request incoming') + try: + fd = self.request.recv_fds(1024, 1)[1][0] + except IndexError: + print(f'did not receive fd') + try: + self.request.send((255).to_bytes(4, 'big')) + except: + pass + return + print(f'received fd') + (uid, gid) = get_peer_credentials(self.request)[1:] + print(f'by user: {uid}, {gid}') + try: + if sys.version_info.minor >= 9: + proc = subprocess.run([QUEUE_BINARY], stdin=fd, group=gid, user=uid) + else: + # This is not strictly safe with a threading server due to preexec_fn. + proc = subprocess.run([QUEUE_BINARY], stdin=fd, preexec_fn=lambda: drop_privs(uid, gid)) + except Exception as e: + print(f'could not execute {QUEUE_BINARY}, because {e}') + try: + self.request.send((255).to_bytes(4, 'big')) + except: + pass + return + print(f'ran {QUEUE_BINARY}, returns {proc.returncode}') + self.request.send(proc.returncode.to_bytes(4, 'big')) + +if __name__ == '__main__': + try: + os.unlink(SERVER_SOCKET) + except OSError: + if os.path.exists(SERVER_SOCKET): + raise + + with socketserver.ThreadingUnixStreamServer(SERVER_SOCKET, QueueingHandler) as server: + os.chmod(SERVER_SOCKET, 0o0777) + server.serve_forever() diff --git a/nullmailer/files/nullmailer-queued.service b/nullmailer/files/nullmailer-queued.service new file mode 100644 index 0000000000000000000000000000000000000000..dc8daa74ee02a2adb05ad72a720778e59b3cb2ec --- /dev/null +++ b/nullmailer/files/nullmailer-queued.service @@ -0,0 +1,13 @@ +[Unit] +Description=Enqueueing facility for restricted clients of nullmailer +After=networking.target +Before=php-fpm@.service + +[Service] +ExecStart=/usr/local/sbin/nullmailer-queued +RuntimeDirectory=nullmailer +ReadWritePaths=/var/spool/nullmailer +ProtectSystem=strict +ProtectControlGroups=yes +ProtectHome=yes +PrivateTmp=yes diff --git a/nullmailer/handlers/main.yml b/nullmailer/handlers/main.yml index a96ee489e14d1c815c39f7dbddd3e87c6c895f6d..8b7744a973033e51436fc546567826781fbcad3c 100644 --- a/nullmailer/handlers/main.yml +++ b/nullmailer/handlers/main.yml @@ -1,5 +1,15 @@ --- -# file: roles/nullmailer/handlers/main.yml - name: restart nullmailer - service: name=nullmailer state=restarted + service: + name: nullmailer + state: restarted + +- name: restart nullmailer-queued + service: + name: nullmailer-queued + state: restarted + +- name: reload systemd service files + systemd: + daemon_reload: true diff --git a/nullmailer/tasks/main.yml b/nullmailer/tasks/main.yml index 78398f86d3b464dfd42b4f108cbfe34fa6db0d40..2afb71ed3572a7a660a6eeaf60de3ec329effc2c 100644 --- a/nullmailer/tasks/main.yml +++ b/nullmailer/tasks/main.yml @@ -95,4 +95,33 @@ tags: - nullmailer +- name: ensure there is a custom enqueueing service + copy: + src: nullmailer-queued.service + dest: /etc/systemd/system/ + notify: + - reload systemd service files + when: nullmailer_enable_queued + - meta: flush_handlers + +- name: ensure there is a custom enqueueing daemon + copy: + src: "nullmailer-{{ item }}.py" + dest: "/usr/local/sbin/nullmailer-{{ item }}" + owner: root + group: root + mode: '0755' + with_items: + - queuec + - queued + notify: + - restart nullmailer-queued + when: nullmailer_enable_queued + +- name: ensure the custom enqueueing service is enabled and running + service: + name: nullmailer-queued + state: started + enabled: true + when: nullmailer_enable_queued