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), 来自 iAddr:iPort 的任何数据包将通过 eAddr:ePort 发送.
- 仅只有接收到主机(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通信解决方法:
(3)如果通信双方一方拥有独立的公网地址另一方在NAT后面,那么可以由位于NAT后面的一方主动发起通信请求;
(4)如果通信双方都位于NAT后面,且双方的NAT类型都是cone NAT,那么可以通过一个STUN服务器发现自己的NAT类型以及内网和外网传输地址映射信息,然后通过Signaling(信令服务器,实现了SIP协议的主机)交换彼此的NAT类型及内网和外网传输地址映射信息,然后通过UDP打洞的方式建立通信连接;
2.协议及用到的相关技术介绍:
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地址;
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地址等。
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 }