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地址等。
import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.DatagramSocket; import java.net.SocketAddress; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import org.ice4j.Transport; import org.ice4j.TransportAddress; import org.ice4j.ice.Agent; import org.ice4j.ice.Component; import org.ice4j.ice.IceMediaStream; import org.ice4j.ice.IceProcessingState; import org.ice4j.ice.LocalCandidate; import org.ice4j.ice.NominationStrategy; import org.ice4j.ice.RemoteCandidate; import org.ice4j.ice.harvest.StunCandidateHarvester; import org.ice4j.ice.harvest.TurnCandidateHarvester; import org.ice4j.security.LongTermCredential; import test.SdpUtils; public class IceClient { private int port; private String streamName; private Agent agent; private String localSdp; private String remoteSdp; private String[] turnServers = new String[] { "stun.jitsi.net:3478" }; private String[] stunServers = new String[] { "stun.stunprotocol.org:3478" }; private String username = "guest"; private String password = "anonymouspower!!"; private IceProcessingListener listener; static Logger log = Logger.getLogger(IceClient.class); public IceClient(int port, String streamName) { this.port = port; this.streamName = streamName; this.listener = new IceProcessingListener(); } public void init() throws Throwable { agent = createAgent(port, streamName); agent.setNominationStrategy(NominationStrategy.NOMINATE_HIGHEST_PRIO); agent.addStateChangeListener(listener); agent.setControlling(false); agent.setTa(10000); localSdp = SdpUtils.createSDPDescription(agent); log.info("=================== feed the following" + " to the remote agent ==================="); System.out.println(localSdp); log.info("======================================" + "========================================\n"); } public DatagramSocket getDatagramSocket() throws Throwable { LocalCandidate localCandidate = agent .getSelectedLocalCandidate(streamName); IceMediaStream stream = agent.getStream(streamName); List<Component> components = stream.getComponents(); for (Component c : components) { log.info(c); } log.info(localCandidate.toString()); LocalCandidate candidate = (LocalCandidate) localCandidate; return candidate.getDatagramSocket(); } public SocketAddress getRemotePeerSocketAddress() { RemoteCandidate remoteCandidate = agent .getSelectedRemoteCandidate(streamName); log.info("Remote candinate transport address:" + remoteCandidate.getTransportAddress()); log.info("Remote candinate host address:" + remoteCandidate.getHostAddress()); log.info("Remote candinate mapped address:" + remoteCandidate.getMappedAddress()); log.info("Remote candinate relayed address:" + remoteCandidate.getRelayedAddress()); log.info("Remote candinate reflexive address:" + remoteCandidate.getReflexiveAddress()); return remoteCandidate.getTransportAddress(); } /** * Reads an SDP description from the standard input.In production * environment that we can exchange SDP with peer through signaling * server(SIP server) */ public void exchangeSdpWithPeer() throws Throwable { log.info("Paste remote SDP here. Enter an empty line to proceed:"); BufferedReader reader = new BufferedReader(new InputStreamReader( System.in)); StringBuilder buff = new StringBuilder(); String line = new String(); while ((line = reader.readLine()) != null) { line = line.trim(); if (line.length() == 0) { break; } buff.append(line); buff.append("\r\n"); } remoteSdp = buff.toString(); SdpUtils.parseSDP(agent, remoteSdp); } public void startConnect() throws InterruptedException { if (StringUtils.isBlank(remoteSdp)) { throw new NullPointerException( "Please exchange sdp information with peer before start connect! "); } agent.startConnectivityEstablishment(); // agent.runInStunKeepAliveThread(); synchronized (listener) { listener.wait(); } } private Agent createAgent(int rtpPort, String streamName) throws Throwable { return createAgent(rtpPort, streamName, false); } private Agent createAgent(int rtpPort, String streamName, boolean isTrickling) throws Throwable { long startTime = System.currentTimeMillis(); Agent agent = new Agent(); agent.setTrickling(isTrickling); // STUN for (String server : stunServers){ String[] pair = server.split(":"); agent.addCandidateHarvester(new StunCandidateHarvester( new TransportAddress(pair[0], Integer.parseInt(pair[1]), Transport.UDP))); } // TURN LongTermCredential longTermCredential = new LongTermCredential(username, password); for (String server : turnServers){ String[] pair = server.split(":"); agent.addCandidateHarvester(new TurnCandidateHarvester( new TransportAddress(pair[0], Integer.parseInt(pair[1]), Transport.UDP), longTermCredential)); } // STREAMS createStream(rtpPort, streamName, agent); long endTime = System.currentTimeMillis(); long total = endTime - startTime; log.info("Total harvesting time: " + total + "ms."); return agent; } private IceMediaStream createStream(int rtpPort, String streamName, Agent agent) throws Throwable { long startTime = System.currentTimeMillis(); IceMediaStream stream = agent.createMediaStream(streamName); // rtp Component component = agent.createComponent(stream, Transport.UDP, rtpPort, rtpPort, rtpPort + 100); long endTime = System.currentTimeMillis(); log.info("Component Name:" + component.getName()); log.info("RTP Component created in " + (endTime - startTime) + " ms"); return stream; } /** * Receive notify event when ice processing state has changed. */ public static final class IceProcessingListener implements PropertyChangeListener { private long startTime = System.currentTimeMillis(); public void propertyChange(PropertyChangeEvent event) { Object state = event.getNewValue(); log.info("Agent entered the " + state + " state."); if (state == IceProcessingState.COMPLETED) { long processingEndTime = System.currentTimeMillis(); log.info("Total ICE processing time: " + (processingEndTime - startTime) + "ms"); Agent agent = (Agent) event.getSource(); List<IceMediaStream> streams = agent.getStreams(); for (IceMediaStream stream : streams) { log.info("Stream name: " + stream.getName()); List<Component> components = stream.getComponents(); for (Component c : components) { log.info("------------------------------------------"); log.info("Component of stream:" + c.getName() + ",selected of pair:" + c.getSelectedPair()); log.info("------------------------------------------"); } } log.info("Printing the completed check lists:"); for (IceMediaStream stream : streams) { log.info("Check list for stream: " + stream.getName()); log.info("nominated check list:" + stream.getCheckList()); } synchronized (this) { this.notifyAll(); } } else if (state == IceProcessingState.TERMINATED) { log.info("ice processing TERMINATED"); } else if (state == IceProcessingState.FAILED) { log.info("ice processing FAILED"); ((Agent) event.getSource()).free(); } } } } import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketAddress; import java.util.concurrent.TimeUnit; public class PeerA { public static void main(String[] args) throws Throwable { try { IceClient client = new IceClient(2020, "audio"); client.init(); client.exchangeSdpWithPeer(); client.startConnect(); final DatagramSocket socket = client.getDatagramSocket(); final SocketAddress remoteAddress = client .getRemotePeerSocketAddress(); System.out.println(socket.toString()); new Thread(new Runnable() { public void run() { while (true) { try { byte[] buf = new byte[1024]; DatagramPacket packet = new DatagramPacket(buf, buf.length); socket.receive(packet); System.out.println("receive:" + new String(packet.getData(), 0, packet .getLength())); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }).start(); new Thread(new Runnable() { public void run() { int count = 1; while (true) { try { byte[] buf = ("send msg " + count++ + "").getBytes(); DatagramPacket packet = new DatagramPacket(buf, buf.length); packet.setSocketAddress(remoteAddress); socket.send(packet); System.out.println("send msg"); TimeUnit.SECONDS.sleep(10); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }).start(); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }