Compare commits

..

11 Commits

Author SHA1 Message Date
Egor Savkin eb7a22729a Prototype mail router
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-19 17:23:37 +08:00
Egor Savkin a6a77f56e1 Remove request rate restriction and remove proxy protocol
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-19 10:57:36 +08:00
Egor Savkin d337612ccc Limit connections and redirect www to canonical
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-19 10:57:36 +08:00
Egor Savkin 3f3a1e62e6 Move away from automated script
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-19 10:57:36 +08:00
Egor Savkin bc3f4608d5 Postfix instead of nginx streams
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-19 10:57:36 +08:00
Egor Savkin fbb6a9b570 Remove imap and pop3 ports proxy
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-19 10:57:36 +08:00
Egor Savkin 4cbbb8d945 Add configuration for the m-labs-intl
Signed-off-by: Egor Savkin <es@m-labs.hk>
2024-08-19 10:57:36 +08:00
Sébastien Bourdeauducq 6dc8214102 nixbld/backup: include gitea DB dump 2024-08-17 18:26:46 +08:00
Sébastien Bourdeauducq a6b216bb87 nixbld/gitea: move to postgresql 2024-08-17 18:18:56 +08:00
Sébastien Bourdeauducq 6e21a95ba8 nixbld/named: add qnetp slave DNS for m-labs-intl.com 2024-08-15 19:52:42 +08:00
Sébastien Bourdeauducq d08186a27a nixbld/named: enable CAA for m-labs-intl.com 2024-08-14 11:52:25 +08:00
7 changed files with 341 additions and 3 deletions

43
m-labs-intl/danted.conf Normal file
View File

@ -0,0 +1,43 @@
logoutput: syslog
user.privileged: root
user.unprivileged: nobody
# The listening network interface or address.
internal: 5.78.86.156 port=2025
internal: 2a01:4ff:1f0:83de::1 port = 2025
# The proxying network interface or address.
external: eth0
# socks-rules determine what is proxied through the external interface.
socksmethod: none
# client-rules determine who can connect to the internal interface.
clientmethod: none
client pass {
from: 94.190.212.123/32 to: 0.0.0.0/0
}
socks pass {
from: 94.190.212.123/32 to: 0.0.0.0/0
}
client pass {
from: 202.77.7.238/32 to: 0.0.0.0/0
}
socks pass {
from: 202.77.7.238/32 to: 0.0.0.0/0
}
client pass {
from: 2001:470:18:390::2/128 to: 0.0.0.0/0
}
socks pass {
from: 2001:470:18:390::2/128 to: 0.0.0.0/0
}
client pass {
from: 2001:470:f891:1:5999:5529:5d:f71d/128 to: 0.0.0.0/0
}
socks pass {
from: 2001:470:f891:1:5999:5529:5d:f71d/128 to: 0.0.0.0/0
}

View File

@ -77,12 +77,14 @@ stream {
# SMTP
server {
listen 25;
proxy_protocol on;
proxy_pass smtp_backend;
}
# Submission (Authenticated SMTP)
server {
listen 587;
proxy_protocol on;
proxy_pass submission_backend;
}
}

View File

@ -1,7 +1,7 @@
# Setup m-labs-intl.com server
```shell
apt install git nginx-full python3 python3.12-venv python3-pip
apt install git nginx-full python3 python3.12-venv python3-pip dante-server
snap install --classic certbot
ln -s /snap/bin/certbot /usr/bin/certbot
useradd -m rfqserver
@ -11,6 +11,8 @@ cp m-labs-intl.com /etc/nginx/sites-available/
cp nginx.conf /etc/nginx/
ln -s /etc/nginx/sites-available/m-labs-intl.com /etc/nginx/sites-enabled/
cp danted.conf /etc/
mkdir -p /var/www/m-labs-intl.com/html
chown -R zolaupd /var/www/m-labs-intl.com/
@ -44,8 +46,10 @@ cp rfq.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable rfq.service
systemctl start rfq.service
systemctl enable danted.service
service nginx restart
service danted restart
certbot --nginx

278
m-labs-intl/smtp_router.py Normal file
View File

@ -0,0 +1,278 @@
import asyncio
import email
import logging
from smtplib import SMTP as SMTPClient
from typing import Dict
from aiosmtpd.proxy_protocol import ProxyData
from python_socks.sync import Proxy
import os
import ssl
from aiosmtpd.controller import Controller
from aiosmtpd.smtp import Envelope, Session, SMTP
from email.message import Message
import dkim
import spf
from functools import lru_cache
import dns.resolver
PROXY = Proxy.from_url('socks5://5.78.86.156:2025')
END_HOST = "localhost"
END_PORT = 25
PROXY_SMTP_HOST = "*"
PROXY_SMTP_PORT = 2025
END_SMTP_HOST = "*"
END_SMTP_PORT = 3025
GENERIC_SMTP_HOST = "*"
GENERIC_SMTP_PORT = 25
log = logging.getLogger("smtphandler")
VALID_PROXY_ADDRS = {"5.78.86.156", "2a01:4ff:1f0:83de::1", "m-labs-intl.com", "mail.m-labs-intl.com"}
VALID_END_ADDRS = {"localhost", "127.0.0.1"}
VALID_RECEPIENTS = {"m-labs.hk", "m-labs.ph", "m-labs-intl.com", "193thz.com", "malloctech.fr"}
@lru_cache(maxsize=256)
def get_mx(domain):
records = dns.resolver.resolve(domain, "MX")
if not records:
return None
result = max(records, key=lambda r: r.preference)
return str(result.exchange)
class ProxiedSMTP(SMTPClient):
def _get_socket(self, host, port, timeout):
return PROXY.connect(dest_host=host, dest_port=port, timeout=timeout)
class GenericMailServer:
"""
Accepts mail from everywhere, and passes it to the real SMTP server, after proper SPF and DKIM checks.
"""
async def handle_MAIL(self,
server: SMTP,
session: Session,
envelope: Envelope,
address: str,
mail_options: list):
ip = session.peer[0]
result, description = spf.check2(ip, address, session.host_name)
valid_spf = result == 'pass'
envelope.spf = valid_spf
log.info("SPF: %s, %s", result, description)
if not valid_spf:
return '550 SPF validation failed'
envelope.mail_from = address
envelope.mail_options.extend(mail_options)
return '250 OK'
async def handle_RCPT(self,
server: SMTP,
session: Session,
envelope: Envelope,
address: str,
rcpt_options: list):
if address.split("@")[1] not in VALID_RECEPIENTS:
return '550 not relaying to that domain'
log.debug("Handle RCPT for %s", address)
envelope.rcpt_tos.append(address)
return '250 OK'
async def handle_DATA(self,
server: SMTP,
session: Session,
envelope: Envelope):
valid_dkim = dkim.verify(envelope.content)
envelope.dkim = valid_dkim
log.info("DKIM: %s", valid_dkim)
message: Message = email.message_from_bytes(envelope.content)
if not valid_dkim:
return '550 DKIM validation failed'
log.info('Message: %s', message)
try:
with SMTPClient(END_HOST, END_PORT) as client:
client.sendmail(
from_addr=envelope.mail_from,
to_addrs=envelope.rcpt_tos,
msg=envelope.original_content
)
except BaseException as e:
print(e)
return '500 Could not process your message'
return '250 Message accepted for delivery'
class EndMailServer:
"""
Accepts mail only from end server. Checks if mail is signed to be from proxy, and sends to proxy if needed.
SPF and DKIM needs to be already included.
"""
async def handle_DATA(self,
server: SMTP,
session: Session,
envelope: Envelope):
ip = session.peer[0]
if ip not in VALID_END_ADDRS:
return "521 Server doesn't accept mail"
message: Message = email.message_from_bytes(envelope.content)
log.info('Message: %s', message)
mx_rcpt: Dict[str, list[str]] = {}
for rcpt in envelope.rcpt_tos:
_, _, domain = rcpt.partition("@")
mx = get_mx(domain)
if mx is None:
continue
mx_rcpt.setdefault(mx, []).append(rcpt)
try:
for mx, rcpts in mx_rcpt.items():
if envelope.mail_from in VALID_PROXY_ADDRS:
with ProxiedSMTP(mx, 25) as client:
client.sendmail(
from_addr=envelope.mail_from,
to_addrs=rcpts,
msg=envelope.original_content
)
else:
with SMTPClient(mx, 25) as client:
client.sendmail(
from_addr=envelope.mail_from,
to_addrs=rcpts,
msg=envelope.original_content
)
except BaseException as e:
print(e)
return '500 Could not process your message'
return '250 Message accepted for delivery'
class ProxyMailServer:
"""
Accepts mail only from proxy server, and passes it to the real SMTP server, after proper SPF and DKIM checks.
"""
async def handle_PROXY(self, server: SMTP, session: Session, envelope: Envelope, proxy_data: ProxyData):
ip = session.peer[0]
envelope.proxy_data = proxy_data
return ip in VALID_PROXY_ADDRS
async def handle_MAIL(self,
server: SMTP,
session: Session,
envelope: Envelope,
address: str,
mail_options: list):
ip = envelope.proxy_data.src_addr
result, description = spf.check2(ip, address, session.host_name)
valid_spf = result == 'pass'
envelope.spf = valid_spf
log.info("SPF: %s, %s", result, description)
if not valid_spf:
return '550 SPF validation failed'
envelope.mail_from = address
envelope.mail_options.extend(mail_options)
return '250 OK'
async def handle_RCPT(self,
server: SMTP,
session: Session,
envelope: Envelope,
address: str,
rcpt_options: list):
if address.split("@")[1] not in VALID_RECEPIENTS:
return '550 not relaying to that domain'
log.debug("Handle RCPT for %s", address)
envelope.rcpt_tos.append(address)
return '250 OK'
async def handle_DATA(self,
server: SMTP,
session: Session,
envelope: Envelope):
valid_dkim = dkim.verify(envelope.content)
envelope.dkim = valid_dkim
log.info("DKIM: %s", valid_dkim)
message: Message = email.message_from_bytes(envelope.content)
if not valid_dkim:
return '550 DKIM validation failed'
log.info('Message: %s', message)
try:
with SMTPClient(END_HOST, END_PORT) as client:
client.sendmail(
from_addr=envelope.mail_from,
to_addrs=envelope.rcpt_tos,
msg=envelope.original_content
)
except BaseException as e:
print(e)
return '500 Could not process your message'
return '250 Message accepted for delivery'
if __name__ == '__main__':
host = os.getenv('SMTP_HOST', '*')
port = int(os.getenv('SMTP_PORT', '25'))
accept_host = os.getenv('ACCEPT_HOST')
ssl_keys = os.getenv('SSL_KEYS')
loop = asyncio.get_event_loop()
end_handler = EndMailServer()
proxy_handler = ProxyMailServer()
generic_handler = GenericMailServer()
ssl_context = None
if ssl_keys:
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.load_cert_chain(ssl_keys + '.crt', ssl_keys + '.key')
generic_controller = Controller(GenericMailServer, hostname=GENERIC_SMTP_HOST, port=GENERIC_SMTP_PORT)
generic_controller.factory = lambda: SMTP(GenericMailServer, enable_SMTPUTF8=True, tls_context=ssl_context)
generic_controller.start()
log.info("Generic SMTP server started on %s:%s", GENERIC_SMTP_HOST, GENERIC_SMTP_PORT)
end_controller = Controller(EndMailServer, hostname=END_SMTP_HOST, port=END_SMTP_PORT)
end_controller.factory = lambda: SMTP(EndMailServer, enable_SMTPUTF8=True)
end_controller.start()
log.info("End SMTP server started on %s:%s", END_SMTP_HOST, END_SMTP_PORT)
proxy_controller = Controller(ProxyMailServer, hostname=PROXY_SMTP_HOST, port=PROXY_SMTP_PORT)
proxy_controller.factory = lambda: SMTP(ProxyMailServer, enable_SMTPUTF8=True, tls_context=ssl_context)
proxy_controller.start()
log.info("Proxy SMTP server started on %s:%s", PROXY_SMTP_HOST, PROXY_SMTP_PORT)
try:
loop.run_forever()
except KeyboardInterrupt:
print("Shutting down")
finally:
generic_controller.stop()
end_controller.stop()
proxy_controller.stop()
loop.stop()
loop.close()

View File

@ -26,9 +26,10 @@ let
${config.services.mysql.package}/bin/mysqldump --user=root --single-transaction flarum > flarum.sql
${config.services.postgresql.package}/bin/pg_dump mattermost > mattermost.sql
${config.services.postgresql.package}/bin/pg_dump rt5 > rt.sql
${config.services.postgresql.package}/bin/pg_dump gitea > gitea.sql
exec 6< /etc/nixos/secret/backup-passphrase
${pkgs.gnutar}/bin/tar cf - ${lib.concatMapStringsSep " " (p: "--exclude \"${p}\"") excludePaths} /etc/nixos /var/vmail /var/lib/hedgedoc /var/lib/gitea /var/lib/afws /var/lib/mattermost/data /var/www/193thz flarum.sql mattermost.sql rt.sql | \
${pkgs.gnutar}/bin/tar cf - ${lib.concatMapStringsSep " " (p: "--exclude \"${p}\"") excludePaths} /etc/nixos /var/vmail /var/lib/hedgedoc /var/lib/gitea /var/lib/afws /var/lib/mattermost/data /var/www/193thz flarum.sql mattermost.sql rt.sql gitea.sql | \
${pkgs.bzip2}/bin/bzip2 | \
${pkgs.gnupg}/bin/gpg --symmetric --batch --passphrase-fd 6
'';

View File

@ -375,10 +375,14 @@ in
notify explicit;
also-notify {
216.218.130.2; # ns1.he.net
213.239.220.50; # ns1.qnetp.net
88.198.32.245; # new qnetp
};
'';
slaves = [
"216.218.133.2" "2001:470:600::2" # slave.dns.he.net
"213.239.220.50" "2a01:4f8:a0:7041::1" # ns1.qnetp.net
"88.198.32.245" # new qnetp
];
};
"200-29.98.206.103.in-addr.arpa" = {
@ -773,6 +777,10 @@ in
services.gitea = {
enable = true;
appName = "M-Labs Git";
database = {
type = "postgres";
socket = "/run/postgresql";
};
mailerPasswordFile = "/etc/nixos/secret/mailerpassword";
settings = {
server = {

View File

@ -1,7 +1,7 @@
$TTL 7200
@ SOA ns.m-labs-intl.com. sb.m-labs.hk. (
2024081402
2024081503
7200
3600
86400
@ -10,12 +10,14 @@ $TTL 7200
NS ns.m-labs-intl.com.
NS ns1.he.net.
NS ns1.qnetp.net.
A 5.78.86.156
AAAA 2a01:4ff:1f0:83de::1
MX 10 mail.m-labs-intl.com.
TXT "v=spf1 mx -all"
TXT "google-site-verification=BlQd5_5wWW7calKC7bZA0GdoxR8-zj4gwJEg9sGJ3l8"
CAA 0 issue "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1768317117"
ns A 94.190.212.123
ns AAAA 2001:470:18:390::2