Proxy経由でSMTPアクセス

Posted on 2021/06/29

ToC

メールサーバーをマネージドサービスで…

「マネージドサービスを使うことでサーバー管理の手間を減らしたい」というのはよく聞く話です。 Alibaba DirectMail や Amazon SES など、多くのマネージドサービスもREST APIイベント連携によるメール機能を提供しています。 これらを利用すると、アプリケーションからAPIコールで直接メールが送信できるので非常に簡単です。

そして、アプリケーションの他にもProxy下のネットワーク環境で、メール通知するツール群があります。 ツール群の多くは、APIではなく、標準的なSMTPプロトコルを利用してメールを送信しているものが多くあります。

今回は、これらもすべて対応して、メールサーバーを完全に無くしてしまおうと欲を出すという話です。

メールサーバーへのアクセス用のテストプログラム

PySocksというモジュールを使って、SMTP通信をProxyサーバー経由で送信ができるようです。
今回は、サンプルコードを公開されていたPythonでメール送信 のコードをベースに詳細の説明を確認しながら Proxy経由でのメール送信のテストプログラムを作成しました。
(サンプルの公開、ありがとうございます)

import smtplib
import ssl
import socks  # PySocks

from email.mime.text import MIMEText
from email.utils import formatdate

FROM_ADDRESS = 'from-address@example.tokyo'
MY_PASSWORD = 'put-your-password-here'
SMTP_SERVER = 'smtpdm-ap-southeast-1.aliyun.com'
SMTP_PORT = 465

# for Squid
HTTP_PROXY_HOST = 'localhost'
HTTP_PROXY_PORT = 3128
# for Dante
SOCKS_PROXY_HOST = 'localhost'
SOCKS_PROXY_PORT = 1080

def create_message(from_addr, to_addr, subject, body):
    msg = MIMEText(body)
    msg['Subject'] = subject
    msg['From'] = from_addr
    msg['To'] = to_addr
    msg['Date'] = formatdate()
    return msg


def send_mail(body_msg, context=None):
    _context = context if context is not None else ssl.create_default_context()
    nego_combo = ("ssl", SMTP_PORT)
    client = smtplib.SMTP_SSL(SMTP_SERVER, nego_combo[1], timeout=10, context=_context)
    client.set_debuglevel(2)
    client.login(FROM_ADDRESS, MY_PASSWORD)
    client.send_message(body_msg)
    client.quit()


if __name__ == '__main__':
    to_address = 'to-address@example.yokohama'
    msg = create_message(FROM_ADDRESS, to_address, 'Hello, world', 'テストメールです。')
    
    # for Squid
    socks.setdefaultproxy(socks.HTTP, HTTP_PROXY_HOST, HTTP_PROXY_PORT)
    socks.wrapmodule(smtplib)
    socks_context = ssl.create_default_context()
    send_mail(msg, socks_context)

    # for Dante
    socks.setdefaultproxy(socks.SOCKS5, SOCKS_PROXY_HOST, SOCKS_PROXY_PORT)
    socks.wrapmodule(smtplib)
    socks_context = ssl.create_default_context()
    send_mail(msg, socks_context)

Proxyについて、少し勉強してみた

SOCKSプロキシとHTTPプロキシの違いについて勉強してみた の記事を読んで、Squidのような Http Proxy(L6) の他に Socks Proxy(L4)があるということだったので、今回は両方を試してみることにしました。 記事では、Socks Proxyとして、delegateというプロダクトを使って検証されていたのですが、 筆者の方が「20世紀的発送」と少し自虐的に書いておられたので、 他のプロダクトも調査してみました。
2021年 現在、継続的にメンテナンスされているプロダクトとして、DanteというSocks Proxyが良い感じだったので、今回はこれを使ってみました。

Proxy経由でのメール送信

Squidアクセス

Squidには、通信を許可するプロトコルやアクセス先を設定するAccess Control List (acl)があり、SMTPのポートを追加してみます。 デフォルトで、各種ポートの設定が、/etc/squid/squid.confに設定されると思いますが、これに追加して下記の設定も追加してみます。

squid-acl.conf

acl SSL_ports port 465
acl Safe_ports port 465

テストプログラムを実行してみると、SquidのProxyを経由して、Alibaba DirectMailのSMTPを使った メール送信ができました。Dockerで構築したので、参考までにDockerfileは下記です。

squid_1  | 172.19.0.1 - - [27/Jun/2021:05:39:17 +0000] "CONNECT 47.88.198.4:465 HTTP/1.1" 200 5092 "-" "-" TCP_TUNNEL:HIER_DIRECT

Danteアクセス

Danteにも、通信制御のためのAccess Control List (acl)があり、SMTPのポートを追加してみます。 アクセス先のエンドポイントの制御もできそうでしたので、少し試してみました。

sockd.conf

debug: 0
logoutput: stdout
internal: 0.0.0.0 port = 1080
external: eth0
socksmethod: username none
clientmethod: none
user.privileged: root
user.unprivileged: nobody

client pass {
    from: 0.0.0.0/0 port 1-65535 to: 0.0.0.0/0 port 1-65535
    log: error
}

socks pass {
    from: 0.0.0.0/0 to: smtpdm-ap-southeast-1.aliyun.com
    log: ioop
}

こちらもテストプログラムを実行してみると、DanteのSocks Proxyを経由して、メール送信ができました。 Dockerfileは下記です。

dante_1  | Jun 27 07:21:11 (1624778471.680951) sockd[80]: info: pass(2): tcp/connect -: 172.21.0.1.62300 172.21.0.2.1080 -> 172.21.0.2.62300 47.88.198.4.465 (517)
dante_1  | Jun 27 07:21:11 (1624778471.761069) sockd[80]: info: pass(2): tcp/connect -: 47.88.198.4.465 172.21.0.2.62300 -> 172.21.0.2.1080 172.21.0.1.62300 (4184)

結論

Proxy経由で、マネージドサービスのSMTPプロトコルを利用することは可能でした。 マネージドサービスを利用するために追加の構成要素が必要になると本末転倒なので、そういう意味では良かったです。

ただ、HTTP_PROXYのように環境変数に設定するだけで動作すると良いのですが、サードパーティ製のアプリケーションが対応しているかどうかはわかりません。
このあたりは、継続調査が必要そうですね。


検証のために作成したDockerfile

お気に入りのAmazon Linux2ベースで作成しています。

Squid

Dockerfile

FROM public.ecr.aws/amazonlinux/amazonlinux:2

RUN amazon-linux-extras enable python3.8 squid4 \
    && yum clean metadata \
    && yum -y update \
    && yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm \
    && yum install -y squid \
                      python38 \
                      jq \
                      tar \
                      zip \
                      unzip \
                      git \
                      vim-common \
                      nkf \
                      gcc \
                      make \
                      autoconf \
                      automake \
                      zlib-devel \
                      bzip2 \
                      bzip2-devel \
                      readline-devel \
                      sqlite \
                      sqlite-devel \
                      openssl-devel \
                      xz \
                      xz-devel \
                      libffi-devel \
                      which \
    && yum clean all \
    && rm -rf /var/yum/cache \
    && rm -rf /var/cache/yum

# Symbolic link to python3
RUN ln -s /usr/bin/python3.8 /usr/bin/python3

# Install native python3 pip3
RUN curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py" \
    && python3 get-pip.py

# Setup squid
RUN ls -al /etc/squid

RUN mkdir -p /etc/squid/conf.d \
  && cp /etc/squid/squid.conf /etc/squid/conf.d/default.conf \
  && echo 'include /etc/squid/conf.d/*.conf' > /etc/squid/squid.conf

RUN chown -R squid:squid /etc/squid/conf.d \
    && chown squid:squid /dev/stdout

COPY squid-log.conf /etc/squid/conf.d/
COPY squid-acl.conf /etc/squid/conf.d/
EXPOSE 3128

user squid
CMD ["/usr/sbin/squid", "-NYCd 1"]

squid-log.conf

logfile_rotate 0
cache_store_log none
access_log stdio:/dev/stdout combined
cache_log stdio:/dev/stdout
coredump_dir /dev/null
pid_filename /var/run/squid/squid.pid

Dante

Dockerfile

FROM public.ecr.aws/amazonlinux/amazonlinux:2

ENV DANTE_VER=1.4.3
ENV DANTE_URL=https://www.inet.no/dante/files/dante-$DANTE_VER.tar.gz
ENV DANTE_SHA=418a065fe1a4b8ace8fbf77c2da269a98f376e7115902e76cda7e741e4846a5d
ENV DANTE_FILE=dante.tar.gz
ENV DANTE_TEMP=/tmp/dante

RUN set -ex \
 # Build environment setup
RUN yum clean metadata \
    && yum -y update \
    && yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm \
    && yum groupinstall -y "Development Tools" \
    && yum install -y jq \
                      tar \
                      zip \
                      unzip \
                      git \
                      wget \
                      which \
                      perl-Digest-SHA \

    && mkdir ${DANTE_TEMP} \
    && cd ${DANTE_TEMP} \
    && curl -sSL ${DANTE_URL} -o ${DANTE_FILE} \
    && echo "${DANTE_SHA} *${DANTE_FILE}" | shasum -c \
    && tar xzf ${DANTE_FILE} --strip 1 \
    && ./configure \
    && make install \
    && cd .. \
    && rm -rf ${DANTE_TEMP} \

    && yum groupremove -y "Development Tools" \
    && yum clean all \
    && rm -rf /var/yum/cache \
    && rm -rf /var/cache/yum

COPY sockd.conf /etc/dante/sockd.conf

ENV CFGFILE=/etc/dante/sockd.conf
ENV PIDFILE=/run/sockd.pid
ENV WORKERS=10

EXPOSE 1080

CMD sockd -f $CFGFILE -p $PIDFILE -N $WORKERS


参照