株式会社クリアコード > ククログ

ククログ

タグ:

東京Ruby会議11での発表「アプリケーションへのRubyインタープリターの組み込み」とOSS Gateワークショップ2016-05-28 #tkrk11 #oss_gate

5月28日に開催された東京Ruby会議11で「アプリケーションへのRubyインタープリターの組み込み」と題して、アプリケーションにRubyを組み込む実装について話しました。

関連リンク:

質疑応答の補足

内容は前述のスライドや発表動画を参照してください。ここでは発表後の質疑応答の内容について補足します。

質問:ruby_sysinit()は呼ばなくていいの?

ruby_sysinit()は呼ばなくていいの?」に対する当日の回答は「ruby_sysinit()の説明は省略した」だったのですが、どうして省略したかを補足します。

Rubyインタプリターを組み込んだアプリケーションの1つであるrubyコマンドの実装(main.c)を見るとruby_sysinit()を呼んでいます。そのため、Rubyインタプリターの初期化には必要なAPIにみえます。

しかし、ruby_sysinit()のコメントには次のように書いています。ざっくり言うとrubyコマンドを初期化するためのもので、Rubyインタプリターを組み込むときはこの関数を呼ぶんじゃなくて自分で初期化してね、と言っています。

Initializes the process for ruby(1).

This function assumes this process is ruby(1) and it has just started. Usually programs that embeds CRuby interpreter should not call this function, and should do their own initialization.

中身を見ると、コマンドライン引数と標準入出力を初期化しています。Windowsで動く場合はもっと初期化しています。

Rubyインタプリターを組み込んだアプリケーション例として紹介したmilter managerではruby_sysinit()を呼んでいません。コマンドライン引数は自前で処理しています。

ただ、ruby_sysinit()を呼ばないというのはmilter managerがWindowsをサポートしていないからできることです。Windows用の初期化をしているrb_w32_sysinit()をチラ見するとわかりますが、この関数がやっている処理のうち、自分に必要な分はどこかを判断してそれらを自前で実装することは難しいでしょう。(Windowsに詳しい人ならそうでもないかもしれません。)

そのため、Windowsでも動くアプリケーションにRubyインタプリターを組み込む場合はruby_sysinit()を呼ぶのがよいでしょう。(コメントでは呼ぶなと書いていますが。)

質問:共有ライブラリーの方にアプリケーションのメイン関数を渡せばアプリケーションでRUBY_INIT_STACKを呼ばなくていいんじゃない?

スライドで言うと以下のページの実装についての質問です。

以下のようにしてembedded_init()内でapplication_main()を呼ぶようにすればいいんじゃない?という話です。

int
main(void)
{
  embedded_ruby_module = dlopen();
  embedded_ruby_init = dlsym(embedded_ruby_module, "embedded_init");
  embedded_ruby_init(application_main);
}
void
embedded_init(main_func application_main)
{
  RUBY_INIT_STACK;
  /* ... */
  application_main();
}

これに対する回答は「RubyインタプリターとPythonインタプリターを一緒に組み込めないのでやりたいことを実現できない」でした。一緒に組み込む時のイメージは次の通りです。

void
main(void)
{
  embedded_ruby_module = dlopen();
  embedded_ruby_init = dlsym(embedded_ruby_module, "embedded_init");
  embedded_ruby_init(application_main);
  /* ↓はアプリケーションのメイン関数の前に実行しないといけないけど、
     ↑でメイン関数が実行されちゃう。 */

  embedded_python_module = dlopen();
  embedded_python_init = dlsym(embedded_python_module, "embedded_init");
  embedded_python_init(application_main);
}

それに対する別案が「他のインタプリターの初期化もやる関数を渡せば?」でした。こんなイメージです。

void
main(void)
{
  embedded_ruby_module = dlopen();
  embedded_ruby_init = dlsym(embedded_ruby_module, "embedded_init");
  embedded_python_module = dlopen();
  embedded_python_init = dlsym(embedded_python_module, "embedded_init");

  init_func init_functions[] = {
    embedded_python_init,
    application_main,
    NULL
  };
  embedded_ruby_init(run_init_functions, init_functions);
}

void
run_init_functions(init_func *init_functions)
{
  if (init_functions[0]) {
    init_functions[0]();
    run_init_functions(init_functions + 1);
  } else {
    application_main();
  }
}

質疑応答はここで時間切れでした。

たしかにこれで動きそうです。

会議後にはこんなアイディアもありました。

これはどういうことかというと、アプリケーション側ではRUBY_INIT_STACKに必要なデータを用意するだけにして、実際の呼び出しは別の共有ライブラリーの方に移す、という実装にすればいいんじゃない?ということです。

RUBY_INIT_STACKruby_init_stack()というアドレスを引数にとるAPIを呼び出しているだけなので、このアイディアも実現できます。こんなイメージです。

void
main(void)
{
  int address;

  embedded_ruby_module = dlopen();
  embedded_ruby_init = dlsym(embedded_ruby_module, "embedded_init");
  embedded_ruby_init(&address);

  application_main();
}
void
embedded_init(int *address)
{
  ruby_init_stack((VALUE *)address);
  /* ... */
}

milter managerでも実装できます。(アプリケーション本体に手を入れられないケースではこの方法の実装は難しいでしょう。)

「アプリケーションがRUBY_INIT_STACKを呼ばないといけない問題」は複数の方法で解決できますね!このような場で話をするといろんなアイディアを聞けて便利ですね!みなさんも積極的に実装の話をしてはいかがでしょうか。

RUBY_INIT_STACK問題が解決したので、複数言語のインタプリターを組み込んだケースのことを想像してみたところ、次はforkで問題にあたりそうです。Ruby以外の言語が複数のスレッドを作っている場合が問題になりそうです。

外部ライブラリーとGC

質疑応答では触れられませんでしたが、外部のライブラリーが使用しているメモリー量をRuby(mrubyもCRubyも)のGCシステムが知らないために適切なタイミングでGCが動かない問題についても補足します。

発表では「外部からRubyのGCシステムにメモリー使用量を通知する仕組みが必要だと思う」と話しました。

CRubyのTypedDataにはメモリー使用量を返すAPIがありますが、それは使えないはずです。それを使うと、GCシステム側が情報をpullする仕組みになるからです。pullする仕組みにすると、いつpullするの?pullした情報をどうやって管理するの?あたりが大変になりそうです。

通知する仕組みだとこれらの問題を解決できそうです。

ということで、gc-triggerという拡張ライブラリーを作ってみました。このライブラリーは他の拡張ライブラリーに使用メモリー通知用のAPIを提供します。他の拡張ライブラリーがこのAPIを使ってメモリー使用量を通知すると、適切なタイミングでGC.startを実行します。

本来はCで提供しているAPIを呼んだほうがよいですが、テスト用・説明用にRubyのAPIも用意したので、そっちを使って使い方を説明します。

ここでは、例としてrcairoを使います。rcairoは画像を扱うからです。画像を扱う拡張ライブラリーはRuby管理外のメモリー使用量が大きめなので、この問題に遭遇しやすいケースなのです。

次のコードは1000個画像オブジェクトを生成します。どの画像オブジェクトも参照されていないのですぐにGCで回収できます。

require "cairo"

width = 6000
height = 6000
1000.times do |i|
  Cairo::ImageSurface.new(:argb32, width, height)
end

gc-triggerでメモリー使用量の増減を通知するコードは次のようになります。

require "gc-trigger"
require "cairo"

def image_surface_finalizer(size)
  lambda do |id|
    GCTrigger.update_memory_usage(-size)
  end
end

def create_image_surface(width, height)
  size = 4 * width * height
  surface = Cairo::ImageSurface.new(:argb32, width, height)
  GCTrigger.update_memory_usage(size)
  ObjectSpace.define_finalizer(surface, image_surface_finalizer(size))
  surface
end

width = 6000
height = 6000
1000.times do |i|
  create_image_surface(width, height)
end

画像オブジェクトのサイズは、単純化して「1画素のサイズ(4バイト)×縦×横」で計算しています。

それぞれの場合でのメモリー使用量をグラフにすると次のようになります。従来の方はメモリー使用量が安定しませんが、gc-triggerを使った方はメモリー使用量が一定になります。

メモリー使用量

発表で話したアイディアを動くようにしてみました。これで通じるでしょうか。

OSS Gateワークショップ2016-05-28

発表でも紹介しましたが、OSS Gateワークショップ2016-05-28を東京Ruby会議11と同時開催しました。東京Ruby会議11でポスターを展示していたので、そこではじめて知ったという方も多かったのではないでしょうか。

OSS GateおよびOSS Gateワークショップを紹介していて痛感したことは「口頭での説明でないとちゃんと伝えられない」ということでした。たとえば、「私はすごくないのでメンターできないです。。。」に対して口頭では伝わるように回答できますが、資料では伝わらないです。これだとスケールしないので口頭での説明でなくても伝わる資料の作成が必要です。

ポスターを展示しての紹介は想像以上に効果があり、OSS Gateワークショップ2016-07-30の登録者が10人以上増えました。東京Ruby会議11実行委員会に協力してもらえて本当によかったです。ありがとうございました。

ちなみに、想像以上に登録者が増えたおかげでメンターが足りなそうです。OSS開発に参加したことがある人(技術力不問)でOSSの開発に参加する人が増えるといいなぁと思う人は「メンター」としてOSS Gateワークショップ2016-07-30に登録してください。すでに定員オーバーでキャンセル待ちでの登録になりますが、気にしないでください。キャンセル待ち扱いでも気にせずに当日来てください。入れます。

まとめ

5月28日に開催された東京Ruby会議11での発表とOSS Gateワークショップ2016-05-28について補足しました。

東京Ruby会議11の参加者アンケートはまだ回答を受け付けているので参加した方はぜひ回答してください。

2016-06-01

milterプロトコル

これはPostfix Advent Calendar 2014の10日目の記事です。

Postfixは2.3からmilterという仕組みをサポートしました。milterとは「mail filter」の略で、送信したメールまたは受信したメールになんらかの処理を行う仕組みです。もともとはSendmailが作った仕組みですが、Sendmail・Postfix以外のMTAでもサポートしているMTAがあります。

Postfix 2.3でのmilterサポートは限定的な機能のみのサポートでしたが、Postfix 2.6ではSendmailとほぼ同等の機能をサポートしています。SendmailとPostfixでマクロ(後述)名が違うなど一部非互換な部分もありますが、SendmailでもPostfixでも同様に使えます。

Postfixユーザーの人にとっては、milterでどのようなことができるか、content filterとの使い分けはどうすればよいか、という情報が有用かと思いますが、ここではそのような説明をしません。そのような説明ではなくmilterプロトコルについて説明します。なお、milterプロトコルについての情報は、Postfixユーザーの人にとっては有用ではなく、milter開発者にとってもそんなに有用ではなく、一部のmilterライブラリー開発者にとっては有用*1です。Postfix Advent Calendarっぽくなくてごめんなさい。

しかも、残念ながら2014/12/10中に完成しませんでした。。。続きはいつかどこかで。。。

はじめに

多くのmilterはlibmilterというSendmailが提供しているライブラリーを利用して実装します。libmilter内でmilterプロトコルを実装しているのでmilter開発者がプロトコルの詳細を気にする必要はありません。libmilterのAPIを知っていれば十分です。しかし、ここではlibmilterのAPIは説明しません。milterを作るための情報を探している人は次のページを参考にしてください。

  • CまたはC++でmilterを実装したい人向け
  • Rubyでmilterを実装したい人向け
  • Pythonでmilterを実装したい人向け
    • pymilter:libmilterのPythonバインディング。
    • ppymilter:Pythonでmilterプロトコルを実装したライブラリー。
  • Perlでmilterを実装したい人向け
  • Haskellでmilterを実装したい人向け
    • RPF:HaskellでのReceiver's Policy Frameworkの実装。この中にmilterプロトコルの実装がある。

普通の人向けの情報は提供したので、ここからは普通の人向けではない情報です。

実はmilterプロトコルの仕様書はありません。Sendmailでの挙動が事実的な仕様になっています。Postfixや各言語でのmilterプロトコルの実装は、Sendmail・libmilterのソースコード*2や、実際のSendmail・libmilterの動きを参考にmilterプロトコルを実装したものです*3

ただ、milterプロトコルについてまとめたメモはあります。Perlでmilterプロトコルを実装した人が残したもの(英語)です。まとめた時期が古い(milterプロトコルのバージョンでいうと2)ので新しいmilterプロトコル(最新はバージョン6)についての情報はないのですが、基本的なことからまとまっているので有用な情報です*4

ベースのやりとり

それではmilterプロトコルについて説明します。

まずはベースとなるやりとりの方法について説明します。

milterプロトコルはMTAとmilter間でやりとりします。基本的に、MTAからコマンドを送り、milterが応答する、という流れになります。

MTA ー コマンド → milter
    ←   応答   ー

コマンドと応答のフォーマットは同じです。フォーマットは次の通りです。

| データサイズ(4バイト)| データID(1バイト) | データ |

「データサイズ」はネットワークバイトオーダーで表現されています。データサイズはデータサイズ自体のサイズ(4バイト)を含みません。「データID」と「データ」を合わせたサイズ(バイト数)です。

「データID」はこのデータの種類を示す1文字のASCII文字です。例えば、「CONNECTというコマンド」を表すデータIDは「C」です。

「データ」はデータIDに関連するデータです。データIDによって中身が変わります。0バイトのときもあります。

milterプロトコルを実装するときは、まずは、このフォーマットをデコードする処理とこのフォーマットにエンコードする処理を実装します。リンク先はmitler managerでの実装です。

一連のやりとり

ベースのやりとりがわかったところで、次は一連のやりとりについて説明します。個々のやりとりはこのあと説明します。

milterプロトコルはSMTPと関連が深いです。SMTPの1つのセッションがmilterプロトコルでの1つのセッションに対応します。milterプロトコルでの1つのセッションは次のようになります。ただし、一部省略しています。

MTA                           milter
ネゴシエーションコマンド →
                          ← ネゴシエーション応答
接続コマンド              →
                          ← 応答
HELOコマンド              →
                          ← 応答
MAIL FROMコマンド         →
                          ← 応答
RCPT TOコマンド1          →
                          ← 応答
RCPT TOコマンド2          →
                          ← 応答
                          ...
RCPT TOコマンドn          →
                          ← 応答
DATAコマンド              →
                          ← 応答
ヘッダーコマンド1         →
                          ← 応答
ヘッダーコマンド2         →
                          ← 応答
                          ...
ヘッダーコマンドn         →
                          ← 応答
ヘッダー終了コマンド      →
                          ← 応答
本文チャンクコマンド1     →
                          ← 応答
本文チャンクコマンド2     →
                          ← 応答
                          ...
本文チャンクコマンドn     →
                          ← 応答
メッセージ終了コマンド    →
                         (← メッセージ変更コマンド)
                          ← 応答

なお、SMTPでメールトランザクションが複数ある場合は、それに対応して「MAIL FROMコマンド」から「メッセージ終了コマンド」の「応答」までを複数回繰り返します。

SMTPのセッションと似ていますね。「HELOコマンド」、「MAIL FROMコマンド」、「RCPT TOコマンド」、「DATAコマンド」はそれぞれSMTPの対応するコマンドを実行したときに実行されます。SMTPで「DATA」の後に指定したメッセージはパースされて個別にmilterに送られます。

コマンドの種類

次は個々のやりとりのうち「MTAから送るデータ」について説明します。

MTAから送るデータをコマンドと呼びます。

コマンドのID、名前、説明は次の通りです。最初の行が凡例です。

  • #{ID}: #{名前}: #{説明}
  • A: アボート: メールトランザクション中で終了するコマンド。
  • B: 本文チャンク: 本文のチャンク(一部)を送るコマンド。本文が大きい場合は複数のチャンクにわけて渡される。(複数の本文チャンクコマンドが送られる。)
  • C: 接続: 接続してきたSMTPクライアントに関する情報を送るコマンド。
  • D: マクロ定義: コマンドに対する付加情報を送るコマンド。詳細は後述。
  • E: メッセージ終了: メッセージ全体を送ったことを知らせるコマンド。このコマンドの応答でだけメールを変更できる。詳細は後述。
  • H: HELO: SMTPのHELO/EHLOに関する情報を送るコマンド。
  • L: ヘッダー: メッセージのヘッダーを1つ送るコマンド。複数のヘッダーがある場合は複数回送られる。
  • M: MAIL FROM: SMTPのMAIL FROMに関する情報を送るコマンド。
  • N: ヘッダー終了: すべてのヘッダーを送ったことを知らせるコマンド。
  • O: ネゴシエーション: milterセッション開始時に動作を調整するコマンド。特殊なコマンド。詳細は後述。
  • Q: 終了: このセッションを終了することを指示するコマンド。
  • R: RCPT TO: SMTPのRCPT TOに関する情報を送るコマンド。複数のRCPT TOを指定した場合は複数回送られる。
  • T: DATA: SMTPのDATAに関する情報を送るコマンド。
  • U: 未知: SMTPで未知のコマンドを指定されたときに送るコマンド。

IDにはコマンドの名前と関連がある文字が使われていることが多いので、データを生で見てもなんとなくわかることがあります。

重要なコマンドについて補足します。

マクロ定義コマンド

マクロ定義コマンドは特殊なコマンドです。

マクロ定義コマンドは次に送るコマンドの付加情報を送るコマンドです。付加情報はキーと値のペアのリストです。なお、マクロ定義コマンドに対してmilterは応答しません。

次の各コマンドの前にMTAが送ります。

  • 接続コマンド
  • HELOコマンド
  • MAIL FROMコマンド
  • RCPT TOコマンド(それぞれのRCPT TOコマンド毎)
  • DATAコマンド
  • ヘッダー終了コマンド
  • メッセージ終了コマンド

図にすると次のようになります。

MTA                           milter
ネゴシエーションコマンド →
                         ← ネゴシエーション応答
マクロ定義コマンド       →
接続コマンド             →
                         ← 応答
マクロ定義コマンド       →
HELOコマンド             →
                         ← 応答
マクロ定義コマンド       →
MAIL FROMコマンド        →
                         ← 応答
マクロ定義コマンド       →
RCPT TOコマンド1         →
                         ← 応答
マクロ定義コマンド       →
RCPT TOコマンド2         →
                         ← 応答
                         ...
マクロ定義コマンド       →
RCPT TOコマンドn         →
                         ← 応答
マクロ定義コマンド       →
DATAコマンド             →
                         ← 応答
ヘッダーコマンド1        →
                         ← 応答
ヘッダーコマンド2        →
                         ← 応答
                         ...
ヘッダーコマンドn        →
                         ← 応答
マクロ定義コマンド       →
ヘッダー終了コマンド     →
                         ← 応答
本文チャンクコマンド1    →
                         ← 応答
本文チャンクコマンド2    →
                         ← 応答
                         ...
本文チャンクコマンドn    →
                         ← 応答
マクロ定義コマンド       →
メッセージ終了コマンド   →
                         (← メッセージ変更コマンド)
                         ← 応答

ただし、これはSendmailの動作です。Postfixは「ヘッダーコマンド」と「本文チャンク」のときもマクロ定義コマンドを送ってきます。「ヘッダーコマンド」の場合は「ヘッダー終了コマンド」用のマクロ定義コマンドを送ってきて、「本文チャンクコマンド」の場合は「メッセージ終了コマンド」用のマクロ定義コマンドを送ってきます。

マクロ定義コマンドのフォーマットは次の通りです。

| データサイズ(4バイト)| M | コマンドID(1バイト) | マクロ定義リスト |

「コマンドID」はどのコマンド用のマクロ定義なのかを示すIDです。前述のコマンドIDと同じ値です。例えば、HELOコマンド用のマクロ定義コマンドなら「H」です。

「マクロ定義リスト」のフォーマットは次の通りです。

| キー1(NULL終端文字列) | 値1(NULL終端文字列) | キー2(NULL終端文字列) | 値2(NULL終端文字列) | ... |

キーは「{...}」というように「{」と「}」で囲まれている場合もあれば囲まれていない場合もあります。

Postfixが送るマクロ定義はmilter_connect_macrosなどで指定します。

ネゴシエーションコマンド

ネゴシエーションコマンドはかなり特殊なコマンドです。セッション開始時にMTAからmilterに1度だけ送ります。

ネゴシエーションコマンドでMTAとmilter間で次のことを決めます。

  • milterプロトコルのバージョン
  • アクション(milterがどんな操作をするか)
  • ステップ(MTAとmilter間のやりとりについて)

ネゴシエーションコマンドでは次のようにMTAとmilterで送りあうデータのフォーマットは同じです。

MTA ー ネゴシエーションコマンド → milter
    ← ネゴシエーションコマンド ー

MTAからmilterに送るときはMTAがサポートしている機能の情報を送り、milterがMTAに応答するときはmilterが要求する機能の情報を送ります。MTAがサポートしていない機能をmilterが要求するとネゴシエーションは失敗し、セッションは確立しません。

データのフォーマットは次の通りです。

| データサイズ(4バイト)| O | バージョン(4バイト) | アクション(4バイト) | ステップ(4バイト) | マクロリスト(0バイト以上) |

「バージョン」、「アクション」、「ステップ」はすべて4バイトの符号なし整数で、ネットワークバイトオーダーです。アクション、ステップは各ビットに意味を割り当てています。(フラグになっているということです。)

「バージョン」はMTAが提示したバージョンよりも低いバージョンをmilterが指定しても構いません。例えば、Postfixは「6」を提示し、milterが「2」を返してもよいです*5

「アクション」のフラグは次の通りです。最初の行が凡例です。

  • #{値}: #{説明}
  • 1 << 0: ヘッダーを追加できる
  • 1 << 1: 本文を変更できる
  • 1 << 2: 宛先を追加できる
  • 1 << 3: 宛先を削除できる
  • 1 << 4: ヘッダーを変更できる
  • 1 << 5: 隔離(配送せずにholdキューに入れる)できる
  • 1 << 6: 差出人を変更できる
  • 1 << 7: パラメーター付きで宛先を追加できる(RCPT TO:<forward-path> [ SP <rcpt-parameters> ] <CRLF>の<rcpt-parameters>を使うかどうか)
  • 1 << 8: milterがマクロ定義を上書きできるか(詳細は後述するマクロリストを参照)

「ステップ」のフラグは次の通りです。最初の行が凡例です。

  • #{値}: #{説明}
  • 1 << 0: MTAが接続コマンドを送らない
  • 1 << 1: MTAがHELOコマンドを送らない
  • 1 << 2: MTAがMAIL FROMコマンドを送らない
  • 1 << 3: MTAがRCPT TOコマンドを送らない
  • 1 << 4: MTAが本文チャンクコマンドを送らない
  • 1 << 5: MTAがヘッダーコマンドを送らない
  • 1 << 6: MTAがヘッダー終了コマンドを送らない
  • 1 << 7: milterがヘッダーコマンドに応答しない
  • 1 << 8: MTAが未知コマンドを送らない
  • 1 << 9: MTAがDATAコマンドを送らない
  • 1 << 10: MTAがスキップ応答(後述)をサポートしているかどうか
  • 1 << 11: MTAが拒否した宛先もmilterに送るかどうか
  • 1 << 12: milterが接続コマンドに応答しない
  • 1 << 13: milterがHELOコマンドに応答しない
  • 1 << 14: milterがMAIL FROMコマンドに応答しない
  • 1 << 15: milterがRCPT TOコマンドに応答しない
  • 1 << 16: milterがDATAコマンドに応答しない
  • 1 << 17: milterが未知コマンドに応答しない
  • 1 << 18: milterがヘッダー終了コマンドに応答しない
  • 1 << 19: milterが本文チャンクコマンドに応答しない
  • 1 << 20: MTAがヘッダーの値の先頭の空白を削除しない。「Subject: xxx」とあった場合、先頭の空白を削除しないで「 xxx」をmilterに送るということ。このフラグを落とすと先頭の空白を削除して「xxx」をmilterに送る。

「MTAが○○コマンドを送らない」について補足します。このフラグを使うとmilterが必要のないコマンドを送ってこないようにMTAに指示することができます。例えば、メール本文が必要ない場合は本文チャンクコマンドを送らないようにすることで、通信量が減りパフォーマンスがあがります。

同様に「milterが○○コマンドに応答しない」について補足します。後述しますが、応答時にはmilterは「このメールを拒否する」、「このメールは受け取る」などをMTAに伝えることができます。milterが特定のコマンドに対して必ず「次の処理にいってくれ」と応答することが事前にわかっている場合は、MTAはmilterの応答を待たずに次の処理にいきます。これもパフォーマンス向上につながります。

「マクロリスト」はユーザーがmilter_connect_macrosなどで指定した値をmilterが上書きするためにあります*6。指定した場合は次のようなフォーマットになります。

| マクロの種類(4バイト) | 空白またはコンマ区切りのマクロ名のリスト(NULL終端の文字列) |

「マクロの種類」は4バイトの符号なし整数で次の値のどれかです。最初の行が凡例です。

  • #{値}: #{説明}
  • 0: CONNECTコマンド用マクロ
  • 1: HELOコマンド用マクロ
  • 2: MAIL FROMコマンド用マクロ
  • 3: RCPT TOコマンド用マクロ
  • 4: DATAコマンド用マクロ
  • 5: メッセージ終了コマンド用マクロ
  • 6: ヘッダー終了コマンド用マクロ

マクロ定義コマンドでは「コマンドID」を使っていましたが、ここでは独自の値になることに注意してください。

他のコマンド

2014/12/10中ではまとめきれなかったので他のコマンドは省略します。ごめんなさい。

応答の種類

次は個々のやりとりのうち「milterから送るデータ」について説明します。

milterから送るデータを応答と呼びます。

応答のID、名前、説明は次の通りです。最初の行が凡例です。

  • #{ID}: #{名前}: #{説明}
  • +: 宛先追加: 宛先を追加する応答。複数の宛先を追加するときは複数回送る。
  • -: 宛先削除: 宛先を削除する応答。「何番目の宛先」という形式で指定する。
  • 2: パラメーター付きで宛先追加: <rcpt-parameters>付きで宛先を追加する。
  • a: 受理: このメールは受理するという応答。milterプロトコルでのメールトランザクションの処理はここで終了し、これ以降MTAからコマンドは送られてこない。
  • b: 本文置換: 本文を変更する応答。変更後の本文が大きい場合は複数のチャンクに分けて応答する。
  • c: 継続: 次のコマンドへいってくれという応答。基本はこれを返す。
  • d: 破棄: 処理中のメッセージを破棄する。SMTPでのメールトランザクションの処理はここで終了するので、milterプロトコルでのメールトランザクションも終了する。
  • e: 差出人変更: 差出人を変更する応答。
  • h: ヘッダー追加: ヘッダーを追加する応答。複数のヘッダーを追加するときは複数回送る。ヘッダーは末尾に追加される*7
  • i: ヘッダー挿入: 指定した位置にヘッダーを挿入する応答。
  • m: ヘッダー変更: 指定した位置のヘッダーを変更する。
  • p: 処理中: milterの処理に時間がかかっていることをMTAに知らせる応答。MTA側のタイムアウト時間を伸ばせる。
  • q: 隔離: このメールを隔離するという応答。
  • r: 拒否: このメールの受信を拒否するという応答。SMTPでは5XX系のレスポンスになる。
  • s: スキップ: このメールトランザクションの処理を途中でやめるという応答。
  • t: 一時拒否: このメールの受信を一時的に拒否するという応答。SMTPでは4XX系のレスポンスになる。
  • y: SMTPレスポンス設定: SMTPレスポンスのコードとメッセージを設定する。

「宛先追加」や「ヘッダー変更」などメッセージ本体を変更する応答は「メッセージ終了コマンド」の応答のときでないと返せないことに注意してください。

2014/12/10中ではまとめきれなかったので応答のデータフォーマットなどの説明は省略します。ごめんなさい。

まとめ

milterプロトコルについてまとめきれませんでした。もしかしたら後で追記するかもしれません。

文章にするのは時間がかかるのですが、口頭で説明するのは文章にするよりも時間がかからないので、興味のある人は、イベントなどでmilter managerの作者にばったり会ったときにでも聞いてください。

(途中ですが)milterプロトコルの説明を見るとmilterではいろんなことができることがわかります。milterを使うとPostfixの設定だけではできないようなことも実現できます。Postfixの設定だけでは実現できないなぁと思ったときは、milterも組み合わせることも検討してみてください。実際、クリアコードではRubyで小さなmilter(100行以内のもの)を書いて、Postfixの設定だけでは実現できない(実現しようとすると複雑になる)メールシステムの構築をお手伝いしていたりします*8。困ったときは、公開できる情報ならmilter managerのメーリングリスト、有償でもよいならお問い合わせフォームを使って相談してみてください。

*1 libmilterのバインディング開発者にはそんなに有用ではないが、自分でmilterプロトコルを実装する開発者には有用。

*2 オープンソースでよかったですね!

*3 たぶん。少なくともmilter managerのmilterプロトコル実装はそう。

*4 milter managerを実装しているときはこの情報を知らなかったので一から調べました。

*5 以前のPostfixはダメでしたが、Postfixにパッチを送って取り込んでもらったので、今はできるようになっています。

*6 こんな機能あったのか。。。milter managerでは実装していないな。。。

*7 MTAの実装依存かも。

*8 100行以上になるような込み入った機能を実現するmilterをRubyで開発することもあります。

つづき: 2015-01-07
2014-12-10

milter manager 2.0.0 リリース

2013年7月29日にmilter managerの約2年ぶりのメジャーバージョンアップである2.0.0をリリースをしました。

前回ククログでmilter managerを紹介したのはmilter manager 1.6.9をリリースしたときである約2年前なので、milter managerとはどういったものなのかmilter managerのウェブサイトから引用します。

milter managerはmilterを使って効果的に迷惑メール対策(スパムメール対策・ウィルスメール対策)を行うフリーソフトウェアです。

milter managerはmilterをサポートしているSendmailやPostfixといったMTAと一緒に使うことができます。Ubuntu、CentOS、FreeBSDなどのプラットフォーム上で動作します。

メジャーバージョンアップですが、1.8系とは互換性があるため、設定ファイルを変更せずにそのまま簡単にアップデートすることができます。前回のリリースから大きな変更もないのにメジャーバージョンをあげて2.0.0にした理由は、開発が継続していることと動作が安定していることをアピールするためです。

milter managerの前回のメジャーアップデートは2011年6月10日でした。そこから、10回目のリリースが今回のメジャーバージョンアップです。リリースの間隔が6ヶ月あいてしまったこともありましたが、こつこつと改良を続けてきました。マイナーバージョンアップリリースはmilter managerのメーリングリストだけでアナウンスしていたため、メーリングリストに参加していない方々には開発の様子が見えづらいものでした。しかし、メジャーバージョンアップとなると大きなイベントですので、これを機に、メーリングリストに参加していない方々にも、開発が継続していること、milter managerがより便利になり、より安定したことをアピールします。

実際、動作実績が増え*1、ユーザーのみなさんが問題を報告してくださったおかげで、1.8.0の頃よりさらに動作が安定しました。milter manager本体はもちろんですが、Rubyでmilterを書くための機能であるRuby/milterもかなり安定しました。Rubyでmilterを実装する機会が増え、さまざまなノウハウが溜まりました。これらのノウハウがRuby/milterに反映されています。

2.0.0は1.8.0よりも確実によくなっていると自信を持って言えます。これまでmilter managerを使ったことがなかったみなさんもぜひ試してみてください!

milter manager 2.0.0 リリースに対する反応

リリースメールを以下のMLに流したところ、いくつか反応があったので紹介します。

日本語:

英語:

全て個人宛てに返信があったので、メールをくれた方の名前やメールアドレスは伏せます。

海外からの反応1:

I've noticed milter manager, now I will package it into Fedora.

Fedora にパッケージを追加するための作業をしてくれるそうです。いつかやりたいと思っていたことなので大変嬉しいご提案です。

海外からの反応2:

How about writing milters in Python, rather than just Ruby?

Ruby だけではなく Python で milter を書くのはどう?とのことですが、既に pymilter というプロジェクトがあるのでご紹介しておきました*2

海外からの反応3:

Use milter in postfix will cause the performance drop about 20% due to the implement in Postfix compared to policyD(tested on Postfix 2.3.5, I am not sure if it have been improved in latest Postfix version). That's why I prefer to PolicyD (greylist, blacklist, whitelist, IP reputation/based detection, etc). Content-based scanning will cost a lot of time which may caused session time-out issue. My suggestion is to let MTA do the basic/fast checking and ask other back-end system/module to scan the content.

BTW, it's a great idea to consolidate those milters into one milter mgr. I will try it later.

自分が知っている情報を共有してくれています。PolicyDというプロダクトは知らなかったので、参考になりました。milter manager のアイデアをとても褒めてくれています。こういったコメントはとても励みになりますね!

test-mails プロジェクトを始めました

milter manager 2.0.0 リリースとは直接関係ないのですが、最近milter managerの関連プロジェクトとして始めた test-mails というプロジェクトがあるので紹介します。

test-mailsプロジェクトは、メールシステムのテストに必要なメールを共有することを目的としたプロジェクトです。共有したメールはオープンソースとして利用できるので、だれでも自分のメールシステムのテストに有効活用できます。

メールシステムの開発をしていると、テストのためにとても多くのパターンのメールが必要になります。そのようなメールを個人で集めたり作成したりするのは大変です。そんなとき、自由に再利用できるテストメール集があると便利だと思いませんか?このようなメール集を共有すると、自分で多くのテスト用メールを作成したり管理したりするコストが削減でき、網羅的なテストをしやすくなることが期待できます。

興味のある人はmilter-manager/test-mailsを参照してください。現在はまだ少しだけですが、テストで使用可能なメールを追加してあります。

*1 10万mails/day以上のメールを処理するシステムでも使われています

*2 もちろん、milter managerはPythonで書かれたmilterも組合せて使うことができます。

つづき: 2014-01-09
2013-07-31

milter managerとは?

先日milter manager 1.6.9をリリースしたので、久しぶりにmilter managerについて紹介します。

特長

milter managerはサーバーサイドの迷惑メール対策システムをできるだけ簡単に構築するためのフリーソフトウェアです。その秘密は強力な設定記述力にあります。

多くのソフトウェアはパラメーターを変更することでプログラムの動作をカスタマイズすることができます。milter managerにはRubyという本格的なプログラミング言語のインタプリタが内蔵されていて、単にパラメーターを変更するだけではなく、システムにインストールされている迷惑メール対策プラグインを自動検出する(設定の自動化)、怪しい送信元は迷惑メールチェックを強化するが認証済みユーザは緩くする、といったこともできてしまいます。しかも、多くの便利な設定は組み込みで含まれているため、そのまま使うだけでも自動でシステムに合わせて効果的な設定になってくれます。

もちろん、導入・運用を簡単にしたいという場合は、外部のメールシステムやアプライアンス製品の利用も十分選択肢に入るはずです。この場合は費用対効果やメールシステムを外部に出してもよいか、どこまでカスタマイズする必要があるか、などが判断基準になることでしょう。milter managerにはユーザ向けメーリングリストもあるので、検討段階などで疑問点などがあった場合はそこで質問してみてください。

まとめ

久しぶりにmilter managerについて紹介しました。

最近のmilter managerはパフォーマンス改善もおこなっており、個人サーバーから大規模なメールシステムまでいろいろな環境で効率的に動かせるようになっています。機会があったらmilter managerの利用を検討してみてはいかがでしょうか。

以下は参考情報です。

2011-04-27

milter manager 1.6.4リリース

先日、milter managerのスケーラビリティ向上計画を紹介しましたが、そこに書いた内容を実装したmilter manager 1.6.4をリリースしました。(メーリングリストでのアナウンス

スケーラビリティ向上のための主な追加機能は以下の3点です。

  1. マルチプロセス対応
  2. libev対応
  3. writeの非同期化

このうち、「マルチプロセス対応」と「libev対応」は「実験的」な扱いということで、デフォルトでは無効になっています。「writeの非同期化」は有効になっています。このため、Postfixや子milterのreadが遅い場面では劇的にスループットが向上するでしょう*1

また、スケーラビリティ向上に加えてRuby対応を強化しています。代表的な変更はRuby 1.9.2に対応したことです。Rubyでmilterを書くのがより便利になりますね。

まとめ

milter manager 1.6.4をリリースしたので代表的な変更内容を紹介しました。

*1 例えば大量の宛先を削除するmilterを使っている場合。この場合、Postfixのcleanupのreadが遅い。

タグ: milter manager | Ruby
2011-01-20

milter managerのスケーラビリティ向上計画

milter managerの開発を始めたのが2008年の9月なので、2年と少し前になります。この当時はPostfixでmilterを使うというのはそれほど一般的ではありませんでした。しかし、最近ではPostfixでmilterを使うというのが選択肢の1つになっています。これは、Postfixのmilter対応が強化されたことと、Web上にPostfixでmilterを利用した事例が増えてきたことが大きな要因でしょう。

このような状況になると、より規模の大きなメールシステムでもmilter managerを利用することを想定しないといけません*1。そのため、milter managerがよりスケールするように内部の構造を改良する予定です。これらの改良は近いうちにmilter manager 1.6.3としてリリースする予定です。1.6.Xシリーズの間は「実験的」な扱いで機能を提供し、1.8.0になった段階で「正式」な機能扱いとする予定です。

改良は以下の2点です。

  1. マルチプロセス対応
  2. イベントループの高速化

マルチプロセス対応ではマルチコアのマシンでの性能向上を目指します。イベントループの高速化では1プロセスあたりの性能向上を目指します。ちなみに、現在はシングルプロセス・シングルスレッドで、イベントループにはpoll(2)*2を使っています。

マルチプロセス対応

マルチコアを活かすためにはマルチプロセス対応にするか、マルチスレッド対応にするかという選択肢がありますが、milter managerではマルチプロセスを採用します。これは、milter manager内部にあるRubyインタプリタとの相性からです。スレッド毎にRubyインタプリタを持てるのであればマルチスレッドでもよかったのですが、現在のRuby(MRI)はそれができないため処理を直列化する必要があり、マルチスレッドにしてもマルチコアを活かしきれないと判断しました。そのためマルチプロセスを選択しています。

以下のようにPostfixのmaster/smtpdと同じような構成になります。

シングルプロセス(現行):
 inet@10025
------------ milter manager

マルチプロセス(改良後):

 inet@10025                           +- milter manager (worker1)
------------ milter manager (master) -+- milter manager (worker2)
                                     ...
                                      +- milter manager (workerN)

Postfixのmasterが「milter manager (master)」に対応し、smtpdが「milter manager (worker)」に対応します。

milter manager (master)が外部からの接続(smtpdからの接続)を受け付け、実際の処理はmilter manager (worker)が行います。外部からの接続の受け付けは、常にmilter manager (master)が行うため、外からはシングルプロセスの構成と区別がつきません。つまり、外部(smtpd)からはmilter managerがどちらの構成を使っていても同じように扱えるため、Postfixの設定を変更する必要はありません。

マルチプロセス構成にするとmilter manager (worker)の数だけCPUコアを利用して並列に処理することができます。

イベントループの高速化

メールシステムは複数のメールを同時に扱います。milter managerはI/Oを多重化し、I/O待ちの間に別のメールを処理することにより、同時に複数のメールを処理しています。つまり、I/OとI/Oを待っている間の処理を繰り返して動作することになります。この繰り返しがイベントループです。ここを高速化することにより、1プロセスで同時に処理できるメール数を増やします。

規模の大きなメールシステムではより同時接続数が増えますが、このとき、I/Oを多重化する仕組みによって性能特性が変わってきます。伝統的なI/O多重化の仕組みはselect(2)やpoll(2)ですが、これらはI/O対象の数(監視するファイルディスクリプタ数)が増えると大きく性能が落ちることが知られています*3。ちなみに、現在はmilter managerはpoll(2)を使っています。

milter managerは1つのメールに対して複数のmilterに接続するため、I/O対象の数がすぐに大きくなってしまいます。例えば、6つmilterを利用している場合は、1メールに対してI/O対象の数が6つ増えます。同時に30メールを処理する場合はI/O対象の数が180になるわけです。そのため、規模が大きいメールシステムではpoll(2)の性能劣化が顕著になるくらいまでI/O対象の数が増えてしまうことになります。

そこで、milter managerのイベントループでpoll(2)ではなくlibevを利用できるようにします。libevを利用することにより、I/O対象の数が増えてもイベントループでの急激な性能劣化を防ぐことができ、より大きな規模のメールシステムでもサービスを提供できるようになります。

まとめ

現在、より大きな規模のメールシステムでもmilter managerを利用できるようにするためにmilter managerを改良しています。まだ改良が終わっていないので少し気が早いのですが、どのような改良を進めているかを紹介しました。

どちらの機能も設定を1つ変更するだけで切り替えることができるようになるので、milter manager 1.6.3では簡単にここで紹介した改良を試すことができるようになる予定です。楽しみにしていてください。

最後になりましたが、今年もmilter managerをよろしくお願いします。

*1 これまでは中規模のメールシステム(アカウント数が1000件いかない程度)でも利用できることを想定していた。

*2 GLibのメインループのバックエンドがpoll(2)だから。

*3 100以下ならそれほど変わらない

2011-01-07

Rubyでmilterを作れる - milter manager 1.6.2リリース

(まだMLではアナウンスしていませんが)milter manager 1.6.2をリリースしました。

このリリースではRubyでmilterを書くために必要な機能がすべて実装されました。これまではメールを拒否したり、隔離する機能は提供していたのですが、差出人を変更する、宛先を追加する、本文を変更するなどメッセージを変更する機能は部分的にしか対応していませんでした。このリリースからメッセージを変更する機能も提供されるようになったので、ほとんどのmilterはRubyでも実装できるようになりました。

Rubyでmilterを書くとどのようになるかはhbstudy#15の発表内容で紹介しています。

milter manager 1.6.2のインストール方法・アップグレード方法は以下にまとまっています。

milter managerを使ってRubyでメールフィルターを作ってみてはいかがでしょうか?

タグ: milter manager | Ruby
2010-11-24

hbstudy#15発表資料: milter managerで簡単迷惑メール対策

hbstudy#15でmilterについて発表しました。

milter managerで簡単迷惑メール対策

公開しているスライドの内容は実際に使ったものと異なっています*1。実際に使ったものや当日の雰囲気などが気になる人はUstreamの録画を観てください。

スライドのPDFやソース、当日使ったmilterなどはスライドページからダウンロードできます。milterはスライドのソースと同じアーカイブに含まれています。

スライドはRabbitというRubyで書かれたフリーソフトウェアで作成しています。Ruby界隈ではとても有名なプレゼンツールなのですが、インフラ界隈ではあまり有名ではないので、当日使ったRabbitの機能を簡単に説明しておきます。

スライドの下にでていたうさぎとかめは、うさぎがページ数を、かめが経過時間を示しています。うさぎが前を走っていればペースが速い、かめが前を走っていれば間に合わない、というようにプレゼンテーションの進み具合が視覚的に一瞬でわかるのがよいところです。「残り7:35」と出てもいい感じで進んでいるのかどうかがわからないですよね。また、タイマー用のテキストがスライドの中にあると不自然ですが、うさぎとかめがスライドの中にいても不自然ではないので、発表者用のビューではなく、表示用のビューに表示することができるのもよいところです。PCの画面とディスプレイの内容を同じ内容にできるので、ディスプレイ側の表示だけおかしくなっている、という状態を防ぐことができます。ただし、聞いている人が発表よりもうさぎとかめの方が気になってしまうという問題があります。ここは発表者の腕でなんとかする必要があります。

また、携帯電話でRabbitを遠隔操作していました。仕組みを図示したのものがとちぎRuby会議02の資料公開にあります。今回も大体これと同じ構成です。処理の流れは以下の通りです。

  1. 携帯電話のi-modeブラウザで「次のページへ遷移」リンクを辿る ーHTTP→
  2. サーバ上で動いているHTTPサーバ(Rabrick: Rubyで書かれている)がノートPC上のRabbitプロセスの「次のページへ遷移メソッド」をdRubyを使って呼び出す ーdRuby→
  3. ーSSHトンネル→
  4. ーdRuby→ ノートPC上のRabbitプロセスの「次のページへ遷移メソッド」が呼び出されて、次のページへ遷移

システムとしてはカッコイイのですが、目の前のノートPC上で動いているRabbitを操作するためにインターネットを経由しているので多少タイムラグがあります。

それでは、以下に発表内容を簡単にまとめておきます。省略している部分が気になる場合は上述のUstreamの録画を観てください。

概要

話すこと

タイトルは「milter managerで簡単迷惑メール対策」でしたが、参加者がそれほどメール環境になじみの深い人ばかりではなさそうだったので、milter managerそのものの話は最後に少しする程度にしました。内容のほとんどはmilter managerがベースとしている技術であるmilterについてです。

milterを言葉や図だけで理解するのは少し大変なので、今回は実際にありうる例を動かしながら理解していきます。

milterについて

milterとは

milterとはSendmailが作ったメールフィルターの仕組みです。実際には、この仕組みの中で使われるネットワークプロトコルや、メールフィルターを開発するためのAPI、メールフィルターそのもののことも「milter」と呼ぶことがあります。ただ、多くの場合は文脈からどれを指しているかがわかるので、それほど混乱することはありません。

milterは2001年9月にリリースされたSendmail 8.12.0から正式サポートされています。9年の歴史がある枯れた技術といえます。また、SendmailだけではなくPostfixでもサポートされています。Postfixでは2006年6月にリリースされた2.3.0からサポートされ、現時点の最新版2.7.1ではmilterのほとんどの機能がサポートされています。こちらも4年の歴史があり、もう十分に実践に投入できるほど使われています。

つまり、現時点ではmilter*2を用いてメールフィルター機能を実現することは「実験的な試み」ではなく「いくつかある有力な選択肢の1つ」といえます。

しかし、日本語でのmilterの情報が少ないことは事実です。milterに関する英語の情報はmilter.orgに集まっています。milter.orgにはmilter*3を検索する機能や開発者向けの情報なども載っています。

たとえば、Technical Overview - Control Flowに載っている以下の擬似コードを見れば、複数のmilter*4を同時に用いたときにどのような動作になるかはわかります*5

For each of N connections {
  For each filter
    process connection (xxfi_connect)
  For each filter
    process helo (xxfi_helo)
  MESSAGE:For each message in this connection (sequentially) {
    For each filter
      process sender (xxfi_envfrom)
    For each recipient {
      For each filter
        process recipient (xxfi_envrcpt)
    }
    For each filter {
      process DATA (xxfi_data)
      For each header
        process header (xxfi_header)
      process end of headers (xxfi_eoh)
      For each body block
        process this body block (xxfi_body)
      process end of message (xxfi_eom)
    }
  }
  For each filter
    process end of connection (xxfi_close)
}

日本語でもこれと同じような情報を読むことができれば、milter利用の敷居が低くなるかもしれません。ここでは、実際に動かしながらmilter*6がどのように動くのかを確認していきます。

milterの挙動

milterの挙動: 3行で

上記のmilterの動作フローをざっくりとまとめると、以下のようになります。

  • 本文は直列(前のmilterの影響を受ける)
  • 本文以外は並列(前のmilterの影響を受けない)
  • 詳細な結果は最後に返す

milterは迷惑メール対策に使われることがほとんどです。迷惑メールの手法は複雑化しているので、1つの迷惑メール対策方法で完璧ということは不可能です。複数のmilter(迷惑メール対策方法)を組み合わせて効果的な迷惑メール対策システムを構築する必要があります。

しかし、milterは上記のような動作のため、milter1の結果を使ってmilter2の挙動を変えるということが難しくなっています。milter1の詳細な結果を使いたければ、メール全体の処理が終わるのを待たなくてはいけません。milter2がエンベロープ情報(送信元や宛先)を取得した段階で実行する迷惑メール対策方法*7を採用している場合は、メール全体の処理が終わるまで待つのは効率的ではありません。

milterがどのように動作するかがわかれば、複数のmilterをどのように組み合わせることが効果的かを検討することができます。そのため、milterを用いて効果的な迷惑メール対策システムを構築する場合はmilterの挙動を理解しておく必要があります。

milterの挙動の詳細

milterプロトコル

miltrの挙動を理解するためには、以下の3つの要素があることを理解するのが早道です。要素の名前はここでの説明のために便宜的に付けたもので、milterで一般的な用語ではないので注意してください。

  • ステージ: milterが処理を行うタイミング
  • アクション: milterが処理を行ったときにメールサーバに返す結果
  • メッセージ変更機能: メールフィルターとして行えること

ステージ

milterが処理を行うタイミングはSMTPのコマンド+αだけあります。どのようなタイミングかを一言解説を付けています*8

  • connect: SMTPクライアントが接続してきたとき。
  • helo: SMTPクライアントがHELO/EHLOコマンドを実行したとき。
  • mail from: SMTPクライアントがMAIL FROMコマンドを実行したとき。
  • rcpt to: SMTPクライアントがRCPT TOコマンドを実行したとき。
  • data: SMTPクライアントがDATAコマンドを実行したとき。
  • header: メールサーバがメールのヘッダー1つを解析したとき。ヘッダーの数だけn回発生する。
  • end of header: メールサーバがメールのヘッダーをすべて解析し終わったとき。
  • body: メール本文を処理しているとき。
  • end of message: メール全体を処理したとき。

「メッセージ変更機能」は「end of message」のときしか実行できません。「メッセージ変更機能」は後述します。

アクション

milterは処理を行うたびにメールサーバに結果を返さないといけません。メールサーバに返せる結果は以下の通りです。

  • continue: 処理を続行する。普通はこれ。
  • accept: 受信する。このmilterは以降の処理を行わない。
  • reject: メールを受信拒否する。SMTPレベルでは500番台の受信拒否になる。
  • tempfail: メールを一時受信拒否する。SMTPレベルでは400番台の受信拒否になる。
  • discard: メールを受信するが、配送せずにそのまま廃棄する。このmilterは以降の処理を行わない。
  • quarantine: メールを受信するが、配送はしない。(本当はアクションではないので、end of messageステージのときしか使えない。)

メッセージ変更

以下のようなメッセージ変更機能があります。メールフィルターとしては十分な機能です。

  • From変更
  • To追加
  • To削除
  • 本文変更
  • ヘッダー追加
  • ヘッダー削除
  • ヘッダー変更

それでは、以下、実例を元に実際の動作を確認します。

ケース1: mail fromでaccept

実例は5つ用意しましたが、ここでは1つだけ紹介します。他はスライドやUstreamの録画を観てください。

ケース1: accept

mail fromステージでacceptアクションを返した場合です。SMTP Authをしている場合は何も処理をしないmilterが多くあります。その場合はこのような動作になります。

mail fromステージでacceptアクションを返すmitlerはRubyで実装するとこのようになります。これはスライドのアーカイブの中のmilters/milter-accept.rbに入っています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require 'milter'

class MilterAccept < Milter::ClientSession
  def envelope_from(from)
    p from
    accept
  end

  def envelope_recipient(to)
    p to
  end
end

command_line = Milter::Client::CommandLine.new
command_line.run do |client, _options|
  client.register(MilterAccept)
end

このとき、SMTPクライアントがRCPT TOコマンドを送ったらどうなるでしょうか。

ケース1: 答え

この場合、milterはacceptを返しているのでメールを受信します。ただし、SMTPクライアントがMAIL FROMより後のRCPT TO以降のコマンドを送ってもmitler側では何も起こりません。それ以降のステージではメールサーバがmilterに通知しないからです。

発表時には実際にSMTPを話しながら動作を確認しています。この様子はUstreamの録画を観てください。また、他の例もUstreamの録画やスライドページを見てください。

milter managerが便利なところ

milter managerの特徴

例を使ってmilterの動作を確認した通り、milterを連携させることが難しいケースがあります。milter managerを使うことでそこを補うことができます。

例えば、「milter評価モード」機能を使うことによって、「milter1がreject/temp failを返した」という情報をmilter2で利用することができます。通常はend of messageまで待たなければ情報を利用することができませんが、reject/temp failはどのステージでも利用することができるので、もっと早い段階で情報を利用することができます。

以下、milter managerの利用例を1つ紹介します。他の利用例などはまた別の機会にでも紹介できるとよいですね。

管理例: ユーザ毎の設定

milter managerを使うことによってユーザ毎に迷惑メール対策方法を変えることができるシステムを構築することができます。例えば、MySQLにユーザ毎の設定を格納しておきます。格納した情報は各milterではなくmilter managerが参照してmilterの挙動を動的に制御します。各milterがそれぞれMySQLの情報を参照するようなシステムにすることもできますが、そうすると、複数のmilterを使う場合に大変です。それぞれのmilterがMySQLに対応し、さらに、MySQL内の情報をどのように使うかを判断しなければいけません。milter managerを使うことにより、その部分を一括管理することができます。

まとめ

管理例: ユーザ毎の設定

迷惑メールが多様化しているため、複数のmilterでそれに対応する必要があります。ただし、複数のmilterを利用した場合の挙動は少し複雑です。まるで理解しがたいというものではなく、少し落ち着いて考えれば理解できるものなので、milterを利用する場合は一度くらいは落ち着いてmilterの挙動を確認しましょう。

複数のmilterを利用する場合はmitler managerも一緒に導入するとより便利です。

お知らせ

宣伝1: クリアコードではプログラミングが好きな開発者を2名募集中です。興味がある人は応募してみてください。よさそうな人を知っていたら教えてあげてください。

宣伝2: クリアコードの開発者は全員オープンソースソフトウェアの開発に関わって磨いてきた技術力を持っています。オープンソースソフトウェアに関して技術的にお困りのことがあったらぜひご相談ください。もちろん、milterに関することもOKです。

*1 その場だけで使う用のものや再配布用ではないものを抜いている。

*2 これは仕組みのこと。

*3 この「milter」はmilterの仕組みを使って実現されたメールフィルターそのもののこと。

*4 これもメールフィルターそのもののこと。

*5 ちょっとウソ。milterの他のことも少し知らないとこれだけだとわからない。

*6 これもメールフィルターのこと。これ以降は注釈をつけませんがわかるはずです。

*7 例えば、SPFやGreylisting(グレイリスト)。SPFは迷惑メール対策にも使われることが多い。

*8 わかりやすさを重視したので、厳密には違う場合もあるので注意してください。

タグ: milter manager | Ruby
2010-09-20

お知らせ: hbstudy#15: milter managerで簡単迷惑メール対策

お知らせです。

来週の週末9/18(土)の19:00-開催されるhbstudy#15milter managerを紹介します。

hbstudyはインフラエンジニアのための勉強会で、#15では迷惑メール対策ソフトウェアmilter managerと監視ソフトウェアZabbixがテーマです。同じ回で違う分野を扱うので、知識の幅が広がりやすいのがよいですね。

「複数のmilterを同時に使ったときの挙動を理解できること」を目標に話す予定です。まだ少し空いているようなので興味のある方は参加してみてください。

2010-09-07

Debian GNU/Linux上でRPMパッケージ・Yumリポジトリを作って公開する方法: milter managerを例にして

注: FedoraやCentOSのRPMパッケージャーが書いた文章ではありません。FedoraやCentOSのRPMパッケージメンテナになりたい方はFedoraやCentOSが公式に配布している文書の方をお勧めします。例えば、How to create an RPM package - FedoraProjectという文書があります。

Debianパッケージの作り方と公開方法: groongaを例にしてのRPM版のような内容です。

ここでは、迷惑メール対策ソフトウェアmilter managerを例にしてDebian GNU/Linux上でCentOS 5.4向けのRPMパッケージを作成し、それを提供するYumリポジトリを作成・公開する方法を紹介します。RPMパッケージの作成では、1つのspecファイルから複数のパッケージを作成します。この方法は、ライブラリを提供するソフトウェアの場合に多く用いられます。

また、Yumリポジトリを登録するRPMも作成します。そのため、ユーザには以下のようにYumリポジトリを登録してもらうことになります。

Yumリポジトリを登録:

% sudo rpm -Uvh http://milter-manager.sourceforge.net/centos/5/milter-manager-repository-1.0.0-0.noarch.rpm

インストール:

% sudo yum install -y milter-manager

ここで紹介する方法を自動化するスクリプト群はmilter managerのリポジトリで公開されています。ここで紹介する方法でRPMパッケージ・Yumリポジトリを作成・公開する場合は参考にしてください。

それでは、まずはパッケージの作り方です。

パッケージの作り方

RPMパッケージを作るためには.specを作ります。ここでは、複数のパッケージを生成する.specを作成するので、まず、どれをどのパッケージにいれるかを検討します。

構成

milter managerはmilterプロトコルを実装したライブラリ、それを利用したmilter管理アプリケーション、ユーティリティツールで構成されています。この場合、以下のように複数のパッケージに分解することが多いようです。

  • libXXX: ライブラリを使用しているソフトウェアを実行するために必要なファイルを提供するパッケージ。/usr/lib/libXXX.soなどを提供する。
  • libXXX-devel: ライブラリを使用しているソフトウェアをビルドするために必要なファイルを提供するパッケージ。/usr/include/XXX/*.hなどを提供することが多い。libXXXに依存する。
  • XXX: (ライブラリではなく)コマンドを提供するパッケージ。libXXXに依存する。

milter managerの場合は以下のパッケージを作成することとします。

  • libmilter-toolkit: /usr/lib/libmilter-client.soなどを提供する。
  • libmilter-toolkit-devel: /usr/include/milter-manager/milter/client.hなどを提供。
  • libmilter-compatible: libmilterとABI互換の/usr/lib/libmilter.soを提供する。
  • libmilter-compatible-devel: libmilterとAPI互換の/usr/include/milter-manager/libmilter/*.hを提供する。
  • milter-manager: milter-managerコマンドを提供する。
  • milter-manager-munin-plugin: milter managerの統計情報を収集するmunin-nodeのプラグインを提供する。

雛形

複数のパッケージを作成する.specは以下のような内容になります。簡単ですね、とは言えないくらいの長さです*1

Summary: 簡単な説明
Name: (メイン)パッケージ名(「milter-manager」など)
Version: バージョン番号(「1.5.0」など)
Release: リリースバージョン(最初は「0{?dist}」にしておけばよい)
License: ライセンス(「GPLv3+」など)
URL: ソフトウェアのサイトのURL
     (「http://milter-manager.sourceforge.net/」など)
Group: (メイン)パッケージが属するグループ
       (「System Environment/Daemons」など。
         「/usr/share/doc/rpm/GROUPS」に一覧がある。)
Source: ソースのアーカイブのURL
        (「http://downloads.sourceforge.net/milter-manager/milter-manager-1.5.0.tar.gz」など)
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-%(%{__id_u} -n)
           (常に↑でOK)
BuildRequires: ビルドに必要なパッケージ
BuildRequires: 必要な分だけ書く
Requires: 動作に必要なパッケージ
Requires: 必要な分だけ書く

%description
詳細なパッケージの説明。複数行になってもOK。

% package -n サブパッケージ名
          (「-n」をつけないと「『パッケージ名』-『サブパッケージ名』」
            というパッケージ名になるので、「libXXX」というパッケージ名を
            つけるときは「-n」をつけないといけない)
Summary: サブパッケージの簡単な説明
Group: サブパッケージの属するグループ

% description -n サブパッケージ名
              (「-n」の意味は「%package」と同じ)
詳細なサブパッケージの説明。複数行になってもOK。

%prep
%setup -q
%build
%configure オプション
          (「--with-default-effective-user=milter-manager」など)
make %{?_smp_mflags}

% install
rm -rf %{buildroot}
make install DESTDIR=%{buildroot}

%clean
rm -rf %{buildroot}

%files
%defattr(-, root, root, -)
(メイン)パッケージに含めるファイルを指定。

%files -n サブパッケージ名
%defattr(-, root, root, -)
サブパッケージに含めるファイルを指定。

%changelog
* 日付 名前 <メールアドレス>
- (バージョン番号-リリース番号)
- new upstream release

1つの.specで1つのパッケージを作る場合は%package -n サブパッケージ名%description -n サブパッケージ名%files -n サブパッケージ名がなくなるだけで、基本的な記述は変わりません。

milter managerの場合は以下のような.specになっています。%prepostなどが増えていますが、これらは、それぞれインストール前・インストール後に実行するシェルスクリプトを指定しているだけです。

そこそこ長いので、詳細に興味がない場合は読み飛ばしてください。

Summary: A milter to use milters effectively
Name: milter-manager
Version: 1.5.0
Release: 11%{?dist}
License: GPLv3+, LGPL3+, AGPL3+, GFDL, Public Domain
URL: http://milter-manager.sourceforge.net/
Group: System Environment/Daemons
Source: http://downloads.sourceforge.net/milter-manager/milter-manager-1.5.0.tar.gz
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-%(%{__id_u} -n)
BuildRequires: intltool
BuildRequires: gettext
BuildRequires: gcc
BuildRequires: make
BuildRequires: glib2-devel
BuildRequires: ruby
BuildRequires: ruby-devel
Requires: glib2
Requires: ruby
Requires: libmilter-toolkit = %{version}-%{release}
Requires(pre): /usr/bin/getent, /usr/sbin/useradd
Requires(pre): /usr/bin/id, /usr/sbin/groupadd
Requires(post): /sbin/chkconfig
Requires(preun): /sbin/service, /sbin/chkconfig
Requires(postun): /sbin/service, /sbin/chkconfig, /usr/sbin/userdel

%description
milter manager administrates milters instead of MTA to reduce milter
administration cost and combine milters flexibly.

%package -n libmilter-toolkit
Summary: A milter protocol library
Group: System Environment/Libraries

%description -n libmilter-toolkit
Both of client-side and server-side milter protocol are implemented.
This package contains the library files required for running services
built using libmilter-toolkit.

%package -n libmilter-toolkit-devel
Summary: Development files for libmilter-toolkit
Group: Development/Libraries
Requires: libmilter-toolkit = %{version}-%{release}

%description -n libmilter-toolkit-devel
This package contains the headers, and other support files
required for developing applications against libmilter-toolkit.

%package -n libmilter-compatible
Summary: libmilter API and ABI compatible milter library
Group: System Environment/Libraries
Requires: libmilter-toolkit = %{version}-%{release}

%description -n libmilter-compatible
A libmilter API and ABI compatible library based on libmilter-toolkit.
This package contains the library files required for running services
built using Sendmail libmilter or libmilter-compatible.

%package -n libmilter-compatible-devel
Summary: Development files for libmilter-compatible
Group: Development/Libraries
Requires: libmilter-compatible = %{version}-%{release}
Requires: libmilter-toolkit-devel = %{version}-%{release}

%description -n libmilter-compatible-devel
This package contains the headers, and other support files
required for developing applications against libmilter-compatible.

%package -n milter-manager-munin-plugin
Summary: Munin plugin for milter manager
Group: System Environment/Libraries
Requires: milter-manager = %{version}-%{release}
Requires: munin-node

%description -n milter-manager-munin-plugin
This package contains the munin plugin for munin-node.

%prep
%setup -q

%build
%configure								\
    --with-default-effective-user=milter-manager			\
    --with-default-effective-group=milter-manager			\
    --with-default-socket-group=smmsp					\
    --with-default-pid-file=/var/run/milter-manager/milter-manager.pid	\
    --with-default-connection-spec=unix:/var/run/milter-manager/milter-manager.sock
make %{?_smp_mflags}

%install
rm -rf %{buildroot}
make install DESTDIR=%{buildroot}

mkdir -p %{buildroot}%{_initrddir}
install -m 755 data/init.d/redhat/milter-manager %{buildroot}%{_initrddir}/milter-manager

mkdir -p %{buildroot}%{_sysconfdir}/sysconfig
install -m 644 data/init.d/redhat/sysconfig/milter-manager %{buildroot}%{_sysconfdir}/sysconfig/milter-manager

mkdir -p %{buildroot}%{_sysconfdir}/cron.d
install -m 600 data/cron.d/redhat/milter-manager-log %{buildroot}%{_sysconfdir}/cron.d/milter-manager-log

mkdir -p %{buildroot}%{_localstatedir}/run/milter-manager/

mkdir -p %{buildroot}%{_sysconfdir}/httpd/conf.d/
cat <<EOC > %{buildroot}%{_sysconfdir}/httpd/conf.d/milter-manager-log.conf
Alias /milter-manager-log/ /var/lib/milter-manager/public_html/log/
EOC

mv %{buildroot}%{_datadir}/milter-manager/munin/ %{buildroot}%{_datadir}/
mkdir -p %{buildroot}%{_sysconfdir}/munin/plugin-conf.d/
cat <<EOC > %{buildroot}%{_sysconfdir}/munin/plugin-conf.d/milter-manager
[milter_manager_*]
  user milter-manager
  env.logdir /var/lib/milter-manager/public_html/log
EOC

%clean
rm -rf %{buildroot}

%pre
if ! /usr/bin/getent group milter-manager &>/dev/null; then
    /usr/sbin/groupadd -r milter-manager || \
        %logmsg "Unexpected error adding group \"milter-manager\". Aborting installation."
fi
if ! /usr/bin/id milter-manager &>/dev/null; then
    /usr/sbin/useradd -r -s /sbin/nologin -c 'milter manager' \
        -d %{_localstatedir}/lib/milter-manager --create-home \
        -g milter-manager milter-manager || \
        %logmsg "Unexpected error adding user \"milter-manager\". Aborting installation."
fi

%post
/sbin/chkconfig --add milter-manager
/bin/mkdir -p /var/run/milter-manager
/bin/chown -R milter-manager:milter-manager /var/run/milter-manager

%post -n milter-manager-munin-plugin
/usr/sbin/munin-node-configure --shell | \
    grep -e '\(milter_manager_\|\(postfix\|sendmail\)_processes\)' | \
    sh
[ -f /var/lock/subsys/munin-node ] && \
    /sbin/service munin-node restart > /dev/null 2>&1
:

%preun
if [ $1 -eq 0 ] ; then
    /sbin/service milter-manager stop > /dev/null 2>&1
    /sbin/chkconfig --del milter-manager
fi

%postun
if [ $1 -ge 1 ] ; then
    /sbin/service milter-manager condrestart > /dev/null 2>&1
fi

if [ $1 -eq 0 ]; then
    /usr/sbin/userdel -r milter-manager || \
        %logmsg "User \"milter-manager\" could not be deleted."
fi

%postun -n milter-manager-munin-plugin
if [ $1 -eq 0 ]; then
    rm %{_sysconfdir}/munin/plugins/milter_manager_* > /dev/null 2>&1
    rm %{_sysconfdir}/munin/plugins/postfix_processes > /dev/null 2>&1
    rm %{_sysconfdir}/munin/plugins/sendmail_processes > /dev/null 2>&1
    [ -f /var/lock/subsys/munin-node ] && \
        /sbin/service munin-node restart > /dev/null 2>&1
    :
fi

%files
%defattr(-, root, root, -)
%doc ChangeLog ChangeLog.toolkit README README.ja NEWS NEWS.ja TODO
%doc %{_datadir}/milter-manager/license/
%doc %{_datadir}/gtk-doc/html/milter-manager/
%{_bindir}/milter-manager-log-analyzer
%{_sbindir}/milter-manager
%{_includedir}/milter-manager/milter/manager.h
%{_includedir}/milter-manager/milter/manager/
%{_libdir}/libmilter-manager.*
%{_libdir}/milter-manager/binding/
%{_libdir}/milter-manager/module/
%{_libdir}/pkgconfig/milter-manager.pc
%{_mandir}/man1/milter-manager.*
%{_mandir}/man1/milter-manager-log-analyzer.*
%{_mandir}/ja/man1/milter-manager.*
%{_mandir}/ja/man1/milter-manager-log-analyzer.*
%{_initrddir}/milter-manager
%{_datadir}/milter-manager/admin/
%{_sysconfdir}/milter-manager/cron.d/
%{_sysconfdir}/milter-manager/init.d/
%{_sysconfdir}/milter-manager/rc.d/
%{_sysconfdir}/cron.d/
%config %{_sysconfdir}/sysconfig/milter-manager
%config %{_sysconfdir}/milter-manager/milter-manager.conf
%config %{_sysconfdir}/milter-manager/defaults/
%config %{_sysconfdir}/milter-manager/applicable-conditions/
%config %{_sysconfdir}/httpd/conf.d/milter-manager-log.conf

%defattr(-, milter-manager, milter-manager, 0755)
%dir %{_localstatedir}/run/milter-manager/

%files -n libmilter-toolkit
%defattr(-,root,root)
%doc ChangeLog ChangeLog.toolkit README README.ja NEWS NEWS.ja TODO
%doc %{_datadir}/milter-manager/license/
%{_bindir}/milter-test-client
%{_bindir}/milter-test-server
%{_bindir}/milter-performance-check
%{_libdir}/libmilter-core.so.*
%{_libdir}/libmilter-client.so.*
%{_libdir}/libmilter-server.so.*
%{_mandir}/man1/milter-test-client.*
%{_mandir}/man1/milter-test-server.*
%{_mandir}/man1/milter-performance-check.*
%{_mandir}/ja/man1/milter-test-client.*
%{_mandir}/ja/man1/milter-test-server.*
%{_mandir}/ja/man1/milter-performance-check.*

%files -n libmilter-toolkit-devel
%defattr(-,root,root)
%doc ChangeLog ChangeLog.toolkit README README.ja NEWS NEWS.ja TODO
%doc %{_datadir}/milter-manager/license/
%doc %{_datadir}/gtk-doc/html/milter-manager/
%{_includedir}/milter-manager/milter/core.h
%{_includedir}/milter-manager/milter/core/
%{_includedir}/milter-manager/milter/client.h
%{_includedir}/milter-manager/milter/client/
%{_includedir}/milter-manager/milter/server.h
%{_includedir}/milter-manager/milter/server/
%{_libdir}/libmilter-core.so
%{_libdir}/libmilter-core.la
%{_libdir}/libmilter-client.so
%{_libdir}/libmilter-client.la
%{_libdir}/libmilter-server.so
%{_libdir}/libmilter-server.la
%{_libdir}/pkgconfig/milter-core.pc
%{_libdir}/pkgconfig/milter-client.pc
%{_libdir}/pkgconfig/milter-server.pc

%files -n libmilter-compatible
%defattr(-,root,root)
%doc ChangeLog ChangeLog.toolkit README README.ja NEWS NEWS.ja TODO
%doc %{_datadir}/milter-manager/license/
%{_libdir}/milter-manager/libmilter.so.*

%files -n libmilter-compatible-devel
%defattr(-,root,root)
%doc ChangeLog ChangeLog.toolkit README README.ja NEWS NEWS.ja TODO
%doc %{_datadir}/milter-manager/license/
%doc %{_datadir}/gtk-doc/html/milter-manager/
%{_includedir}/milter-manager/libmilter/
%{_libdir}/milter-manager/libmilter.so
%{_libdir}/milter-manager/libmilter.la
%{_libdir}/pkgconfig/libmilter.pc

%files -n milter-manager-munin-plugin
%defattr(-,root,root)
%doc ChangeLog ChangeLog.toolkit README README.ja NEWS NEWS.ja TODO
%doc %{_datadir}/milter-manager/license/
%{_datadir}/munin/
%config %{_sysconfdir}/munin/plugin-conf.d/

%changelog
* Thu Feb 17 2010 Kouhei Sutou <kou@clear-code.com>
- (1.5.0-11)
- new upstream release

* Thu Oct 29 2009 Kouhei Sutou <kou@clear-code.com>
- (1.4.1-0)
- new upstream release

* Thu Oct 13 2009 Kouhei Sutou <kou@clear-code.com>
- (1.4.0-0)
- new upstream release

* Wed Sep 16 2009 Kouhei Sutou <kou@clear-code.com>
- (1.3.1-0)
- new upstream release

* Wed Aug 12 2009 Kouhei Sutou <kou@clear-code.com>
- (1.3.0-0)
- new upstream release

* Fri Jul 17 2009 Kouhei Sutou <kou@clear-code.com>
- (1.2.0-0)
- new upstream release

* Fri Jul 03 2009 Kouhei Sutou <kou@clear-code.com>
- (1.1.1-0)
- new upstream release

* Tue Jun 02 2009 Kouhei Sutou <kou@clear-code.com>
- (1.1.0-0)
- initial 1.1.x development seriese release

* Thu Apr 16 2009 Kouhei Sutou <kou@clear-code.com>
- (1.0.0-1)
- initial stable release

ビルド: rinse

Debian GNU/Linux上でCentOS用のRPMパッケージをビルドすることは茨の道です。そこで、CentOS用のchroot環境を作って、そこでビルドすることにします。こうすることで、CentOSの実機がなくてもビルドできる上に、きれいな環境でビルドすることもできます。

Debian GNU/LinuxやUbuntuのchroot環境を作るにはdebootstrapが便利です。CentOSやFedoraのchroot環境を作るにはrinseが便利です。

/var/lib/chroot/centos-i386/以下にCentOS 5.4 i386用のchroot環境を構築するには以下のようにします。

% sudo aptitude install -y rinse
% sudo mkdir -p /var/lib/chroot/centos-i386/etc/rpm/
% sudo sh -c "echo i386-centos-linux > /var/lib/chroot/centos-i386/etc/rpm/platform"
% sudo rinse --arch i386 --distribution centos-5 --directory /var/lib/chroot/centos-i386/

/etc/fstabに以下を追記:

/dev          /var/lib/chroot/centos-i386/dev     none   bind     0 0
devpts-chroot /var/lib/chroot/centos-i386/dev/pts devpts defaults 0 0
proc-chroot   /var/lib/chroot/centos-i386/proc    proc   defaults 0 0

本質的な部分はsudo rinse ...だけです。sudo rinse ...の前にあるRPMパッケージで利用するプラットフォームを指定している箇所はrinseのバグを回避するためです*2。これがないとamd64のDebian GNU/Linux環境でi386のCentOS環境を作成することができません。

/etc/fstabへの追記は、再起動する度にmountしなおすのが面倒だからです。

この作業はbuild-in-chroot.shの中で自動化されています。

ビルド: chroot

CentOSのchroot環境ができたら、その環境にビルド専用アカウントを作成し、そのユーザでビルドします。ここで紹介する作業はbuild-rpm.shの中で自動化されています。ここでは、その中の一部を説明します。

まず、ビルド用ユーザが存在しない場合はユーザを作成します。

1
2
3
4
USER_NAME=milter-manager-build
if ! id $USER_NAME >/dev/null 2>&1; then
  useradd -m $USER_NAME
fi

次に、そのビルド用ユーザが実行するビルドスクリプトを作成します。.tar.gzや.specなどのビルドに必要なファイルはchroot環境の/tmp/以下(/var/lib/chroot/centos-i386/tmp/以下)に事前にコピーしておきます。それぞれの処理内容はコメントとして説明しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
BUILD_SCRIPT=/tmp/build-milter-manager.sh
VERSION=`cat /tmp/milter-manager-version`

cat <<EOF > $BUILD_SCRIPT
#!/bin/sh

# RPMパッケージのビルドは~/rpm/以下で行う。
if [ ! -f ~/.rpmmacros ]; then
    cat <<EOM > ~/.rpmmacros
%_topdir \$HOME/rpm
EOM
fi

# RPMパッケージ作成に必要なディレクトリを作成。
# rpmdevtoolsパッケージに含まれているrpmdev-setuptreeコマンド
# でも同様のことができるよう。
mkdir -p rpm/SOURCES
mkdir -p rpm/SPECS
mkdir -p rpm/BUILD
mkdir -p rpm/RPMS
mkdir -p rpm/SRPMS

# ソースと.specを配置。
cp /tmp/milter-manager-$VERSION.tar.gz rpm/SOURCES/
cp /tmp/milter-manager.spec rpm/SPECS/

# RPMパッケージ作成。
rpmbuild -ba rpm/SPECS/milter-manager.spec
EOF

このビルドスクリプトをビルド用ユーザで実行します。

1
2
chmod +x $BUILD_SCRIPT
su - $USER_NAME $BUILD_SCRIPT

ビルドが成功するとビルド用ユーザの~/rpm/RPMS/i386/以下にRPMパッケージができます*3

RPMパッケージができたら、それらに署名をします。

署名

RPMでは、パッケージに署名することによりパッケージ作成者のなりすましを防止することができます。署名の検証を無効にすることもできるので、署名なしのRPMパッケージでもYumリポジトリで公開・インストールすることはできますが、よほどの理由がない場合は署名をするべきでしょう。

パッケージへの署名はパッケージ作成時にも作成後にも行うことができます。パッケージ作成後に行う場合はrpmコマンドの--resignオプションを使います。署名する鍵は_gpg_nameで指定します。

% rpm -D "_gpg_name Kouhei Sutou <kou@clear-code.com>" --resign XXX.rpm

これをそれぞれの.rpmに対して行います。

RPMパッケージに署名したら、署名済みRPM使ってYumリポジトリを作ります。

Yumリポジトリの作成

milter managerは安定版と開発版の2つのリリースラインがあります。そのため、以下のようなディレクトリ構成でYumリポジトリも2つ作ります。下の図のdevelopmentとstableがそれぞれYumリポジトリになります。各YumリポジトリはSRPMS, i386, x86_64ディレクトリを持ち、その下にビルドしたRPMパッケージを配置します。

.
+--- centos/
     +--- 5/
          +--- development
          |    +--- SRPMS/
          |    |    +--- milter-manager-1.5.0-0.src.rpm
          |    |    +--- ...
          |    +--- i386/
          |    |    +--- CentOS/
          |    |         +--- milter-manager-1.5.0-0.i386.rpm
          |    |         +--- ...
          |    +--- x86_64/
          |         +--- CentOS/
          |              +--- milter-manager-1.5.0-0.x86_64.rpm
          |              +--- ...
          +--- stable
               +--- SRPMS/
               |    +--- milter-manager-1.4.1-0.src.rpm
               |    +--- ...
               +--- i386/
               |    +--- CentOS/
               |         +--- milter-manager-1.4.1-0.i386.rpm
               |         +--- ...
               +--- x86_64/
                    +--- CentOS/
                         +--- milter-manager-1.4.1-0.x86_64.rpm
                         +--- ...

このように.rpmを配置したらSRPMS, i386, x86_64のそれぞれのディレクトリに対してcreaterepoコマンドを実行します。この作業はupdate-repository.shで自動化されています。

1
2
3
for dir in centos/5/*/*; do
    createrepo $dir
done

createrepoコマンドを実行するとそれぞれのディレクトリの下にrepodataというディレクトリが作成され、パッケージの情報が格納されます。

これでYumリポジトリは完成です。HTTPでアクセスできるところにアップロードしてください。

http://milter-manager.sourceforge.net/centos/以下でアクセスできるところにアップロードしたとすると、Yumのリポジトリ指定は以下のようになります。

/etc/yum.repos.d/milter-manager.repo:

[milter-manager]
name=milter manager for CentOS-$releasever
baseurl=http://milter-manager.sourceforge.net/centos/$releasever/stable/$basearch/
gpgcheck=1
enabled=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-milter-manager

gpgkeyはRPMに署名した時に使ったキーの公開鍵が入ったファイルを指定します。公開鍵は以下のコマンドで出力できます。

% gpg --export --armor "Kouhei Sutou <kou@clear-code.com>"

これを/etc/pki/rpm-gpg/RPM-GPG-KEY-milter-managerに置くことになります。

つまり、Yumリポジトリを登録するためには以下の作業が必要になります。

  1. /etc/yum.repos.d/milter-manager.repoの作成
  2. 公開鍵の配置

2段階にわかれているので面倒ですね。Yumリポジトリ登録の手間を軽減するために、「Yumリポジトリを登録するRPMパッケージ」を作りましょう。

Yumリポジトリ登録RPMの作成

ここで作成する「Yumリポジトリを登録するRPMパッケージ」はbuild-repository-rpm.shで自動化されています。

Yumリポジトリ登録に必要なものは「リポジトリを指定するファイル」と「RPMパッケージを署名している公開鍵」の2つです。それらをアーカイブした.tar.gzがソースのパッケージを作成します。

% tar cvzf milter-manager-repository.tar.gz milter-manager.repo RPM-GPG-KEY-milter-manager

.specはこのようになります。

Summary: milter manager RPM repository configuration
Name: milter-manager-repository
Version: 1.0.0
Release: 0
License: GPLv3+
URL: http://milter-manager.sourceforge.net/
Source: milter-manager-repository.tar.gz
Group: System Environment/Base
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-%(%{__id_u} -n)
BuildArchitectures: noarch

%description
milter manager RPM repository configuration.

%prep
%setup -c

%build

%install
%{__rm} -rf %{buildroot}

%{__install} -Dp -m0644 RPM-GPG-KEY-milter-manager %{buildroot}%{_sysconfdir}/pki/rpm-gpg/RPM-GPG-KEY-milter-manager

%{__install} -Dp -m0644 milter-manager.repo %{buildroot}%{_sysconfdir}/yum.repos.d/milter-manager.repo

%clean
%{__rm} -rf %{buildroot}

%post
rpm -q gpg-pubkey-1c837f31-4a2b9c3f &>/dev/null || \
    rpm --import %{_sysconfdir}/pki/rpm-gpg/RPM-GPG-KEY-milter-manager

%files
%defattr(-, root, root, 0755)
%doc *
%pubkey RPM-GPG-KEY-milter-manager
%dir %{_sysconfdir}/yum.repos.d/
%config(noreplace) %{_sysconfdir}/yum.repos.d/milter-manager.repo
%dir %{_sysconfdir}/pki/rpm-gpg/
%{_sysconfdir}/pki/rpm-gpg/RPM-GPG-KEY-milter-manager

%changelog
* Sat Feb 06 2010 Kouhei Sutou <kou@clear-code.com>
- (1.0.0-0)
- Initial package.

大事な部分はBuildArchitectures%postの部分です。

このYumリポジトリ登録RPMはプラットフォームに関係なく使えるのでnoarchを指定しています。

%postではRPMにも公開鍵を登録しています。

%post
rpm -q gpg-pubkey-1c837f31-4a2b9c3f &>/dev/null || \
    rpm --import %{_sysconfdir}/pki/rpm-gpg/RPM-GPG-KEY-milter-manager

このYumリポジトリ登録RPMはDebian GNU/Linux上でも作成できます。作成方法はCentOS上での方法と同じです。

% echo "%_topdir $HOME/rpm" > ~/.rpmmacros
% mkdir -p ~/rpm/{SOURCES,SPECS,BUILD,RPMS,SRPMS}
% cp milter-manager-repository.tar.gz ~/rpm/SOURCES/
% cp milter-manager-repository.spec ~/rpm/SPECS/
% rpmbuild -ba ~/rpm/SPECS/milter-manager-repository.spec

これで、~/rpm/RPMS/noarch/以下に.rpmができ、~/rpm/SRPMS/以下に.src.rpmができます。

ここで作成したRPMが冒頭で紹介したRPMです。このRPMを使うことで以下のようにパッケージをインストールできるのでしたね。

% sudo rpm -Uvh http://milter-manager.sourceforge.net/centos/5/milter-manager-repository-1.0.0-0.noarch.rpm
% sudo yum install -y milter-manager

Yumリポジトリの公開

ここまできたら、後はアクセスできる場所にアップロードするだけです。SourceForge.netではrsyncでアップロードできます。今回はhttp://milter-manager.sourceforge.net/centos/以下で公開したいので以下のようなコマンドになります。

% rsync -avz --exclude .gitignore centos/ \
    ktou,milter-manager@web.sourceforge.net:/home/groups/m/mi/milter-manager/htdocs/centos

rsyncではパスの最後の「/」の有無に注意してください。

まとめ

以上がRPMパッケージの作成と作成したRPMパッケージをYumリポジトリで公開する手順です。これら一連の手順を自動化したものはmilter managerのリポジトリで公開しています。読んでみてわかる通り、手順が多く、バージョンが上がる毎に手動で作業するのは大変です。簡単にバージョンアップに対応できるよう、自動化しておきましょう。

*1 Fedoraにはrpmdevtoolsパッケージに含まれているrpmdev-newspecで雛形を作れるようです。

*2 このバグを修正するパッチは作者に送りましたが、まだ取り込まれていません。

*3 src.rpmは~/rpm/SRPMS/以下にできます。

2010-03-03

2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|
タグ:
RubyKaigi 2015 sponsor RubyKaigi 2015 speaker RubyKaigi 2015 committer RubyKaigi 2014 official-sponsor RubyKaigi 2014 speaker RubyKaigi 2014 committer RubyKaigi 2013 OfficialSponsor RubyKaigi 2013 Speaker RubyKaigi 2013 Committer SapporoRubyKaigi 2012 OfficialSponsor SapporoRubyKaigi 2012 Speaker RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer badge_speaker.gif RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer
SapporoRubyKaigi02Sponsor
SapporoRubyKaigi02Speaker
RubyKaigi2009Sponsor
RubyKaigi2009Speaker
RubyKaigi2008Speaker