IPMessenger に UDP hole punching を実装してみる
Skype が NAT 越えに使用しているという UDP hole punching を勉強がてら IPMessenger で実装してみました。UDP hole punching については P2Pとファイアウォール というページが非常に参考になります。
まず、Perl の Net::IPMessenger モジュールを CPAN から持ってきます(自作のモジュールを試しに拡張してみたかっただけ)。そしてこれに二つのメッセージを追加します。ついでにスーパーノードを指定するメソッドを追加するのと、スーパノードに依頼を出すコマンド(examples/ip_messenger.pl が使う)を作っておきます。
% diff -u IPMessenger.pm.org IPMessenger.pm
--- IPMessenger.pm.org Thu Oct 12 00:58:26 2006 +++ IPMessenger.pm Thu Oct 11 01:14:58 2007 @@ -8,15 +8,15 @@ use Net::IPMessenger::RecvEventHandler; use Net::IPMessenger::MessageCommand; __PACKAGE__->mk_accessors( packet_count sending_packet user message nickname groupname username hostname socket serveraddr sendretry broadcast - event_handler debug + supernode event_handler debug ); our $VERSION = '0.06'; @@ -73,6 +73,11 @@ sub add_broadcast { my $self = shift; push @{ $self->broadcast }, shift; } +sub add_supernode { + my $self = shift; + $self->supernode(shift); +} + sub recv {}
% diff -u MessageCommand.pm.org MessageCommand.pm
--- MessageCommand.pm.org Tue Sep 26 16:47:56 2006 +++ MessageCommand.pm Tue Oct 2 23:57:49 2007 @@ -32,6 +32,8 @@ GETDIRFILE => 0x00000062, GETPUBKEY => 0x00000072, GETPUBKEY => 0x00000073, + ASKPUNCHING => 0x00000080, + ORDERPUNCHING => 0x00000081, ); our $MODE = 0x000000ff;
% diff -u RecvEventHandler.pm.org RecvEventHandler.pm
--- RecvEventHandler.pm.org Thu Oct 12 00:16:12 2006 +++ RecvEventHandler.pm Thu Oct 11 01:26:12 2007 @@ -97,6 +97,53 @@ ); } +sub ASKPUNCHING { + my $self = shift; + my $them = shift; + my $user = shift; + + my $nick = $user->option; + my $addr; + my $port; + for my $val ( values %{ $them->user } ) { + if ( $nick eq $val->nickname ) { + $addr = $val->peeraddr; + $port = $val->peerport; + last; + } + } + return unless $addr; + + $them->send( + { + peeraddr => $addr, + peerport => $port, + command => $them->messagecommand('ORDERPUNCHING'), + option => inet_ntoa( $them->socket->peeraddr ) + . "\0" + . $them->socket->peerport, + } + ); +} + +sub ORDERPUNCHING { + my $self = shift; + my $them = shift; + my $user = shift; + + my $option = $user->option; + my( $addr, $port ) = split /\0/, $option; + + $them->send( + { + peeraddr => $addr, + peerport => $port, + command => $them->messagecommand('BR_ENTRY'), + option => $them->my_info, + } + ); +} +
% diff -u CommandLine.pm.org CommandLine.pm
--- CommandLine.pm.org Thu Oct 5 15:07:47 2006 +++ CommandLine.pm Tue Oct 2 23:58:45 2007 @@ -83,6 +83,21 @@ return; } +sub ask { + my $self = shift; + my $sendto = shift; + + my $command = $self->messagecommand('ASKPUNCHING'); + $self->send( + { + peeraddr => $self->supernode, + command => $command, + option => $sendto, + } + ); + return; +} + sub exit { my $self = shift; }
こんな順序でメッセージを送信していけば NAT の問題さえなければ UDP hole punching が出来るはず。
(4) ORDER_PUNCHING (3) ASK_PUNCHING +---------------------------+ +--------------------> | | | (1) BR_ENTRY (1) BR_ENTRY | | +-----------------> E <---------------------+ | | | | | | | | | | | | | | | | | | | | | | | | | | | NAT NAT | | | | || (2) BR_ENTRY || | | +- B || A -----------------------------> C || D <-+ || (5) BR_ENTRY || || <----------------------------- || A : 192.168.1.2 B : 192.168.246.129 C : 192.168.1.3 D : 192.168.239.128 E : 192.168.1.4
まず、examples/ip_messenger.pl をノード B, D, E で起動します。B, D は E をスーパーノードとして指定しておきます。
ここから、メッセージ送信を開始します。
- 最初に B, D は、E 宛に BR_ENTRY メッセージを送信する
- B から D に BR_ENTRY メッセージ送信 C までは届くが NAT は越えない
- (2) が失敗したので E に ASK_PUNCHING メッセージを送信
- E は (1) で D から BR_ENTRY を受信して D を知っているので D に ORDER_PUNCHING メッセージを送信する。これは NAT を越えて届く
- D から B に BR_ENTRYを送信する。これも NAT を越えて届く
この後の B <-> D 間の通信は UDP hole punching の状態になり、普通に双方向のメッセージが届くようになります。
NAT によってはこの構成では正しく動かないものをあるみたいですが、UDP hole punching そのものを実装するのは結構簡単な印象でした。どちらかというとNAT 2台とさらに別ノードがある環境を構築するのが大変です。VMWare を使うと NAT の中のノードが簡単に作れるのでこんなテストのときにはお勧めなのかな。