UPnP(NATトラバーサル)を使ったグローバルIP取得

Universal Plug and Playとは、機器を接続するだけで利用可能になるプラグ・アンド・プレイ(PnP)を ネットワークに拡張して、機器を接続しただけでコンピュータネットワークへの参加を可能に概念のプロトコルです。
(なお、技術的に PnPと関係あるものではありません。)
一般にブロードバンドルータなどには、LAN内の特定のホストに外部からアクセスできるようにする方法として 「ポートフォワーディング」の機能を持つものがあります。
この機能は、外部のインターネットから、ルータの特定ポートにアクセスがあった場合に、 それを内部LANの特定ホストに転送(フォワーディング)するようなう機能です。
(ポートフォワーディングは、静的NATや静的IPマスカレードなどと呼ばれることもあります。)
これを利用すれば、家内LANで動作しているサーバーをインターネットに公開して、 家の外からアクセスするような使い方ができるようになります。
ポートフォワーディングの設定を、 UPnP NATトラバーサルを使えば自動的に設定することが可能となります。
UPnPの仕様では 各種機能を持ったデバイスと、そのデバイスに対して制御を行なうコントロールポイントを定義しています。
この定義に基づいて対応のデバイス側では、自身が持つ機能をXML形式で公開しています。
そして これをコントロールする側では、そのをXML形式の情報を参照して、必要な機能を設定をする形態です。
(このような、LANで提供されるサービスの自動検索と利用のための設定には、 Mac OS で使われているBonjourと呼ばれる技術もあります。)

以下で、UPnP NATトラバーサルを使ってLANのWebサーバーをインターネットから利用できるように、ブロードバンドルータを ポートフォワーディングの設定を行うJavaのプログラム(SetMyHomeGlobalIP2)の例を紹介します。

プログラムの流れ

この例で示すプログラムは次のようになっています。 これらを実現するために次のようなクラスを作成した。(全てstaticで作られる)
SetMyHomeGlobalIP2
debugFlag: boolean各メソッドの処理過程の表示(このページの各出力情報は、これを利用)
serviceType: Stringpost_UPnP_Message内のSoapActionを指定する時に利用
SCPDURL: StringこのURIのHTTP取得で、関連するアクセス方の詳細を得る。(例:WANPPPConnection.xml)
controlURL: StringSOAPのパスを意味します。post_UPnP_Messageで利用
serviceId,eventSubURL: StringgetParameteで記憶するが、利用していない
getGlobaleDeviceLocation(): String 【1】の処理で、UPnp対応のルータを探し、そのURI返す。
getHttpGet(uri): String 【2】,【3】の処理内で、uriをHTTPのGETで取得し,得た文字列を返す。
getParameter(xmlStr , serviceTypeName): boolean xmlStrのXML文字列からserviceTypeNameのサービス情報取得(【2】のサービスアクセス情報取得用で、上記クラス変数に記憶する。)
post_UPnP_Message(String ip_Port,String acctionName,String postMsg) : String各種 SOAP のサービスを実行する時に、共通で使う処理をまとめたメソッド
post_GetExternalIPAddress(String ip_Port) : String【4】ブロードバンドルータのWAN側のグローバルIPアドレスを得る。
post_AddPortMapping(String ip_Port, String newExternalPort,String newInternalClient, String newInternalPort) : boolean【5】のポートフォワーディングの追加処理
post_DeletePortMapping(String ip_Port, String newExternalPort) : boolean【5】のポートフォワーディングの削除処理
post_GetGenericPortMappingEntry(String ip_Port,String strIndex) : String【5】のポートフォワーディングの確認処理
getLocalHost(int number) Stringこのソフトを起動したPCのIPアドレスを取得する。上記の【5】の設定でnewInternalClientの指定で使われる

各種メソッド

getGlobaleDeviceLocationメソッド
        //グローバルIPコネクションを持つUPnp対応のルータを探す
        public static String getGlobaleDeviceLocation() throws Exception{
        String queryResponse=null;
        //グローバルIPコネクションを持つUPnp対応のルータを探す文字列
        String query = "M-SEARCH * HTTP/1.1\n"+
"HOST: 239.255.255.250:1900\n"+
"MX: 3\n"+
"MAN: \"ssdp:discover\"\n"+
"ST: urn:schemas-upnp-org:service:WANIPConnection:1\n\n";

        //UnpnマルチキャストのIPアドレスとポート
                InetAddress inet = InetAddress.getByName("239.255.255.250");
                int portNumber = 1900;//ポート番号
                MulticastSocket multSock = new MulticastSocket();//マルチキャスト送信用
                multSock.setTimeToLive(10);//寿命設定
                byte[] buf = query.getBytes("UTF-8");//バイト列に変換
                DatagramPacket packet;//マルチキャストソケット生成
                multSock.joinGroup(inet); //指定のマルチキャストアドレスのグループに参加★
                packet = new DatagramPacket(buf, buf.length, inet, portNumber);//IPアドレス、ポート番号も指定
                if(debugFlag) System.out.println(new String(buf , "UTF-8"));
                multSock.send(packet);//送信

                byte[] data = new byte[1024];
                packet = new DatagramPacket(data, data.length);
                multSock.setSoTimeout(20000);

                multSock.receive(packet);//受信& wait
                multSock.close();
                int len = packet.getLength();//受信バイト数取得
                queryResponse = new String(data, 0, len,"UTF-8");//受信バイト列のデコード
                if(debugFlag) {
                        System.out.println("-----------------------------");
                        System.out.println(queryResponse);
                }

                //上記で得られたqueryResponse から、「LOCATION: 」以降の1行を取得します。
                // 例えば『Location: http://192.168.0.1:80/DeviceDescription.xml』の行が取得されます。
                int idx = queryResponse.toUpperCase().indexOf("LOCATION: ");
                if(idx == -1){
                        if(debugFlag) System.out.println("「LOCATION: 」の取得に失敗しました。");
                        return null;
                }
                String loacation = queryResponse.substring(idx + "LOCATION: ".length());
                idx = loacation.indexOf("\n");
                loacation = loacation.substring(0, idx).trim();
                return loacation;
        }
getHttpGetメソッド
        //uriをHTTPのGETで取得し,得た文字列を返す。
        public static String getHttpGet(String uri) throws Exception {
                if(debugFlag) System.out.println("============HTTP GET "+uri);
                String str="";
                URL url = new URL(uri);
                HttpURLConnection huc = (HttpURLConnection) url.openConnection();
                huc.setRequestMethod("GET");
                huc.connect();
                InputStreamReader isr = new InputStreamReader(huc.getInputStream(),"UTF-8");
                int c;
                while((c = isr.read()) != -1){
                        str+=(char)c;
                }
                huc.disconnect();
                return str;
        }
getParameter(String services_Str, String serviceTypeName)メソッド
        //引数に<service></service>群がある文字列を指定する。その中に含まれるserviceTypeNameのサービスの各上記情報を取得して設定する。
        public static boolean getParameter(String services_Str, String serviceTypeName){
                if(debugFlag) System.out.println("++++++++++++++++++++ getParameter(   ,"+ serviceTypeName + ")");
                serviceTypeName = serviceTypeName.toUpperCase();
                //serviceTypeNameがある<service></service>を探し、それを基準とする。
                int idx = services_Str.toUpperCase().indexOf(serviceTypeName);
                if(idx == -1)return false;
                //この<service>内で、<controlURL>を取得する。
                idx = services_Str.toUpperCase().lastIndexOf("<SERVICE>", idx);
                services_Str = services_Str.substring(idx);//この<service>の文字列を先頭になる文字列を得る

                //この<service>内で、<controlURL>を取得する。
                idx = services_Str.toUpperCase().indexOf("<SERVICETYPE>");
                serviceType = services_Str.substring("<serviceType>".length()+idx);
                idx = serviceType.toUpperCase().indexOf("</SERVICETYPE>");
                serviceType = serviceType.substring(0,idx);
                if(debugFlag) System.out.println("<serviceType>:"+serviceType);

                //この<service>内で、<serviceId>を取得する。
                idx = services_Str.toUpperCase().indexOf("<SERVICEID>");
                if(idx != -1){
                        serviceId = services_Str.substring( "<serviceId>".length()+idx);
                        idx = serviceId.toUpperCase().indexOf("</SERVICEID>");
                        serviceId = serviceId.substring(0,idx);
                        if(debugFlag) System.out.println("<serviceId>:"+serviceId);
                }

                //この<service>内で、<SCPDURL>を取得する。
                idx = services_Str.toUpperCase().indexOf("<SCPDURL>");
                if(idx != -1){
                        SCPDURL = services_Str.substring("<SCPDURL>".length()+idx);
                        idx = SCPDURL.toUpperCase().indexOf("</SCPDURL>");
                        SCPDURL = SCPDURL.substring(0,idx);
                        if(debugFlag) System.out.println("<SCPDURL>:"+SCPDURL);
                }

                //この<service>内で、<controlURL>を取得する。
                idx = services_Str.toUpperCase().indexOf("<CONTROLURL>");
                if(idx != -1){
                        controlURL = services_Str.substring("<controlURL>".length()+idx);
                        idx = controlURL.toUpperCase().indexOf("</CONTROLURL>");
                        controlURL = controlURL.substring(0,idx);
                        if(debugFlag) System.out.println("<controlURL>:"+controlURL);
                }

                //この<service>内で、<eventSubURL>を取得する。
                idx = services_Str.toUpperCase().indexOf(    "<EVENTSUBURL>");
                if(idx != -1){
                        eventSubURL = services_Str.substring("<eventSubURL>".length()+idx);
                        idx = eventSubURL.toUpperCase().indexOf("</EVENTSUBURL>");
                        eventSubURL = eventSubURL.substring(0,idx);
                        if(debugFlag) System.out.println("<eventSubURL>:"+eventSubURL);
                }
                if(debugFlag) System.out.println("++++++++++++++++++++++++++++++++++++++++++++\n\n");

                return true;
        }
post_UPnP_Messageメソッド
        public static String post_UPnP_Message(String ip_Port,  String acctionName,String postMsg)throws Exception
        {
                byte []postMsgBuf = postMsg.getBytes("UTF-8");
                String str = "";//戻り値
                String post = "POST "+controlURL+"/WANPPPConnection HTTP/1.1\r\n"+
"SoapAction: " + serviceType + "#"+acctionName+"\r\n"+
"Host: "+ip_Port+"\r\n"+
"Content-Type: text/xml\r\n"+
"Content-Length: " + postMsgBuf.length + "\r\n\r\n";

                String uriPath = "http://" + ip_Port + controlURL;
                URL url = new URL(uriPath);
                if(debugFlag) {
                        System.out.println("-----URL:"+uriPath);
                        System.out.println("-----post_UPnP_Message:\n"+post);
                        System.out.println(postMsg);
                }
                String ip = null;
                int port=80;
                int idx = ip_Port.indexOf(":");
                if(idx != -1){
                        ip= ip_Port.substring(0,idx);
                        port = Integer.parseInt(ip_Port.substring(idx+1));
                }
                Socket soket = new Socket(ip, port);
                OutputStream outStream = soket.getOutputStream();
                InputStream is = soket.getInputStream();
                byte []buf = post.getBytes();
                outStream.write(buf);
                outStream.write(postMsgBuf);
                outStream.flush();
                InputStreamReader isr = new InputStreamReader(is);
                int c;
                while((c = isr.read()) != -1){
                        str+=(char)c;
                }
                soket.close();
                if(debugFlag)System.out.println("------------------------ that over.\n");
                return str;
        }
post_GetExternalIPAddressメソッド
        public static String post_GetExternalIPAddress(String ip_Port) throws Exception{
                String postMsg = "<?xml version=\"1.0\"?>\r\n"+
"<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n"+
"<SOAP-ENV:Body>\r\n"+
 "<m:GetExternalIPAddress xmlns:m=\"" + serviceType +"\">\r\n"+
"</m:GetExternalIPAddress>\r\n"+
"</SOAP-ENV:Body>\r\n"+
"</SOAP-ENV:Envelope>\r\n";

                String resposeMsg = post_UPnP_Message(ip_Port,  "GetExternalIPAddress",  postMsg);
                if(debugFlag) System.out.println("-----GetExternalIPAddress:"+resposeMsg);
                int idx = resposeMsg.toUpperCase().indexOf("<NEWEXTERNALIPADDRESS>");
                resposeMsg = resposeMsg.substring(idx + "<NEWEXTERNALIPADDRESS>".length());
                idx = resposeMsg.indexOf("<");
                resposeMsg = resposeMsg.substring(0, idx);
                return resposeMsg;
        }
post_AddPortMappingメソッド
        public static  boolean post_AddPortMapping(String ip_Port, String newExternalPort,
                                                        String  newInternalClient, String newInternalPort) throws Exception{
                String postMsg = "<?xml version=\"1.0\"?>\r\n"+
"<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n"+
"<SOAP-ENV:Body>\r\n"+
 "<m:AddPortMapping xmlns:m=\"" + serviceType +"\">\r\n"+
        "<NewRemoteHost></NewRemoteHost>\r\n"+
        "<NewExternalPort>"+newExternalPort+"</NewExternalPort>\r\n"+
        "<NewProtocol>TCP</NewProtocol>\r\n"+
        "<NewInternalPort>"+newInternalPort+"</NewInternalPort>\r\n"+
        "<NewInternalClient>"+newInternalClient+"</NewInternalClient>\r\n"+
        "<NewEnabled>1</NewEnabled>\r\n"+
        "<NewPortMappingDescription>UPnP "+newExternalPort+" port Mapping</NewPortMappingDescription>\r\n"+
        "<NewLeaseDuration>0</NewLeaseDuration>\r\n"+
"</m:AddPortMapping>\r\n"+
"</SOAP-ENV:Body>\r\n"+
"</SOAP-ENV:Envelope>\r\n";

                String resposeMsg = post_UPnP_Message(ip_Port,  "AddPortMapping",  postMsg);
                if(debugFlag) System.out.println("-----AddPortMapping:"+resposeMsg);
                int idx = resposeMsg.toUpperCase().indexOf("\n");
                if(idx != -1) resposeMsg = resposeMsg.substring(0,idx).toUpperCase();
                idx = resposeMsg.toUpperCase().indexOf("200");//成功の番号がレスポンス先頭行にあるか
                return idx != -1;
        }
post_DeletePortMappingメソッド
        public static boolean post_DeletePortMapping(String ip_Port, String newExternalPort) throws Exception{
                String postMsg = "<?xml version=\"1.0\"?>\r\n"+
"<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n"+
"<SOAP-ENV:Body>\r\n"+
 "<m:DeletePortMapping xmlns:m=\"" + serviceType +"\">\r\n"+
        "<NewRemoteHost></NewRemoteHost>\r\n"+
        "<NewExternalPort>"+newExternalPort+"</NewExternalPort>\r\n"+
        "<NewProtocol>TCP</NewProtocol>\r\n"+
"</m:DeletePortMapping>\r\n"+
"</SOAP-ENV:Body>\r\n"+
"</SOAP-ENV:Envelope>\r\n";

                String resposeMsg = post_UPnP_Message(ip_Port,  "DeletePortMapping",  postMsg);
                if(debugFlag) System.out.println("-----DeletePortMapping:"+resposeMsg);
                int idx = resposeMsg.toUpperCase().indexOf("\n");
                if(idx != -1) resposeMsg = resposeMsg.substring(0,idx).toUpperCase();
                idx = resposeMsg.toUpperCase().indexOf("200");//成功の番号がレスポンス先頭行にあるか
                return idx != -1;
        }
post_GetGenericPortMappingEntryメソッド
        public static String post_GetGenericPortMappingEntry(String ip_Port,String strIndex) throws Exception{
                String postMsg = "<?xml version=\"1.0\"?>\r\n"+
"<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n"+
"<SOAP-ENV:Body>\r\n"+
 "<m:GetGenericPortMappingEntry xmlns:m=\"" + serviceType +"\">\r\n"+
        "<NewPortMappingIndex>"+strIndex+"</NewPortMappingIndex>\r\n"+
"</m:GetGenericPortMappingEntry>\r\n"+
"</SOAP-ENV:Body>\r\n"+
"</SOAP-ENV:Envelope>\r\n";

                String resposeMsg = post_UPnP_Message(ip_Port,  "GetGenericPortMappingEntry",  postMsg);
                if(debugFlag) System.out.println("-----GetGenericPortMappingEntry:"+resposeMsg);
                return resposeMsg;
        }
getLocalHostメソッド
        public static String getLocalHost(int number) throws Exception {
                String host = null;
                Enumeration <NetworkInterface> netSet = NetworkInterface.getNetworkInterfaces();
                LOOP: while(netSet.hasMoreElements()){
                        NetworkInterface netinterface = (NetworkInterface) netSet.nextElement();
                        Enumeration <InetAddress> inetSet = netinterface.getInetAddresses();
                        InetAddress inet  = InetAddress.getByName("localhost");
                        while(inetSet.hasMoreElements()){
                                inet = (InetAddress) inetSet.nextElement();
                                host = inet.getHostAddress();
                                if(debugFlag){
                                        System.out.println("--------"+inet.getClass().getName());
                                        System.out.println("IPアドレス:" + host);
                                        System.out.println("IPアドレス:" + inet);
                                } else if(inet.getClass().getName().equals("java.net.Inet4Address")){
                                         if(--number <= 0) break LOOP;
                                }
                        }
                }
                return host;
        }

以上の作成したSetMyHomeGlobalIP2.javaのソースファイル内容は、ここから参照できます。