はじめに

インターネットには、ときどき
なぜ存在しているのかわからないが、確かに仕様がある
そんなプロトコルが残っている。

そのひとつが Quote of the Day(QOTD)
TCP/UDP 17番ポートで動くサービスだ。

そして今回は、
RFC 865 を真面目に読み(A4一枚程度の文量)、
返す内容を「アイカツ格言」にして実装してみた。

RFC 865 をちゃんと読む

RFC 865 Quote of the Day Protocol は、1983 年 5 月に Jon Postel によって書かれた、 ARPANET 時代の非常に小さなサービス定義だ。

冒頭で明言されている目的はこれだけである。

A useful debugging and measurement tool is a quote of the day service.
A quote of the day service simply sends a short message without regard to the input.

日本語訳 by チャッピー

有用なデバッグおよび測定ツールの一つが、Quote of the Day(QOTD)サービスである。
QOTDサービスは、入力内容を一切考慮せず、単に短いメッセージを送信するだけのサービスである。

つまり、 何を送っても関係なく、向こうから一方的に短い文章が返ってくるだけの、超シンプルなサービス
という認識で良いと思う。

TCP ベースの QOTD

RFC は TCP 版の挙動を、非常に具体的に定義している。

  • サーバは TCP ポート 17 で待ち受ける
  • 接続が確立したら 短いメッセージを送信
  • クライアントから受信したデータは すべて破棄
  • メッセージ送信後、即座に接続を閉じる

上記より、TCPでありながら

  • ネゴシエーションなし
  • コマンドなし

という、エクストリームな設計になっている。

UDP ベースの QOTD

UDP版はさらに簡潔だ。

  • サーバはUDPポート17で待ち受ける
  • データグラムを1つ受信したら、内容を無視して格言を含むデータグラムを1つ返信

送られてきたデータが何であろうと、 意味を解釈する必要は一切ない。

Quote構文について

RFC 865は、Quoteの内容についても最小限しか縛らない。

  • 特定の構文は 存在しない
  • ASCII の印字可能文字、空白、CR/LF を推奨
  • 1行でも複数行でもよい
  • 512文字未満であること

ここで重要なのは、

There is no specific syntax for the quote.

とある。

つまり、 「今日の格言」が何であるかは、完全に実装者の自由である。

じゃあアイカツ格言にしよう!

格言ってなんだ?

という連想ゲームをしていたら、2ノード目くらいにアイカツ格言 が出てきた。

アイカツ格言を知らない方に説明すると、2012年くらいに放送していたTVアニメアイカツの最後にSDキャラが格言を言うコーナーがあった。

アイカツ格言は

  • 短い
  • 単体で成立する
  • 文脈不要
  • 何度受け取ってもダメージがない(むしろ回復する)
  • デバッグ中に見ても心が荒れない という完全にQOTD向きコンテンツでもある。

RFC的には単なる文字列かもしれないが、人間(アイカツおじさん)にとっては エモーショナルである。

実装

今回の実装はシンプルだ。

  1. TCP / UDP 両対応
  2. 受信データは完全無視
  3. アイカツ格言をランダムに1つ選ぶ
  4. 512文字以内で返す
  5. それっぽく「今日の格言」感を出す

これだけである。

実装コード

Pythonで実装した。
ポート17番はrootで実行する必要があるので、各自で色々やってみてほしい。

import socket
import threading
import random
import time

HOST = "0.0.0.0"
PORT = 17

# アイカツ格言(50話まで)
AIKATSU_QUOTES = [
    "芸能人はカードが命",
    "全ては今日からのレッスンにかかっている",
    "アイドルは特訓も命",
    "カメラの向こう側へ微笑め",
    "ランウェイはみんなの滑走路",
    "サインは顔をあげて",
    "おいしいものは最初に食べろ",
    "ライバルは成長のスパイス",
    "Move・on・now!",
    "みんな見えないところでがんばってる",
    "ラブはパワー",
    "Merry Christmas!",
    "甘いものは別腹!!",
    "アドリブは空気を読んで",
    "アイドルは時間も命",
    "アピールはステージの華",
    "失敗を恐れるな!",
    "思いはチョコにのせて♥",
    "キャラクターは一日にしてならず",
    "壁に耳あり障子に目あり",
    "チームワークは大切に",
    "変装は穏やかに",
    "ファッションは笑顔から",
    "旅は道連れ",
    "事実は小説より奇なり",
    "花には団子",
    "アイカツは楽しんで",
    "眠れぬ夜には散歩",
    "あいさつは大切に",
    "ライブは一体感!",
    "母の味は忘れがたし",
    "かけもちはほどほどに",
    "傘はしっかり持とう",
    "オーディションだってステージ!",
    "面接は普段どおりに",
    "見られてこそ星は輝く",
    "3人寄ればソレイユ!",
    "対バンライブで盛りあがろう!",
    "獅子の子落とし",
    "ドレスは人がまとってこそ完成する",
    "台風は大変",
    "快眠は寝床から",
    "おいしい笑顔はかわいい",
    "音楽はハッピー",
    "たまには休もう",
    "ジョニーに歴史あり!",
    "目は口ほどにものを言う",
    "アイドルは崖が好き",
    "緊張したら笑おう",
    "アイドルはおもしろい!",
]

def make_quote() -> bytes:
    quote = random.choice(AIKATSU_QUOTES)

    msg = f"今日のアイカツ格言\n{quote}\n"

    return msg[:512].encode("utf-8", errors="replace")

def tcp_server():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind((HOST, PORT))
        s.listen(128)
        print(f"[TCP] listening on {HOST}:{PORT}")

        while True:
            conn, addr = s.accept()
            threading.Thread(
                target=handle_tcp_client,
                args=(conn, addr),
                daemon=True
            ).start()

def handle_tcp_client(conn: socket.socket, addr):
    with conn:
        try:
            conn.settimeout(0.2)
            try:
                conn.recv(4096)
            except socket.timeout:
                pass

            conn.sendall(make_quote())
        except Exception as e:
            print(f"[TCP] {addr} error: {e}")

def udp_server():
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
        s.bind((HOST, PORT))
        print(f"[UDP] listening on {HOST}:{PORT}")

        while True:
            _, addr = s.recvfrom(65535)
            try:
                s.sendto(make_quote(), addr)
            except Exception as e:
                print(f"[UDP] {addr} error: {e}")

def main():
    threading.Thread(target=tcp_server, daemon=True).start()
    threading.Thread(target=udp_server, daemon=True).start()
    print("Aikatsu QOTD server running.")
    while True:
        time.sleep(3600)

if __name__ == "__main__":
    main()

なお、見るからにDoS攻撃の標的になりそうな処理なので、流石に公開はしていない。

TCPとUDPでアクセスしてみた