2014年1月18日土曜日

P2P探訪 STUNでNat越え その4



まずは、スーパーノード候補として最有力なNATであるか調査してみましょう。
外部から見えている、「アドレスとポート」が常に一定であり。
他からのアクセスを制限していないNATのことです。(フルコーン)


○STUNでの確認方法

 どのようにして、制限があるかを確認するのでしょうか? STUNの仕組みはとても単純です。実際に通信してみて各種条件で通信ができるか試します。実際に試してみて、もしも通信できたならば、同一の条件下の端末とは通信可能といえるでしょう。


具体的には、実際に外部からメッセージを送信してもらう。そして、送信しもらったメッセージを受け取る事ができるかを確認します。

  • 接続したUDPサーバーから、メッセージを受け取れる事
  • 接続したUDPサーバーの異なるポートから、メッセージを受け取れること
  • 接続したUDPサーバーと異なるサーバーから、メッセージを受け取る事ができる事



○Bindingリクエスト

 実際にSTUNの通信内容を見ていきましょう。サーバーにレスポンスする条件(ポートとアドレス)を指定してレスポンスを返しもらいます。このレスポンスの依頼をSTUNでは、Bindingリクエストと読んでいます。

 例えば以下のような、依頼を出す事でしょう。
  1. CL はサーバーへ「受け取ったサーバーから、レスポンスを返してもらう。」依頼をだす。
  2. CL はサーバーへ「受け取ったサーバーから、ポートだけ変えてレスポンスを返してもらう。」依頼をだす。
  3. CL はサーバーへ「受け取ったサーバーから、ポートとアドレスを変えてレスポンスを返してもらう。」依頼をだす。


 3のレスポンスを受け取る事ができたならば、「フルコーン」といえます。



○ 実際に送受信するメッセージ

STUNではBindingリクエストと呼ばれるリクエストほをサーバーへサーバーへ送信します。


1. 送信先のアドレスとポートから返信するように依頼をだす。
{
0x00, 0x00,   // 最初の2byteは0
0x00, 0x01,   // Binding リクエストを意味する 0x01 
0x00, 0x08,   // Attrinuteのサイズ 8バイトを表す 0x08
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, // 16バイトのid
0x00, 0x03, // ChangeRequest 
0x00, 0x04, // attibuteのボティのサイズ 4
0x00, 0x00, 0x00, 0x00// 送信先のアドレスとポートから返信する
};

2. IPとポートを変えて返信

{
0x00, 0x00,   // 最初の2byteは0
0x00, 0x01,   // Binding リクエストを意味する 0x01 
0x00, 0x08,   // Attrinuteのサイズ 8バイトを表す 0x08
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, // 16バイトのid
0x00, 0x03, // ChangeRequest 
0x00, 0x04, // attibuteのボティのサイズ 4
0x00, 0x00, 0x00, 0x06// 送信先のアドレスとポートから返信する
};



以下のような返答をレスポンスを受け取る事ができます。

{
0x00, 0x00, // 最初の2バイトがは0
0x01, 0x01, // Bindingレスポンスを意味する。0x101 
0x00, 36, // length
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, //id
// 
0x00, 0x04// レスポンスをしてくれたサーバーのアドレスを意味する(SOURCE_ADDRESS)
0x00, 0x08, //length
0x00, 0x01, //family
0x03, (byte)(0xFF&0x20), //レスポンスしてくれたポート
127, 0, 0, 1,//レスポンスしてくれたアドレス
//
0x00, 0x01, // サーバーから見えたクライアントのアドレス(MAPPED_ADDRESS),
0x00, 0x08, //length
0x00, 0x01, //family
0x03, (byte)(0xFF&0x20), //サーバーから見えたクライアントのポート
127, 0, 0, 1,//サーバーから見えたクライアントのアドレス
//
0x00, 0x05, // STUNサーバーが持つ、異なるポートと異なるアドレス(CHANGE_ADDRESS)
0x00, 0x08, //length
0x00, 0x01, //family
0x03, (byte)(0xFF&0x20), //port
127, 0, 0, 1,


};

うけとったレスポンスを元に、自身がフルコーンなのか判定できます。判定するのは、STUNサーバーではなく。STUNクライアントであるところに注意してください。
今回の場合、このレスポンスを受け取る事ができたならば、フルコーンであると言えます。

参照検証ように作成したコード

https://github.com/kyorohiro/Hetimatan/blob/master/Hetimatan/src_nat/net/hetimatan/net/stun/HtunServer.java


まとめ/次回予告


という事で、STUNの正体は見えてきたでしょうか。
基本的な考え方は、「様々な条件で通信ができるか試してみ。どの条件で通信できたかで分類する」と行ったものである事が理解できたでしょう。

次回は、UDPパンチについて解説します。通信に細工をする事でNATをだまして、通信できるようにするテクニックです。






2014年1月13日月曜日

P2P探訪 STUNでNat越え その3

 STUNサーバーをつくりながら、NATの構成を推測する方法を解説していきます。
※ 制限のあるNAT配下で、通信をできるようにする方法については、後回しにします。
気になる方は「udp hole punching」とかで検索してください。

○もっとも厄介な制限

もっとも、厄介な制限はなんでしょうか? それは、UDPの使用に制限がかかっている場合です。
まずは、UDPの使用をできる事を確認してみましょう。


○サーバーに問い合わせて確認する

 UDPの使用に制限があるかはアプリからは判断できません。なぜならば、 制限を加えているのは、主にルータだからです。なので、実際に外部のUDPサーバーと通信してみるより方法がありません。
  外部のUDPサーバーに アクセスしてみて返答があれば、UDPが使える。返答がなければUDPが使えない。として判定できます。



○ 作った見よう

 本書では、NAT越えをじょじょ広げていき、Stunにサーバーもどきを作っていきます。ただ、UDPが使用可能かのチェックをするのに必要な最小のこ構成は、「外部に返答を返すUDPサーバーを用意する」だけです。
 早速用意してみました。

 やった事
  • Serversman で、vpsを借りる。
    stunを実現するには、ipアドレスが2つ必要です。Standardプラン以降のものを準備する必要があるでしょう。もちろん、P2Pアプリとして実現するのであれば、Entryプランを2つ取得しても良いでしょう。http://kyorohiro.blogspot.jp/2013/07/blog-post.html

  • 確認用に作成したコード
サーバーから見えているクライアントのアドレスとポートを返すだけのアプリです。    https://github.com/kyorohiro/Hetimatan/blob/master/Hetimatan/src_nat/net/hetimatan/net/stun/HtunServer.java


○ 次回

  Stunもどきの判定能力をじょじょにあげていたいと思います。次回はフルコーンNATかを判定してみる予定です。




2014年1月8日水曜日

P2P探訪 STUNでNAT越え その2

中継サーバーを間におく事で、お互いのアドレスとポート番号を特定する事ができまはた。UDPをSocketを使用して、通信してみましょう。しかし、残念ながら、多くの皆さんは通信に失敗する事でしょう。

<後述するような中継サーバー(P2PTracker)を試してみましょう>

ルータの制限

なぜだ? UDPならばできそうなものだが?

「アドレスとポートから送り先をたどれない」ならば、そもそもUDPでの通信ができないではないか?
 そもそも、UDPはTCPと違いコネクションを持ちません。UDPは通信相手もUDPパケットに含まれるアドレスとポート番号を頼りにして相手と通信をします。
 ですから、「UDPパケットを受け取ったサーバー」と「そのサーバーからパケット情報をもらったクライアント」では、差が無いように思えます。


残念ながら、ルータによって制限がかけられています。

残念ながらルータには制限がかけられている場合があります。UDPの通信の仕組みはどうあれ、「UDPで送ったパケットを返信するのは、送った先の端末から」なのです。
ルータ制作者の立場にたってみれば、それ以外を想定する必要はないでしょう。

 
例えば、通信した「相手のアドレス」意外は不正な通信の可能性が高いとみなして、パケットを破棄する。(制限付きNAT)
例えば、「通信した相手のポート番号」意外は不正な通信の可能性が高いと見なして、パケットを破棄する。
※ 制限している訳では無くて、効率の問題でそうなっているだけかも知れません。

そもそも、UDPの本来の目的を満たすだけならば、送信相手によって、アドレスとポートを変えても良いでしょう。(シンメトリックNAT)

まとめ/次回予告

UDPに制限がある事を理解して頂けたことでしょう。 次回は、「制限を突破する方法」または、「制限の種類を特定する方法」について解説して行きたいと思います。



サンプルコード

○中継サーバー

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.util.LinkedList;


public class P2PTracker {

    public static void main(String[] args) {
        P2PTracker tracker;
        try {
            tracker = new P2PTracker();
            tracker.run();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private LinkedList mPeerList = new LinkedList<>();
    private DatagramSocket mSocket= null;

    public P2PTracker() throws IOException {
        mSocket = new DatagramSocket(8080);
    }

    public void run() {
        try {
            do {
                DatagramPacket packet = receive();
                requestPacket(packet.getAddress().getHostAddress(), packet.getPort());
            } while(true);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public DatagramPacket receive() throws IOException {
            byte[] buf = new byte[1024];
            DatagramPacket packet= new DatagramPacket(buf,buf.length);
            mSocket.receive(packet);
            PeerInfo info = new PeerInfo();
            info.mHost = packet.getAddress().getHostName();
            info.mPort = packet.getPort();
            if(!mPeerList.contains(info)) {
                mPeerList.addFirst(info);
                if(mPeerList.size() > 50) {
                    mPeerList.removeLast();
                }
            }
            return packet;
    }

    public void requestPacket(String host, int port) throws UnknownHostException, IOException {
        byte[] buf = getPeerInfoList().toString().getBytes();
        DatagramPacket packet= new DatagramPacket(
                buf, buf.length,
                Inet4Address.getByName(host), port);
        mSocket.send(packet);
    }

    public String getPeerInfoList() {
        StringBuilder buider = new StringBuilder();
        for(PeerInfo info : mPeerList) {
            buider.append(""+info.mHost+":"+info.mPort+",");
        }

        System.out.println("#s#"+buider.toString());
        return buider.toString();
    }

    public static class PeerInfo {
        public String mHost = "";
        public int mPort = 0;

        @Override
        public boolean equals(Object obj) {
            if(!(obj instanceof PeerInfo)) {
                return false;
            }
            PeerInfo target = (PeerInfo)obj;
            if(target.mHost.equals(mHost) && target.mPort == mPort) {
                return true;
            } else {
                return false;
            }
        }
    }
}



○クライアントの中継サーバと通信する部分

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;


public class P2PClient {

    public static void main(String[] args) {
        P2PClient client = new P2PClient("xx.xx.xx.xx");
        client.run(8081);
    }

    private String mHost = "127.0.0.1";
        public P2PClient(String host) {
        mHost = host;
    }

    public void run(int port) {
        try {
            clientAction(port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void clientAction(int port) throws IOException {
        DatagramSocket socket = new DatagramSocket(port);
        byte[] buf = new byte[1024];
        DatagramPacket sendPacket= new DatagramPacket(
          buf,buf.length,
          Inet4Address.getByName(mHost), 
          8080);
        socket.send(sendPacket);

        DatagramPacket receivePacket = new DatagramPacket(buf, buf.length);
        socket.receive(receivePacket);
        System.out.println("#cl#"+new String(receivePacket.getData()));
        socket.close();
    }
}


....
...
..


2014年1月7日火曜日

P2P探訪 STUNでNAT越え その1


UPnPを用いて、NAT越えできました。しかし、ルータがUPnPをサポートしていなかったり。UPnPだけでは越えられないNATがあります。

本文では、その代案として前回解説できなかった。「適当なサーバーに接続してみて、相手から見えているアドレスを返してもらう方法」について解説していきます。

TCPの限界

インターネットで公開されている情報のほとんどは、TCPという通信方法でデータをやり取りされています。ですから、インターネットで情報を公開したい場合は、TCPサーバーを立ち上げる事を考える事でしょう。
 しかし、ルータがUPnPをサポートしていない場合、TCPを用いたサーバーを運用する事は困難になります。※ 基本、無理と考えもらって問題ありません。


接続相手から教えてもらう方法はどうした?

適当なサーバーに接続してみて、相手から見えているアドレスを返してもらう事で実現できないのでしょうか。前回はできそうな事を臭わせていました。しかし、TCPにおいて、これは困難です。

実際にTCPのプログラムを書き確認して見ましょう。接続相手のホストアドレスは推測できます。しかし、ポート番号を知るすべはありません。


import java.io.IOException;
import java.net.Inet4Address;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;


public class TCPTest {

 public static void main(String[] args) {
  TCPTest test = new TCPTest();
  test.startServer();
  try {
   Thread.sleep(3000);
  } catch (InterruptedException e) {
   e.printStackTrace();
  }
  test.startClient();
 }

 private Server mServer = new Server();
 public void startServer() {
  mServer.start();
 }

 public void startClient() {
  try {
   clientAction();
  } catch (UnknownHostException e) {
   e.printStackTrace();
  } catch (IOException e) {
   e.printStackTrace();
  }
 }

 public class Server extends Thread {
  @Override
  public void run() {
   try {
    serverAction();
   } catch (IOException e) {
    e.printStackTrace();
   }
  }
 }
 public void serverAction() throws IOException {
  int port = 8080;
  ServerSocket server = new ServerSocket(port);
  Socket connection = server.accept();
  System.out.println("hostAddress="+connection.getInetAddress().getHostAddress());
  System.out.println("port="+connection.getPort());
 }

 public void clientAction() throws UnknownHostException, IOException {
  int port = 8080;
  Socket client = new Socket(Inet4Address.getByName("127.0.0.1"), port);
  client.getOutputStream().write("binding".getBytes());
 }
}


以下のような値が表示されます。
hostAddress=127.0.0.1

port=52106


サーバーがaccept()時に生成するSocketのさす「アドレスとポート」は接続先のサーバーのアドレスではありません。
なので、ポート番号は0-65535と限りがありますが、どのポートでサーバーが待ち受けているか調べるには、最悪65536回試す必要があります。



UDPを使おう

そこで、NATを越えて通信する方法として、UDPを使用する事でこの問題を解決しましょう。
UDPは、TCPと異なり、相手に接続するポート番号と、待ち受けるポート番号同じになります。このため、「相手から見えているアドレス」だけでなく、ポート番号も知る事ができるのです。



import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;


public class UDPTest {

 public static void main(String[] args) {
  UDPTest test = new UDPTest();
  test.startServer();
  try {
   Thread.sleep(3000);
  } catch (InterruptedException e) {
   e.printStackTrace();
  }
  test.startClient();
 }

 public void startClient() {
  try {
   clientAction();
  } catch (IOException e) {
   e.printStackTrace();
  }
 }

 public void startServer() {
  Thread th = new Thread() {
   public void run() {
    try {
     serverAction();
    } catch (IOException e) {
     e.printStackTrace();
    }
   }
  };
  th.start();
 }
 public void clientAction() throws IOException {
  DatagramSocket socket = new DatagramSocket(8081);
  byte[] buf = new byte[1024];
  DatagramPacket packet= new DatagramPacket(
    buf,buf.length,
    Inet4Address.getByName("127.0.0.1"),
    8080
    );
  socket.send(packet);
 }

 public void serverAction() throws IOException {
  DatagramSocket socket = new DatagramSocket(8080);
  byte[] buf = new byte[1024];
  DatagramPacket packet= new DatagramPacket(buf,buf.length);
  socket.receive(packet);
  System.out.println("address="+packet.getAddress());
  System.out.println("port="+packet.getPort());
 }

}


以下のような値が表示されます。
address=/127.0.0.1

port=8081


サーバーへ接続したクライアントが待ち受けしている。アドレスとポート番号を取得できました。

まとめ

このような感じで、UDPを使う事で、アプリが外部に公開したいる「アドレス」と「ポート番号」を取得できました。UDP使えば、ご家庭のアプリをサーバーとして公開する事も可能でしょう。


次回予告
しかし、取得した「アドレス」と「ポート」には、制限があった。
素直につかえないぞ!!


-------
kyorohiro work

kyorohiro.strikingly.com





2014年1月3日金曜日

P2P探訪 UPnPでNAT越えする

 P2Pアプリは、サーバーとクライアントの両方機能をもったアプリです。基本的には、各言語のServer用のSocketでプログラムを書くことでこのサーバー部分の機能を実装できます。しかし、ご家庭の端末はそれだけでは実現できない事があります。


○  NATの弊害


 「端末から見えている自分のIP」と「通信相手から見えている自分のIP」がことなる場合があるからです。※ 異なるのが普通と事と考えてもよいでしょう。
 サーバーとしての機能を活用するためには、相手に自分のIPを伝える必要があります。そもそも、相手が自分のIPを知らないと、接続してもらえません。



○ 相手から見えているIPを知る方法

そこで、相手から見えているIPを調べて、相手に通知してあげましょう。そうすれば、サーバーとして機能を果たす事ができます。

相手から見えているIPを知る方法はいくつかあります。
* a. ルータに確認する
* b.適当なサーバーに接続してみて、相手から見えているアドレスを返してもらうぬ
などです。


ここでは、「a.ルータに確認する方法」について紹介します。


○ UPnPを使おう

ルーターとは、UPnPプロトコルを通して会話する事ができます。

1 "239.255.255.250" 1900に参加する


例えば以下のような感じ
<pre>
MulticastSocket ssdpSocket = null;
InetSocketAddress ssdpGroup = new InetSocketAddress("239.255.255.250", 1900);
InetAddress nicAddress = InetAddress.getByName(hostName);
ssdpSocket = new MulticastSocket(new InetSocketAddress(nicAddress, SSDP_PORT));
ssdpSocket.joinGroup(ssdpGroupNetworkInterface.getByInetAddress(nicAddress));

</pre>

2. ルータを探す

UPnPは同一ネットワーク(ルータ内)のコンピュータへ、「M-SEARCH」をブルードキャスト送信する事で実現できます。

例えば以下のような感じ
<pre>
String message =
"M-SEARCH * HTTP/1.1"+"\r\b"+
"HOST:239.255.255.250:1900"+"\r\b"+
"MAN:\"ssdp:discover\""+"\r\b"+
"ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1"+"\r\b"+
"MX:3"+"\r\b"+
"+"\r\b";


ssdpSocket.send(new DatagramPacket(
message.getBytes(), message.getBytes().length, ssdpGroup);

</pre>

こんなのが返る
<pre>
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=120
Location: http://192.168.0.1:2869/upnp/rootdevice.xml
SERVER: IGD-HTTP/1.1 UPnP/1.0 UPnP-Device-Host/1.0
ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1
EXT:
USN: uuid:79f0447f-860fbb81::urn:schemas-upnp-org:device:InternetGatewayDevice:1

</pre>

3. NAT関連のサービスを探す

http://192.168.0.1:2869/upnp/rootdevice.xmlをGetします。このXMLファイルには、そのデバイスが提供しているサービスが書かれています。このファイルをパースしてNAT関連のサービスを探しましょう。

例えば、
GET /upnp/rootdevice.xml HTTP/1.1

Host: 192.168.0.1
を送る。
</pre>

こんなのが返る
<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
..
.
<service>
<serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType>
<serviceId>urn:upnp-org:serviceId:L3Frwd1</serviceId>
<controlURL>/upnp/control/L3Frwd1</controlURL>
<eventSubURL>/upnp/event/L3Frwd1</eventSubURL>
<SCPDURL>/upnp/L3Frwd1.xml</SCPDURL>

</service>
...
..
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
<controlURL>/upnp/control/WANIPConn1</controlURL>
<eventSubURL>/upnp/event/WANIPConn1</eventSubURL>
<SCPDURL>/upnp/WANIPConn1.xml</SCPDURL>
</service>
..
.
</root>

serviceTypeが、「WANIPConnection」「WANPPPConnection」のものが、NAT関連のサービスになります。


4. 通信相手から見えている自分のIPを取得する。

見つかったサービスから、IPアドレスを取得する事ができます。


こんな感じで要求を出す事ができます。
<pre>
POST /upnp/control/WANIPConn1 HTTP/1.0
SOAPACTION: "urn:schemas-upnp-org:service:WANPPPConnection:1#AddPortMapping"
Content-Length: 718
Host: 192.168.0.1

<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV:="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <SOAP-ENV:Body>
    <m:AddPortMapping xmlns:m="urn:schemas-upnp-org:service:WANPPPConnection:1">
      <NewRemoteHost></NewRemoteHost>
      <NewExternalPort>8081</NewExternalPort>
      <NewProtocol>TCP</NewProtocol>
      <NewInternalPort>8081</NewInternalPort>
      <NewInternalClient>192.168.0.3</NewInternalClient>
      <NewEnabled>1</NewEnabled>
      <NewPortMappingDescription>test</NewPortMappingDescription>
      <NewLeaseDuration>3600</NewLeaseDuration>
    </m:AddPortMapping>
  </SOAP-ENV:Body>

</SOAP-ENV:Envelope>
</pre>

こんなのが返ります。

HTTP/1.1 200 OK
CONTENT-LENGTH: 423
CONTENT-TYPE: text/xml; charset="utf-8"
SERVER: IGD-HTTP/1.1 UPnP/1.0 UPnP-Device-Host/1.0
EXT:

<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<m:GetExternalIPAddressResponse xmlns:m="urn:schemas-upnp-org:service:WANIPConnection:1">
<NewExternalIPAddress>xxx.xxx.xxx.xxx</NewExternalIPAddress>
</m:GetExternalIPAddressResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>



5. Portをマッピングする

「通信相手から見えているポート番号」と「自分から見えているポート番号」を関連付けます。見つかったサービスへ要求を出す事で実現できます。
例えば以下のような感じ

<pre>
POST /upnp/control/WANIPConn1 HTTP/1.0
SOAPACTION: "urn:schemas-upnp-org:service:WANPPPConnection:1#AddPortMapping"
Content-Length: 718
Host: 192.168.0.1

<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV:="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <SOAP-ENV:Body>
    <m:AddPortMapping xmlns:m="urn:schemas-upnp-org:service:WANPPPConnection:1">
      <NewRemoteHost></NewRemoteHost>
      <NewExternalPort>8081</NewExternalPort>
      <NewProtocol>TCP</NewProtocol>
      <NewInternalPort>8081</NewInternalPort>
      <NewInternalClient>192.168.0.3</NewInternalClient>
      <NewEnabled>1</NewEnabled>
      <NewPortMappingDescription>test</NewPortMappingDescription>
      <NewLeaseDuration>3600</NewLeaseDuration>
    </m:AddPortMapping>
  </SOAP-ENV:Body>

</SOAP-ENV:Envelope>
</pre>

成功すると、200 OKが返ります。

6. Portをマッピングを解除する

必要ななくなったら、ポートの関連づけを解除しましょう。以下のような感じででせきます。

POST /upnp/control/WANIPConn1 HTTP/1.0
SOAPACTION: "urn:schemas-upnp-org:service:WANPPPConnection:1#
DeletePortMapping"
Content-Length: xxx
Host: 192.168.0.1

<?xml version=\"1.0\"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV:=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">
 <SOAP-ENV:Body>
<m:DeletePortMapping xmlns:m=\"urn:schemas-upnp-org:service:WANPPPConnection:1\">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>8081</NewExternalPort>
<NewProtocol>TCP</NewProtocol>
</m:DeletePortMapping>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

成功すると、200 OKが返ります。




○参考



http://upnp.org/sdcps-and-certification/



○検証用に書いたコード

https://github.com/kyorohiro/Hetimatan/tree/master/Hetimatan/src_util/net/hetimatan/net/ssdp


-------
kyorohiro work

kyorohiro.strikingly.com





2013年10月14日月曜日

P2P探訪 Raider 最初のメッセージ

TorrentクローンをJava で作成しています。
そろそろ、Applet上で動作するデモができそうです。 試してみて解ったことが結構ありました。
そこで、学習したしことを、epub形式でまとめています。http://p.booklog.jp/users/kyorohiro


やる気スイッチが入らないので、書きかけの物をさらします。



-----------------

最初のメッセージ

 TorrentクライアントはHandShakeが完了すると、次に 自分の持っているデータの相手に通知します。またデータを配信する場合、相手にデータを配信する意図がある事を伝えます。
 

どのデータを持っているかをBitfieldとして渡す。

 最初に送信するメッセージはBitfieldです。自分がどのようなデータを持っているかを相手に知らせます。 

データを分割して管理している 

  Torrent方式ではデータを分割して管理しています。 分割する単位はTorrentファイルに記載されておりTorrentネットワーク内で分割単位を共有しています。実際に分割したデータをPieceと読んでいます。通常、Pieceは「16KB」「32KB」「64KB」「128KB」「256KB」といった単位で分割されます。

 分割したデータには、1番から昇順に番号が降られます。例えば、160KBのデータを16KBで分割する場合、「0-16KB」までのデータを1番、「16-32KB」までのデータを2番、...「152KB-160KB」までのデータを10番と行った感じで番号が降られます。
 

データをBitfieldで渡す

 データを持っている場合は1。持っていない場合は0が設定します。例えば、160KBのデータを16KBで分割する場合、例えば、1番目のデータを持っている場合、「0x80 0x00」のようなバイトデータになります。 1番目と10番目のデータを持っている場合は、「0x80, 0x40」とったバイトデータになります。
 
以下のようなフォーマットで送信されます。
  • 0-4バイト1+Bitfieldのサイズ
  • 4-5バイト5
  • 5- Bitfieldの生データ(byte単位)
 具体的に、 メッセージとして「0x00, 0x00, 0x00, 0x03, 0x05, 0x80, 0x40」のようなバイトデータを送信します。「0x00, 0x00, 0x00, 0x03」がメッセージのサイズ、「0x05」が識別子、「0x80, 0x40」が持っているPieceの情報です。
 

相手にデータを配信する意思があることを伝える

 データを配信する意思があることを相手に伝えます。Torrent方式ではデータを配信する意図を示した端末のみデータを配信してくれます。
  

データ配信する意図を示す(unchoke)

 具体的には、unchokeメッセージを送信します。このメッセージを受け取ると相手のクライアントからrequestメッセージが呼ばれデータの配信が開始されます。
 unchokeメッゼージは以下のようなフォーマットです。
  • 0-4バイト目1
  • 4-5バイト目1
 
 具体的には、「0x00, 0x00, 0x00, 0x01, 0x01」のようなバイトデータを送信します。
 


P2P探訪 Raider ハンドシェークしてみる

TorrentクローンをJava で作成しています。
そろそろ、Applet上で動作するデモができそうです。 試してみて解ったことが結構ありました。
そこで、学習したしことを、epub形式でまとめています。http://p.booklog.jp/users/kyorohiro


やる気スイッチが入らないので、書きかけの物をさらします。



-----------------

ハンドシェークしてみる

  Trackerから、ピアの情報を所得できるようになりました。 これで、データを配信してるTorrentクライアントと接続できるようになりました。  本章では、他のTorrentクライアントへデータの転送許可を取る機能を実現します。
  

TorrentクライアントはTCPサーバー/クライアント

 TorrentクライアントはP2Pアプリです。Torrentクライアントはサーバーとクライアントの両方の機能を持っています。他のTorrentクライアントから接続を受け入れ、データを配信する」というサーバー的な機能。そして、「他のTorrentクライアントへ接続しデータを転送してもらう」というクライアント的な機能です。
 
 しかし、P2Pといっても、特別な方法で通信はしているわけではありません。汎用的な方法が利用されています。Torrentクライアントでは通信にTCP/IPを使用しています。TCP/IPはHttp通信で使用されています。これは、ブラウザーから「youtube」や「Google」にアクセスする時に使用する通信方法と同じ仕組みです。 

Handshakeする

 実際に、Torrentサーバーへ接続してみましょう。Torrent方式では、最初の接続/通信でデータ転送の許可をとります。この時、転送して欲しいデータについての情報をサーバーへ渡します。サーバーはそのデータが持っていれば、基本的に接続を受け入れます。
  

Socketで接続

まずは、Trackerから取得したアドレスへTCP/IPのソケットを生成して接続します。
 
SocketChannel socketChannel = SocketChannel.open();;
socketChannel.connect(new InetSocketAddress(hostname, port));
 
 
この時に生成したコネクションは、データ転送が完了するまで使用し続けます。Http1.0などを利用している古いサーバーなどでは、データを転送するたびに、コネクションを張ります。しかし、Torrentでは一度生成したコネクションはデータ転送が完了するまで確保し続けます。
 
 

データ転送の許可をとる

 接続を開始する前にお互いの情報を交換します。このメッセージの交換をHashshakeと呼びます。
  • 0-1バイト19
  • 1-20バイト"BitTorrent protocol"
  • 20-28バイト {0 0 0 0 0 0 0 0} のバイト配列
  • 28-48バイトInfoHash
  • 48-68バイトPeerId
 を送信します。するとサーバーから、
  • 0-1バイト19
  • 1-20バイト"BitTorrent protocol"
  • 20-28バイト {0 0 0 0 0 0 0 0} のバイト配列
  • 28-48バイトInfoHash
  • 48-68バイトPeerId
とメッセージが返されます。
 

@"19","BitTorrent protocol"

 先頭は識別子が入ります。19は識別子の文字数です。 その後、識別子である「BitTorrent protocol」が続きます。

@{0 0 0 0 0 0 0 0}

 拡張機能を利用するかのフラグです。 例えばDHTをサポートしている場合は0x01がセットされます。


@InfoHash

 どのデータをダウンロードしたいのかを表すHashです。TorrentクライアントのInfo辞書のSHA1です。


@PeerId

 Trackerへ渡したPeerIdを渡してください。

mbedtls dart の 開発を始めた web wasm ffi io flutter

C言語 で開発した機能を、Dart をターゲットとして、Web でも サーバーでも、そして Flutter  でも使えるようにしたい。 そこで、mbedtls の 暗号化の部分を Dart 向けのPackageを作りながら、 実現方法を試行錯誤する事にした。 dart...