🕳️
Pythonで実装する、IPアドレスベースの同時接続数制限
はじめに
前回の記事では、アイドル状態のTCP接続が大量に存在すると、サーバーのメモリが枯渇しうること を確認しました。悪意のあるユーザーや、バグのあるクライアントからの過剰な接続は、サービス全体を停止させる原因となります。
では、特定のユーザー(IPアドレス)からの過剰な接続を防ぐには、どうすれば良いのでしょうか?
今回は、IPアドレスごとに同時接続数を制限するシンプルなレートリミッタを、Pythonのasyncioを使って実装します。
マシンスペック
MacBook Air M2 arm64
準備
Dockerファイルの作成
# Dockerfile
FROM python:3.11-slim
# procps: freeコマンドなど、メモリやプロセス情報を確認するツール群
RUN apt-get update && apt-get install -y procps iproute2 netcat-traditional
WORKDIR /app
サーバ
# ratelimit_server.py
import asyncio
from collections import defaultdict
# IPアドレスごとの接続数を保持する辞書
# defaultdictを使うと、キーが存在しない場合に自動で0を返してくれるので便利
CONNECTION_COUNTS = defaultdict(int)
# 1IPアドレスあたりの最大同時接続数
CONNECTION_LIMIT = 5
async def handle_connection(reader, writer):
# 接続元の情報を取得
peername = writer.get_extra_info('peername')
if peername is None:
writer.close()
await writer.wait_closed()
return
ip = peername[0]
# --- 接続数制限のロジック ---
# MutexやLockなしで安全にアクセスするため、ここで一度に処理
current_count = CONNECTION_COUNTS.get(ip, 0)
if current_count >= CONNECTION_LIMIT:
print(f"[拒否] {ip}からの接続は上限({CONNECTION_LIMIT})を超えました。")
writer.close()
await writer.wait_closed()
return
# 上限に達していなければ、カウントを増やして接続を許可
CONNECTION_COUNTS[ip] = current_count + 1
print(f"[許可] {ip}からの新しい接続。 (現在: {CONNECTION_COUNTS[ip]}接続)")
# -----------------------------
try:
# クライアントが接続を切るまで待機
while True:
data = await reader.read(1024)
if not data:
break
except Exception as e:
print(f"エラー発生: {e}")
finally:
# --- 接続終了時の後処理 ---
# 接続が切れたら、必ずカウントを減らす
CONNECTION_COUNTS[ip] -= 1
print(f"[切断] {ip}が接続を終了しました。 (残り: {CONNECTION_COUNTS[ip]}接続)")
# カウントが0になったら、メモリ節約のために辞書からキーを削除
if CONNECTION_COUNTS[ip] == 0:
del CONNECTION_COUNTS[ip]
writer.close()
await writer.wait_closed()
async def main():
server = await asyncio.start_server(handle_connection, '0.0.0.0', 8888)
addr = server.sockets[0].getsockname()
print(f'レートリミッタ付きサーバーが {addr} で起動しました (接続上限: {CONNECTION_LIMIT}/IP)')
async with server:
await server.serve_forever()
if __name__ == '__main__':
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nサーバーを停止します。")
terminalのメインプロセスで実施(サーバ)
docker build -t memory-test .
# RAMは256に制限
docker run --rm -it --name mem-test --memory=256m -v "$PWD":/app memory-test bash
python3 ratelimit_server.py
terminalの別プロセスで実施(クライアント)
docker exec -it mem-test bash
for i in $(seq 1 6); do nc localhost 8888 & done
結果
クライアントからの接続時に下記のメッセージが出ました。
レートリミッタ付きサーバーが ('0.0.0.0', 8888) で起動しました (接続上限: 5/IP)
[許可] 127.0.0.1からの新しい接続。 (現在: 1接続)
[許可] 127.0.0.1からの新しい接続。 (現在: 2接続)
[許可] 127.0.0.1からの新しい接続。 (現在: 3接続)
[許可] 127.0.0.1からの新しい接続。 (現在: 4接続)
[許可] 127.0.0.1からの新しい接続。 (現在: 5接続)
[拒否] 127.0.0.1からの接続は上限(5)を超えました。
まとめ
今回は、クライアントの同時接続数を制限する実験をしました。皆様の学習の参考になれば幸いです。
Discussion