天天看點

NAT穿透解決方案介紹

最近公司要實作在各種網絡環境下面的多屏互動(機頂盒、android phone、iphone及PC端)的需求;由于IP位址資源有限的原因,目前我們使用的各種終端裝置都位于區域網路後面也就是多台裝置共享同一個公網IP;例如:如果位于區域網路裡面的一個終端Agent A要與網際網路上的另一個終端Agent B通信,當A發送的data packet經過區域網路出口處的NAT裝置時,NAT會将data packet裡面的source address字段替換成相應的公網IP和Port,然後再發送data packet到Agent B。Agent B看到的source address就是經過轉換後的IP和Port并不知道Agent A的區域網路位址;當Agent B的響應到達Agent A的NAT裝置後,NAT裝置查找記憶體中儲存的和這個外網位址相對應的内網位址,如果找到後就将這個data packet轉發到這個位址,這樣就實作了通信。

然而由于目前存在着各種不同類型的NAT裝置對NAT有着不同的實作方式(将内外位址映射成外網位址的時候有着不同的行為方式),這就給NAT的穿透帶來了麻煩;目前主要的NAT類型有如下幾種:

1)Full-cone NAT, also known as one-to-one NAT

  • 一旦一個内網位址 (iAddr:iPort) 被映射到一個外部位址 (eAddr:ePort), 來自 iAddr:iPort 的任何資料包将通過 eAddr:ePort 發送.
  • 任何外部主機能夠通過eAddr:ePort這個位址發送資料包到iAddr:iPort.

2)Address-restricted-cone NAT

  • 一旦一個内網位址 (iAddr:iPort) 被映射到一個外部位址 (eAddr:ePort), 來自 iAddr:iPort 的任何資料包将通過 eAddr:ePort 發送.
  • 僅隻有接收到主機(iAddr:iPort)通過eAddr:ePort發送的資料包的外部主機通過該主機的任何端口發送到eAddr:ePort的資料包才能夠被正确的轉發到iAddr:iPort.也就是說主機有關端口無關.

3)Port-restricted cone NAT

類似于address restricted cone NAT, 但是端口号有限制.

  • 僅隻有接收到主機(iAddr:iPort)通過eAddr:ePort發送的資料包的外部主機通過該主機的相同端口發送到eAddr:ePort的資料包才能夠被正确的轉發到iAddr:iPort.

4)Symmetric NAT

  • 來自相同内部ip和port發送到相同目的地ip和port的請求被映射到唯一的外部ip和port位址;如果相同的内部主機采用相同的ip和port位址發送到不同的目的地,那麼重新配置設定映射位址。
  • 隻有先前收到内部主機發送的包的外部主機才能夠發送傳回包到内部主機。

針對前面三種NAT類型(即cone NAT)隻要通信雙方彼此知道對方的内部位址和外部位址的映射關系,然後通過UDP打洞的方式就可以建立互相連接配接的通信;但是第四種也就是Symmetric NAT的話由于每次向不同目的地發送資料包時采用不同的外部位址,也就沒辦法通過直接的方式建立P2P連接配接。

1.各種網絡環境下的P2P通信解決方法:

(1)如果通信雙方在同一個區域網路内,這種情況下可以不借助任何外力直接通過内網位址通信即可;

(2)如果通信雙方都在有獨立的公網位址,這種情況下當然可以不借助任何外力直接通信即可;

(3)如果通信雙方一方擁有獨立的公網位址另一方在NAT後面,那麼可以由位于NAT後面的一方主動發起通信請求;

(4)如果通信雙方都位于NAT後面,且雙方的NAT類型都是cone NAT,那麼可以通過一個STUN伺服器發現自己的NAT類型以及内網和外網傳輸位址映射資訊,然後通過Signaling(信令伺服器,實作了SIP協定的主機)交換彼此的NAT類型及内網和外網傳輸位址映射資訊,然後通過UDP打洞的方式建立通信連接配接;

(5)如果通信雙方有一方的NAT類型是Symmetric NAT,則無法直接建立P2P連接配接,這個時候就需要借助TURN(Traversal Using Relay NAT)即轉發伺服器來實作間接通信;

2.協定及用到的相關技術介紹:

SDP(Session Description Protocol)

當初始化多媒體電視會議、IP電話、視訊流等會話的時候,參與者之間會要求傳送媒介的詳細、傳輸位址和其他會話描述中繼資料等資訊;SDP為這些資訊提供一種和傳輸方式無關的标準的表現形式。也就是說SDP僅僅隻是一種描述會話資訊的格式。它主要被各種不同的傳輸協定作為一種資訊交換的格式使用列如:HTTP、RTSP、SIP、Email等各種協定。

如ICE裡面的SDP内容為:

v=0
o=ice4j.org 0 0 IN IP4 192.168.106.215
s=-
t=0 0
a=ice-options:trickle
a=ice-ufrag:bc01a
a=ice-pwd:1boove7ehnpo1lqho7unefni36
m=audio 3030 RTP/AVP 0
c=IN 192.168.106.215 IP4
a=mid:audio
a=candidate:1 1 udp 2130706431 192.168.106.215 3030 typ host
a=candidate:2 1 udp 1694498815 121.15.130.xxx 64923 typ srflx raddr 192.168.106.215 rport 3030      

STUN(Session Traversal Utilities for NAT)

NAT會話穿透工具;STUN提供了一種方式使一個端點能夠确定NAT配置設定的和本地私有IP位址和端口相對應的公網IP位址和端口以及NAT的類型資訊。它也為端點提供了一種方式保持一個NAT綁定不過期。NAT綁定過期則表示為相同的内網位址重新配置設定外網位址也就是端口号。

TURN(Traversal Using Relay NAT)

TURN是STUN協定的擴充,在實際應用中他也可以充當STUN的角色;如果一個位于NAT後面的裝置想要和另外一個位于NAT後面的裝置建立通信,當采用UDP打洞技術不能改實作的時候就必須要一台中間伺服器扮演資料包轉發的角色,這台TURN伺服器需要擁有公網的IP位址;

SIP(Session Initiation Protocol)

是一種Signaling(信令)通信協定;有許多網際網路應用需要建立有多個參與者的會話和管理參與者之間互相的資料交換,然而如果這些工作讓應用的參與者來實作是比較複雜的如:使用者也許在端點之間移動、通過多個名稱尋址和也許同時使用幾種不同的媒介通信。有許多協定能夠實作各種形式的多媒體會話進行資料傳送例如聲音、視訊或者文本消息。SIP能夠和這些協定一同合作,使一個客服端能夠發現參與這個會話的其他客服端并共享同一會話。為了定位後面加入會話的參與者等功能,SIP能夠為代理伺服器建立基礎設施,客服端可以通過這個代理伺服器實作會話注冊、邀請參與會話等功能。SIP是一個建立、修改和終止會話的靈活的多種用途的工具,不依賴于底層的傳輸協定并且不依賴于被建立的會話類型。

ICE(Interactive Connectivity Establishment)

是實作NAT穿透的一種技術方案;ICE是一種NAT穿透技術,通過offer/answer模型建立基于UDP的媒介流。ICE是offer/answer模型的擴充,通過在offer和answer的SDP裡面包含多種IP位址和端口,然後對本地SDP和遠端SDP裡面的IP位址進行配對,然後通過P2P連通性檢查進行連通性測試工作,如果測試通過即表明該傳輸位址對可以建立連接配接。其中IP位址和端口(也就是位址)有以下幾種:本機位址、通過STUN伺服器反射後擷取的server-reflexive位址(内網位址被NAT映射後的位址)、relayed位址(和TURN轉發伺服器相對應的位址)及Peer reflexive位址等。

3.ICE進行NAT穿透的基本過程:

在通常的ICE部署環境中,我們有兩個客服端想要建立通信連接配接,他們可以直接通過signaling伺服器(如SIP伺服器)執行offer/answer過程來交換SDP消息。

在ICE過程開始的時候,客服端忽略他們各自的網絡拓撲結構,不管是不是在NAT裝置後面或者多個NAT後面,ICE允許客服端發現他們的所在網絡的拓撲結構的資訊,然後找出一個或者更多的可以建立通信連接配接的路徑。

下圖顯示了一個典型的ICE部署環境,客服端L和R都在各自的NAT裝置後面,下面簡單描述下ICE建立通信的過程:

(1)L和R先分别通過STUN和TURN伺服器擷取自己的host address,server-reflexive address、relayed address(和TURN轉發伺服器相對應的位址),其中server-reflexive address和relayed address通過定時重新整理保證位址不過期。這些位址通常叫做candinate位址。

(2)給這些candinate位址配置設定優先級排序并格式化成SDP格式,通過SIP伺服器交換彼此的SDP;

(3)交換完成後根據一定的原則把本地的候選和遠端的候選進行配對,每一對都有自己的優先級并根據優先級進行排序後放入Check清單裡面(兩邊都會有相同的Check清單)。

(4)然後進行連接配接性測試,測試前會選擇一個客服端扮演Controlled角色和另一個扮演Controling角色,連通性檢查完成後扮演Controling角色的客服端負責在有效的Candinate對清單裡面選擇一個作為一個被選中的傳輸通道并通知Controlled的客服端。

(5)利用被選中的candinate位址對進行通信。

NAT穿透解決方案介紹

4.ICE JAVA實作代碼

我這裡的樣例代碼采用ICE4J來實作,ICE4J的API文檔可以參考http://bluejimp.com/jitsi/ice4j/javadoc/,在這個實作裡面沒有利用SIP伺服器進行SDP資訊的交換而是采用手動輸入的方式,在生産環境中可以部署一台socket.io或者其他SIP伺服器

NAT穿透解決方案介紹
NAT穿透解決方案介紹
1 /** 
  2 * Copyright (c) 2014 All Rights Reserved.
  3 * TODO
  4 */
  5 
  6 import java.beans.PropertyChangeEvent;
  7 import java.beans.PropertyChangeListener;
  8 import java.io.BufferedReader;
  9 import java.io.InputStreamReader;
 10 import java.net.DatagramSocket;
 11 import java.net.SocketAddress;
 12 import java.util.List;
 13 
 14 import org.apache.commons.lang3.StringUtils;
 15 import org.apache.log4j.Logger;
 16 import org.ice4j.Transport;
 17 import org.ice4j.TransportAddress;
 18 import org.ice4j.ice.Agent;
 19 import org.ice4j.ice.Component;
 20 import org.ice4j.ice.IceMediaStream;
 21 import org.ice4j.ice.IceProcessingState;
 22 import org.ice4j.ice.LocalCandidate;
 23 import org.ice4j.ice.NominationStrategy;
 24 import org.ice4j.ice.RemoteCandidate;
 25 import org.ice4j.ice.harvest.StunCandidateHarvester;
 26 import org.ice4j.ice.harvest.TurnCandidateHarvester;
 27 import org.ice4j.security.LongTermCredential;
 28 
 29 import test.SdpUtils;
 30 
 31 public class IceClient {
 32 
 33      private int port;
 34 
 35      private String streamName;
 36 
 37      private Agent agent;
 38 
 39      private String localSdp;
 40 
 41      private String remoteSdp;
 42     
 43      private String[] turnServers = new String[] { "stun.jitsi.net:3478" };
 44     
 45      private String[] stunServers = new String[] { "stun.stunprotocol.org:3478" };
 46     
 47      private String username = "guest";
 48     
 49      private String password = "anonymouspower!!";
 50     
 51      private IceProcessingListener listener;
 52 
 53      static Logger log = Logger.getLogger(IceClient.class);
 54 
 55      public IceClient(int port, String streamName) {
 56           this.port = port;
 57           this.streamName = streamName;
 58           this.listener = new IceProcessingListener();
 59      }
 60 
 61      public void init() throws Throwable {
 62 
 63           agent = createAgent(port, streamName);
 64 
 65           agent.setNominationStrategy(NominationStrategy.NOMINATE_HIGHEST_PRIO);
 66          
 67           agent.addStateChangeListener(listener);
 68 
 69           agent.setControlling(false);
 70 
 71           agent.setTa(10000);
 72 
 73           localSdp = SdpUtils.createSDPDescription(agent);
 74 
 75           log.info("=================== feed the following"
 76                     + " to the remote agent ===================");
 77 
 78           System.out.println(localSdp);
 79 
 80           log.info("======================================"
 81                     + "========================================\n");
 82      }
 83     
 84      public DatagramSocket getDatagramSocket() throws Throwable {
 85 
 86           LocalCandidate localCandidate = agent
 87                     .getSelectedLocalCandidate(streamName);
 88 
 89           IceMediaStream stream = agent.getStream(streamName);
 90           List<Component> components = stream.getComponents();
 91           for (Component c : components) {
 92                log.info(c);
 93           }
 94           log.info(localCandidate.toString());
 95           LocalCandidate candidate = (LocalCandidate) localCandidate;
 96           return candidate.getDatagramSocket();
 97 
 98      }
 99 
100      public SocketAddress getRemotePeerSocketAddress() {
101           RemoteCandidate remoteCandidate = agent
102                     .getSelectedRemoteCandidate(streamName);
103           log.info("Remote candinate transport address:"
104                     + remoteCandidate.getTransportAddress());
105           log.info("Remote candinate host address:"
106                     + remoteCandidate.getHostAddress());
107           log.info("Remote candinate mapped address:"
108                     + remoteCandidate.getMappedAddress());
109           log.info("Remote candinate relayed address:"
110                     + remoteCandidate.getRelayedAddress());
111           log.info("Remote candinate reflexive address:"
112                     + remoteCandidate.getReflexiveAddress());
113           return remoteCandidate.getTransportAddress();
114      }
115 
116      /**
117      * Reads an SDP description from the standard input.In production
118      * environment that we can exchange SDP with peer through signaling
119      * server(SIP server)
120      */
121      public void exchangeSdpWithPeer() throws Throwable {
122           log.info("Paste remote SDP here. Enter an empty line to proceed:");
123           BufferedReader reader = new BufferedReader(new InputStreamReader(
124                     System.in));
125 
126           StringBuilder buff = new StringBuilder();
127           String line = new String();
128 
129           while ((line = reader.readLine()) != null) {
130                line = line.trim();
131                if (line.length() == 0) {
132                     break;
133                }
134                buff.append(line);
135                buff.append("\r\n");
136           }
137 
138           remoteSdp = buff.toString();
139 
140           SdpUtils.parseSDP(agent, remoteSdp);
141      }
142 
143      public void startConnect() throws InterruptedException {
144 
145           if (StringUtils.isBlank(remoteSdp)) {
146                throw new NullPointerException(
147                          "Please exchange sdp information with peer before start connect! ");
148           }
149 
150           agent.startConnectivityEstablishment();
151 
152           // agent.runInStunKeepAliveThread();
153 
154           synchronized (listener) {
155                listener.wait();
156           }
157 
158      }
159 
160      private Agent createAgent(int rtpPort, String streamName) throws Throwable {
161           return createAgent(rtpPort, streamName, false);
162      }
163 
164      private Agent createAgent(int rtpPort, String streamName,
165                boolean isTrickling) throws Throwable {
166          
167           long startTime = System.currentTimeMillis();
168          
169           Agent agent = new Agent();
170          
171           agent.setTrickling(isTrickling);
172 
173           // STUN
174           for (String server : stunServers){
175                String[] pair = server.split(":");
176                agent.addCandidateHarvester(new StunCandidateHarvester(
177                          new TransportAddress(pair[0], Integer.parseInt(pair[1]),
178                                    Transport.UDP)));
179           }
180 
181           // TURN
182           LongTermCredential longTermCredential = new LongTermCredential(username,
183                     password);
184 
185           for (String server : turnServers){
186                String[] pair = server.split(":");
187                agent.addCandidateHarvester(new TurnCandidateHarvester(
188                          new TransportAddress(pair[0], Integer.parseInt(pair[1]), Transport.UDP),
189                          longTermCredential));
190           }
191           // STREAMS
192           createStream(rtpPort, streamName, agent);
193 
194           long endTime = System.currentTimeMillis();
195           long total = endTime - startTime;
196 
197           log.info("Total harvesting time: " + total + "ms.");
198 
199           return agent;
200      }
201 
202      private IceMediaStream createStream(int rtpPort, String streamName,
203                Agent agent) throws Throwable {
204           long startTime = System.currentTimeMillis();
205           IceMediaStream stream = agent.createMediaStream(streamName);
206           // rtp
207           Component component = agent.createComponent(stream, Transport.UDP,
208                     rtpPort, rtpPort, rtpPort + 100);
209 
210           long endTime = System.currentTimeMillis();
211           log.info("Component Name:" + component.getName());
212           log.info("RTP Component created in " + (endTime - startTime) + " ms");
213 
214           return stream;
215      }
216 
217      /**
218      * Receive notify event when ice processing state has changed.
219      */
220      public static final class IceProcessingListener implements
221                PropertyChangeListener {
222 
223           private long startTime = System.currentTimeMillis();
224 
225           public void propertyChange(PropertyChangeEvent event) {
226 
227                Object state = event.getNewValue();
228 
229                log.info("Agent entered the " + state + " state.");
230                if (state == IceProcessingState.COMPLETED) {
231                     long processingEndTime = System.currentTimeMillis();
232                     log.info("Total ICE processing time: "
233                               + (processingEndTime - startTime) + "ms");
234                     Agent agent = (Agent) event.getSource();
235                     List<IceMediaStream> streams = agent.getStreams();
236 
237                     for (IceMediaStream stream : streams) {
238                          log.info("Stream name: " + stream.getName());
239                          List<Component> components = stream.getComponents();
240                          for (Component c : components) {
241                               log.info("------------------------------------------");
242                               log.info("Component of stream:" + c.getName()
243                                         + ",selected of pair:" + c.getSelectedPair());
244                               log.info("------------------------------------------");
245                          }
246                     }
247 
248                     log.info("Printing the completed check lists:");
249                     for (IceMediaStream stream : streams) {
250 
251                          log.info("Check list for  stream: " + stream.getName());
252 
253                          log.info("nominated check list:" + stream.getCheckList());
254                     }
255                     synchronized (this) {
256                          this.notifyAll();
257                     }
258                } else if (state == IceProcessingState.TERMINATED) {
259                     log.info("ice processing TERMINATED");
260                } else if (state == IceProcessingState.FAILED) {
261                     log.info("ice processing FAILED");
262                     ((Agent) event.getSource()).free();
263                }
264           }
265      }
266 }
267  
268 import java.io.IOException;
269 import java.net.DatagramPacket;
270 import java.net.DatagramSocket;
271 import java.net.SocketAddress;
272 import java.util.concurrent.TimeUnit;
273 
274 
275 public class PeerA {
276 
277      public static void main(String[] args) throws Throwable {
278           try {
279                IceClient client = new IceClient(2020, "audio");
280                client.init();
281                client.exchangeSdpWithPeer();
282                client.startConnect();
283                final DatagramSocket socket = client.getDatagramSocket();
284                final SocketAddress remoteAddress = client
285                          .getRemotePeerSocketAddress();
286                System.out.println(socket.toString());
287                new Thread(new Runnable() {
288 
289                     public void run() {
290                          while (true) {
291                               try {
292                                    byte[] buf = new byte[1024];
293                                    DatagramPacket packet = new DatagramPacket(buf,
294                                              buf.length);
295                                    socket.receive(packet);
296                                    System.out.println("receive:"
297                                              + new String(packet.getData(), 0, packet
298                                                        .getLength()));
299                               } catch (IOException e) {
300                                    // TODO Auto-generated catch block
301                                    e.printStackTrace();
302                               }
303 
304                          }
305                     }
306                }).start();
307 
308                new Thread(new Runnable() {
309 
310                     public void run() {
311                          int count = 1;
312                          while (true) {
313                               try {
314                                    byte[] buf = ("send msg " + count++ + "").getBytes();
315                                    DatagramPacket packet = new DatagramPacket(buf,
316                                              buf.length);
317 
318                                    packet.setSocketAddress(remoteAddress);
319                                    socket.send(packet);
320                                    System.out.println("send msg");
321                                    TimeUnit.SECONDS.sleep(10);
322                               } catch (Exception e) {
323                                    // TODO Auto-generated catch block
324                                    e.printStackTrace();
325                               }
326 
327                          }
328                     }
329                }).start();
330           } catch (Exception e) {
331                // TODO Auto-generated catch block
332                e.printStackTrace();
333           }
334 
335      }
336 
337 }      

View Code

5.參考資料

  ICE:https://tools.ietf.org/html/rfc5245

  SDP:http://tools.ietf.org/html/rfc4566

  SIP:http://tools.ietf.org/html/rfc3261

  NAT:http://en.wikipedia.org/wiki/Network_address_translation

 STUN:http://tools.ietf.org/html/rfc5389

 TURN:http://tools.ietf.org/html/rfc5766

ICE4J:http://code.google.com/p/ice4j/