554 5.4.6 Too many hops によりメールが届かない件

ここ最近は MTA (Message Transfer Agent)について・・・いや、もっとわかりやすく言うとメールサーバについてお勉強中です。

ネットワークレイヤーは元々苦手分野で MTA まわりも本当に様々な仕様があってうんざりなのですが、今日は 554 5.4.6 Too many hops というエラーについて調べていたので備忘録兼ねて情報をまとめておこうかと思います。

554 5.4.6 Too many hops とは何か?

- スポンサーリンク -

554 という数値は MTA から返されるエラー番号。5xx 系なので意味的には Server-Error となります。
Too many hops の意味は、送信しようとしたメールが MTA で設定されている最大ホップ数を超えたために相手先へメールが届かないことを意味します。ホップ数とは、本来の意味ではメールが宛先に届くまでに通過するメールサーバー数を意味しています。

実際に Yahoo メールから Gmail へメールを送信した場合のメールヘッダーを例にとって説明をしてみます。MTA がホップ数として見ているポイントは「Received」で始まる行の数です。下記の例では hop = 12 となります。少なくとも qmail と postfix はこの数で Too many hops の制御を行っています。ひょっとすると hop のカウントに仕方は MTA 毎に異なるかも知れません。正確にメールサーバの経由数をカウントしている MTA もありそうですが、postfix と qmail はもっと安直にカウントしています。詳細は後述のソースコード解析にて説明します。

Delivered-To: ******@gmail.com
Received: by 10.216.79.65 with SMTP id h43cs152012wee;
        Tue, 2 Nov 2010 15:23:12 -0700 (PDT)
Received: by 10.216.231.146 with SMTP id l18mr10232048weq.52.1288736589142;
        Tue, 02 Nov 2010 15:23:09 -0700 (PDT)
Return-Path: 
Received: from mail-wy0-f170.google.com (mail-wy0-f170.google.com [74.125.82.170])
        by mx.google.com with ESMTP id x5si12512967weq.202.2010.11.02.15.23.07;
        Tue, 02 Nov 2010 15:23:07 -0700 (PDT)
Received-SPF: pass (google.com: domain of drk+caf_=******=gmail.com@drk7.jp designates 74.125.82.170 as permitted sender) client-ip=74.125.82.170;
DomainKey-Status: good (test mode)
Authentication-Results: mx.google.com; spf=pass (google.com: domain of drk+caf_=******=gmail.com@drk7.jp designates 74.125.82.170 as permitted sender) smtp.mail=drk+caf_=******=gmail.com@drk7.jp; domainkeys=pass (test mode) header.From=******@yahoo.co.jp
Received: by mail-wy0-f170.google.com with SMTP id 35so7011233wyb.1
        for <******@gmail.com>; Tue, 02 Nov 2010 15:23:07 -0700 (PDT)
Received: by 10.227.9.148 with SMTP id l20mr17351955wbl.184.1288736587332;
        Tue, 02 Nov 2010 15:23:07 -0700 (PDT)
X-Forwarded-To: ******@gmail.com
X-Forwarded-For: drk@drk7.jp ******@gmail.com
Delivered-To: drk@drk7.jp
Received: by 10.227.7.147 with SMTP id d19cs186305wbd;
        Tue, 2 Nov 2010 15:23:06 -0700 (PDT)
Received: by 10.142.221.13 with SMTP id t13mr6071668wfg.56.1288736584912;
        Tue, 02 Nov 2010 15:23:04 -0700 (PDT)
Return-Path: <******@yahoo.co.jp>
Received: from web31105.mail.kks.yahoo.co.jp (web31105.mail.kks.yahoo.co.jp [114.111.99.148])
        by mx.google.com with SMTP id d34si14573309wfj.75.2010.11.02.15.23.02;
        Tue, 02 Nov 2010 15:23:03 -0700 (PDT)
Received-SPF: pass (google.com: domain of ******@yahoo.co.jp designates 114.111.99.148 as permitted sender) client-ip=114.111.99.148;
DomainKey-Status: good (test mode)
Received: (qmail 73656 invoked by uid 60001); 2 Nov 2010 22:23:02 -0000
DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws;
  s=yj20050223; d=yahoo.co.jp;
  h=Message-ID:X-YMail-OSG:Received:X-Mailer:Date:From:Reply-To:Subject:To:MIME-Version:Content-Type:Content-Transfer-Encoding;
  b=ZvKxlbLcUvtI9XluEgFuLIXe8OGHLAic3WeXXFWdQJN22gDA2tffuAOpyQjr0IxoI4i0TEne+MizwAzEbVLumAXB5aW1q4Kf/QtqmxYcRGc32Jo9DmXZjNC4B95Yfv46  ;
Message-ID: <240947.55161.qm@web31105.mail.kks.yahoo.co.jp>
X-YMail-OSG: RrzMg3sVM1nMsO3UO6tiW634GkRGXIq_fm_uDMf1m.meY1b9lyA2m9y.MFUCublrvlfR9f6sk7FEEFbFWIUdksB2gXlKEUkne0YLziOB1ykvVWLpbJa855OKMykzE_YKBUmFXu2np0YdSUMl_96saA_dN7iqG1xp2CzW_l_m8axm2wfpPKZObCS7V04vz4ShR6dPaIPysJ5B62eOrgYoGv4f4ViqzqvY2rBeDl3v3HaIeMDgL2_gqcvz
Received: from [202.239.241.66] by web31105.mail.kks.yahoo.co.jp via HTTP; Wed, 03 Nov 2010 07:23:02 JST
X-Mailer: YahooMailWebService/0.7.289.12_26
Date: Wed, 3 Nov 2010 07:23:02 +0900 (JST)
From: ******@yahoo.co.jp
Reply-To: drk@drk7.jp
Subject: test
To: drk@drk7.jp
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

this is test

Received ヘッダーの解読方法(読み方)ですが、基本的には下から上に順に読んでいきます。前述したとおりメールサーバ間の中継情報が順に記載されています。上記ヘッダー情報で言えば、下記のような配送状況になります。

Received: from [202.239.241.66] by web31105.mail.kks.yahoo.co.jp via HTTP; Wed, 03 Nov 2010 07:23:02 JST
202.239.241.66 → web31105.mail.kks.yahoo.co.jp 

Received: from web31105.mail.kks.yahoo.co.jp (web31105.mail.kks.yahoo.co.jp [114.111.99.148])
        by mx.google.com with SMTP id d34si14573309wfj.75.2010.11.02.15.23.02;
        Tue, 02 Nov 2010 15:23:03 -0700 (PDT)
web31105.mail.kks.yahoo.co.jp → mx.google.com
・・・以下略

実際には Received ヘッダーとしては下記のような配送状況以外の情報も含まれますが、postfix や qmail では Received ヘッダを安直に一律カウントする実装となっています。

Received-SPF: pass (google.com: domain of ******@yahoo.co.jp designates 114.111.99.148 as permitted sender) client-ip=114.111.99.148;
DomainKey-Status: good (test mode)

Received: (qmail 73656 invoked by uid 60001); 2 Nov 2010 22:23:02 -0000
DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws;
  s=yj20050223; d=yahoo.co.jp;
  h=Message-ID:X-YMail-OSG:Received:X-Mailer:Date:From:Reply-To:Subject:To:MIME-Version:Content-Type:Content-Transfer-Encoding;
  b=ZvKxlbLcUvtI9XluEgFuLIXe8OGHLAic3WeXXFWdQJN22gDA2tffuAOpyQjr0IxoI4i0TEne+MizwAzEbVLumAXB5aW1q4Kf/QtqmxYcRGc32Jo9DmXZjNC4B95Yfv46  ;

netqmail 1.16 のソースコードを見てみます。too many hops 関連のコードは qmail-smtp.c [293行目] 付近のコードです。簡単に言っちゃうと received で始まるヘッダー部分を数えるロジックです。

void blast(hops)
int *hops;
{
  char ch;
  int state;
  int flaginheader;
  int pos; /* number of bytes since most recent \n, if fih */
  int flagmaybex; /* 1 if this line might match RECEIVED, if fih */
  int flagmaybey; /* 1 if this line might match \r\n, if fih */
  int flagmaybez; /* 1 if this line might match DELIVERED, if fih */
 
  state = 1;
  *hops = 0;
  flaginheader = 1;
  pos = 0; flagmaybex = flagmaybey = flagmaybez = 1;
  for (;;) {
    substdio_get(&ssin,&ch,1);
    if (flaginheader) {
      if (pos < 9) {
        if (ch != "delivered"[pos]) if (ch != "DELIVERED"[pos]) flagmaybez = 0;
        if (flagmaybez) if (pos == 8) ++*hops;
        if (pos < 8)
          if (ch != "received"[pos]) if (ch != "RECEIVED"[pos]) flagmaybex = 0;
        if (flagmaybex) if (pos == 7) ++*hops;
        if (pos < 2) if (ch != "\r\n"[pos]) flagmaybey = 0;
        if (flagmaybey) if (pos == 1) flaginheader = 0;
        ++pos;
      }
      if (ch == '\n') { pos = 0; flagmaybex = flagmaybey = flagmaybez = 1; }
    }
    switch(state) {
      case 0:
        if (ch == '\n') straynewline();
        if (ch == '\r') { state = 4; continue; }
        break;
      case 1: /* \r\n */
        if (ch == '\n') straynewline();
        if (ch == '.') { state = 2; continue; }
        if (ch == '\r') { state = 4; continue; }
        state = 0;
        break;
      case 2: /* \r\n + . */
        if (ch == '\n') straynewline();
        if (ch == '\r') { state = 3; continue; }
        state = 0;
        break;
      case 3: /* \r\n + .\r */
        if (ch == '\n') return;
        put(".");
        put("\r");
        if (ch == '\r') { state = 4; continue; }
        state = 0;
        break;
      case 4: /* + \r */
        if (ch == '\n') { state = 1; break; }
        if (ch != '\r') { put("\r"); state = 0; }
    }
    put(&ch);
  }
}

そして qmail-smtp.c [368行目]付近のコードで MAXHOPS との比較を行いエラー判定を行っています。

void smtp_data(arg) char *arg; {
  int hops;
  unsigned long qp;
  char *qqx;
 
  if (!seenmail) { err_wantmail(); return; }
  if (!rcptto.len) { err_wantrcpt(); return; }
  seenmail = 0;
  if (databytes) bytestooverflow = databytes + 1;
  if (qmail_open(&qqt) == -1) { err_qqt(); return; }
  qp = qmail_qp(&qqt);
  out("354 go ahead\r\n");
 
  received(&qqt,"SMTP",local,remoteip,remotehost,remoteinfo,fakehelo);
  blast(&hops);
  hops = (hops >= MAXHOPS);
  if (hops) qmail_fail(&qqt);
  qmail_from(&qqt,mailfrom.s);
  qmail_put(&qqt,rcptto.s,rcptto.len);
 
  qqx = qmail_close(&qqt);
  if (!*qqx) { acceptmessage(qp); return; }
  if (hops) { out("554 too many hops, this message is looping (#5.4.6)\r\n"); return; }
  if (databytes) if (!bytestooverflow) { out("552 sorry, that message size exceeds my databytes limit (#5.3.4)\r\n"); return; }
  if (*qqx == 'D') out("554 "); else out("451 ");
  out(qqx + 1);
  out("\r\n");
}

postfix 2.7.1 のソースコードを見てみます。too many hops 関連のコードは cleanup_message.c [567行目] 付近のコードです。ロジックは qmail とほぼ同じで received ヘッダーの個数を数えているだけです。

    else {
	state->headers_seen |= (1 << hdr_opts->type);
	if (hdr_opts->type == HDR_MESSAGE_ID)
	    msg_info("%s: message-id=%s", state->queue_id, hdrval);
	if (hdr_opts->type == HDR_RESENT_MESSAGE_ID)
	    msg_info("%s: resent-message-id=%s", state->queue_id, hdrval);
	if (hdr_opts->type == HDR_RECEIVED)
	    if (++state->hop_count >= var_hopcount_limit)
		state->errs |= CLEANUP_STAT_HOPS;
	if (CLEANUP_OUT_OK(state)) {
	    if (hdr_opts->flags & HDR_OPT_RR)
		state->resent = "Resent-";
	    if ((hdr_opts->flags & HDR_OPT_SENDER)
		&& state->hdr_rewrite_context) {
		cleanup_rewrite_sender(state, hdr_opts, header_buf);
	    } else if ((hdr_opts->flags & HDR_OPT_RECIP)
		       && state->hdr_rewrite_context) {
		cleanup_rewrite_recip(state, hdr_opts, header_buf);
	    } else if ((hdr_opts->flags & HDR_OPT_DROP) == 0) {
		cleanup_out_header(state, header_buf);
	    }
	}
    }


さて長くなってきました。もう少しおつきあいください。そもそも hops 数をサーバ側で制御している理由はなんでしょう?

答えはメールサーバ間でメッセージのループが発生した際に、メッセージが永久ループしないように適切にメッセージを破棄する必要があります。業界用語ではメールのピンポンと呼ぶのですが、ピンポンを防ぐための機能として Too many hops エラー制御しているわけです。現在の電子メール配送システムはインテリジェンスではないため、設定に不具合があればメールサーバ間で永遠に中継を繰り返すことがあり得ます。

例えば、下記のようにメールのリレー設定を間違えると容易に発生し得ます。qmail の smtproutes の設定をわざと間違えれば再現できます。

(1) メールサーバAではメールをメールサーバBにリレーすると設定します。
(2) メールサーバBではメールをメールサーバAにリレーすると設定します。

これだけでループの完成です。qmail の smtproutes の設定をわざと間違えれば再現できます。

とまぁそんな理由で Too many hops の値が MTA 毎に設定されているわけですが、これまた設定値がバラバラであることが調べてみてわかりました。

sendmail MaxHopCount = 25
Postfix hopcount_limit = 50
net-qmail MAXHOPS = 100
Exchange Server HKLM\System\CCS\Services\MSExchangeIMC\MaxReceivedHeaders = 18

果たして数値としての適正値はなんなんでしょう?その答えは RFC2821 にて記載されています。
http://www.faqs.org/rfcs/rfc2821.html

3.7 Relaying
〜中略〜
As discussed in section 2.4.1, a relay SMTP has no need to inspect or act upon the headers or body of the message data and MUST NOT do so except to add its own "Received:" header (section 4.4) and, optionally, to attempt to detect looping in the mail system (see section 6.2).

6.2 Loop Detection
Simple counting of the number of "Received:" headers in a message has proven to be an effective, although rarely optimal, method of detecting loops in mail systems. SMTP servers using this technique SHOULD use a large rejection threshold, normally at least 100 Received entries. Whatever mechanisms are used, servers MUST contain provisions for detecting and stopping trivial loops.

せっかくなので日本語訳版も書いておきます。
http://srgia.com/docs/rfc2821j.html

3.7 Relaying
セクション 2.4.1 で議論されているように、リレー SMTP はメッセージデータのヘッダ部やボディ部を検査したり、それに基づいて動作したりする必要はなく、自分自身の "Received:" ヘッダを追加する場合(セクション 4.4 参照)と、任意でメールシステムのループを検出する試みを行う場合(セクション 6.2 参照)とを除き、そうしてはならない。

6.2 Loop Detection
メールシステムにおけるループを検出するためにメッセージ内の "Received:" ヘッダの数を数えるという単純な方法は、最適であることはまれだとは言え、効果的な手段であることが立証されている。この手法を使用する SMTP サーバーは、大きな閾値(少なくとも 100 個の Received エントリー)を使用するべきである。どのようなメカニズムが使用されるとしても、サーバーは平凡なループを検出・停止するための対策を含んでいなければならない。

結論です。Too many hops に関する閾値として MTA は 100 以上を設定されているのが望ましいとされています。100 以上で実装されているのは、僕が調べた中では net-qmail のみでした。実際問題としては、Exchange-server や sendmail のような小さな値では too many hops エラーがそれなりに発生することが推測されます。その場合には設定値を変更することでエラーは解消されると思います。

おしまい。

今夜わかるメールプロトコル (Network)
上野 宣
翔泳社
売り上げランキング: 80186
おすすめ度の平均: 4.5
5 メールプロトコル入門に最適
5 著者の「愛」を感じます(笑)
4 メール送受信の仕組・プロトコル等がとても分りやすい本
5 【Telnetで理解する書籍】
4 メール配送にまつわる話はコレ一冊でOK
電子メールプロトコル―基本・実装・運用
デイビッド ウッド
オライリー・ジャパン
売り上げランキング: 203385
おすすめ度の平均: 3.5
4 初めて中古書籍購入
3 非常によい内容ですが・・・
4 日本事情も追記した、翻訳だけに終わらない内容
- スポンサーリンク -