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 をスーパーノードとして指定しておきます。
ここから、メッセージ送信を開始します。

  1. 最初に B, D は、E 宛に BR_ENTRY メッセージを送信する
  2. B から D に BR_ENTRY メッセージ送信 C までは届くが NAT は越えない
  3. (2) が失敗したので E に ASK_PUNCHING メッセージを送信
  4. E は (1) で D から BR_ENTRY を受信して D を知っているので D に ORDER_PUNCHING メッセージを送信する。これは NAT を越えて届く
  5. D から B に BR_ENTRYを送信する。これも NAT を越えて届く

この後の B <-> D 間の通信は UDP hole punching の状態になり、普通に双方向のメッセージが届くようになります。
NAT によってはこの構成では正しく動かないものをあるみたいですが、UDP hole punching そのものを実装するのは結構簡単な印象でした。どちらかというとNAT 2台とさらに別ノードがある環境を構築するのが大変です。VMWare を使うと NAT の中のノードが簡単に作れるのでこんなテストのときにはお勧めなのかな。