什么是内网穿透

正常我们使用的电脑都是在局域网内部,我们的 ip 也是私有 ip。当我们请求网页的时候,先通过路由器,然后再到达目的服务器。既然我们自己电脑上的 ip 是私有的,服务器返回的网页是怎么到达我们个人的电脑上的呢?这个其实是路由器帮我们解决了这个问题。当我们的请求到达路由器时,它会将数据包的的原ip地址和端口替换为外网的ip和端口,这个外网的端口是随机产生的没有冲突的。服务端收到消息之后,自然会向这个外网ip和端口发送数据,而路由器会把所有接收到的关于这个外网端口的数据全部发给我们内网的电脑。相当于我们内网的ip+端口映射了一个外网的ip+端口,只要这个映射关系存在,内网就能与外网打通。这个机制其实可以通过 napt 协议自动来实现,无需手动配置。总结起来就是,我们内网向外网请求,然后有一个ip+端口的映射关系,然后就能愉快的上网了。但是有一个问题,就是这个映射关系只有内网才能触发,而且不用的话很快会清除并不会永存。如果外网想随时请求内网里的服务怎么办?我们把这个映射弄成持久的就行了,我们可以去路由器里手动配置这个映射关系,当然我们也可以通过 upnp 协议来自动设置,这样在我们写的程序里,只要发一个upnp 协议的消息就可以设置这个映射关系了。有了这个映射关系,外网甚至是另一个内网就可以通过我们的外网ip+端口,访问我们内网的服务了。也就是达到了所谓的内网穿透。

什么是 upnp

全名为 Universal Plug and Play,通用即插即用,通过 这里 可以详细了解。这是一套开放的协议,旨在让局域网中的设备能够无缝连接并自动交换控制命令和信息,独立于任何操作系统、编程语言。最大的特点就是零配置和自动发现。具体的协议栈如下: upnp.png

可以看到,它们是基于互联网中已有的协议,又在上层定义了自己特有的一套协议。该协议栈中分为两个角色,被控制的设备和控制点,被控制的设备类似于一个server,控制点类似于一个client,它们之间通过这套协议通信。这两种都可以在各种类型的设备上实现比如个人电脑、嵌入式设备等。

所有的设备先获取ip地址,然后具体的工作过程可分为以下几步:

1. 设备发现

通过 ssdp 协议实现,该协议传输层使用 udp 协议,具体内容如下:

当控制点加入网络时,发送组播寻找 upnp 设备:

1
2
3
4
5
6
7
8
M-SEARCH * HTTP/1.1  
HOST: 239.255.255.250:1900  
MAN: "ssdp:discover"  
MX: seconds to delay response  
ST: search target  
USER-AGENT: OS/version UPnP/2.0 product/version  
CPFN.UPNP.ORG: friendly name of the control point   
CPUUID.UPNP.ORG: uuid of the control point 

返回:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
HTTP/1.1 200 OK  
CACHE-CONTROL: max-age = seconds until advertisement expires  
DATE: when response was generated  
EXT:  
LOCATION: URL for UPnP description for root device  
SERVER: OS/version UPnP/2.0 product/version  
ST: search target  
USN: composite identifier for the advertisement  
BOOTID.UPNP.ORG: number increased each time device sends an initial announce or an update message  
CONFIGID.UPNP.ORG: number used for caching description information  
SEARCHPORT.UPNP.ORG: number identifies port on which device responds to unicast M-SEARCH 

当 upnp 设备加入网络,发送组播宣告自己的存在:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
NOTIFY * HTTP/1.1  
HOST: 239.255.255.250:1900  
LOCATION: URL for UPnP description for root device  
NT: notification type  
NTS: ssdp:update  
USN: composite identifier for the advertisement  
BOOTID.UPNP.ORG: BOOTID value that the device has used in its previous announcements  
CONFIGID.UPNP.ORG: number used for caching description information  
NEXTBOOTID.UPNP.ORG: new BOOTID value that the device will use in subsequent announcements  
SEARCHPORT.UPNP.ORG: number identifies port on which device responds to unicast M-SEARCH   

2. 设备描述

控制点获取设备描述,比如 UUID、服务

通过 soap 协议实现,该协议是基于 http 协议,传输层使用 tcp 协议,使用 xml 描述设备和服务:

设备描述:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="utf-8"?>

<root xmlns="urn:schemas-upnp-org:device-1-0" configId="configuration number">  
  <specVersion> 
    <major>2</major>  
    <minor>0</minor> 
  </specVersion>  
  <device> 
    <deviceType>urn:schemas-upnp-org:device:deviceType:v</deviceType>  
    <friendlyName>short user-friendly title</friendlyName>  
    <manufacturer>manufacturer name</manufacturer>  
    <manufacturerURL>URL to manufacturer site</manufacturerURL>  
    <modelDescription>long user-friendly title</modelDescription>  
    <modelName>model name</modelName>  
    <modelNumber>model number</modelNumber>  
    <modelURL>URL to model site</modelURL>  
    <serialNumber>manufacturer's serial number</serialNumber>  
    <UDN>uuid:UUID</UDN>  
    <UPC>Universal Product Code</UPC>  
    <iconList> 
      <icon> 
        <mimetype>image/format</mimetype>  
        <width>horizontal pixels</width>  
        <height>vertical pixels</height>  
        <depth>color depth</depth>  
        <url>URL to icon</url> 
      </icon>  
      <!-- XML to declare other icons, if any, go here --> 
    </iconList>  
    <serviceList> 
      <service> 
        <serviceType>urn:schemas-upnp-org:service:serviceType:v</serviceType>  
        <serviceId>urn:upnp-org:serviceId:serviceID</serviceId>  
        <SCPDURL>URL to service description</SCPDURL>  
        <controlURL>URL for control</controlURL>  
        <eventSubURL>URL for eventing</eventSubURL> 
      </service>  
      <!-- Declarations for other services defined by a UPnP Forum working committee          (if any) go here -->  
      <!-- Declarations for other services added by UPnP vendor (if any) go here --> 
    </serviceList>  
    <deviceList> 
      <!-- Description of embedded devices defined by a UPnP Forum working committee         (if any) go here -->  
      <!-- Description of embedded devices added by UPnP vendor (if any) go here --> 
    </deviceList>  
    <presentationURL>URL for presentation</presentationURL> 
  </device> 
</root>

服务描述:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?xml version="1.0" encoding="utf-8"?>

<scpd xmlns="urn:schemas-upnp-org:service-1-0" xmlns:dt1="urn:domain-name:more-datatypes" configId="configuration number">  
  <specVersion> 
    <major>2</major>  
    <minor>0</minor> 
  </specVersion>  
  <actionList> 
    <action> 
      <name>actionName</name>  
      <argumentList> 
        <argument> 
          <name>argumentNameIn1</name>  
          <direction>in</direction>  
          <relatedStateVariable>stateVariableName</relatedStateVariable> 
        </argument>  
        <argument> 
          <name>argumentNameOut1</name>  
          <direction>out</direction>  
          <retval/>  
          <relatedStateVariable>stateVariableName</relatedStateVariable> 
        </argument>  
        <argument> 
          <name>argumentNameOut2</name>  
          <direction>out</direction>  
          <relatedStateVariable>stateVariableName</relatedStateVariable> 
        </argument> 
      </argumentList> 
    </action> 
  </actionList>  
  <serviceStateTable> 
    <stateVariable> 
      <name>variableName</name>  
      <dataType>basic data type</dataType>  
      <defaultValue>default value</defaultValue>  
      <allowedValueRange> 
        <minimum>minimum value</minimum>  
        <maximum>maximum value</maximum>  
        <step>increment value</step> 
      </allowedValueRange> 
    </stateVariable>  
    <stateVariable> 
      <name>variableName</name>  
      <dataType type="dt1:variable data type">string</dataType>  
      <defaultValue>default value</defaultValue>  
      <allowedValueList> 
        <allowedValue>enumerated value</allowedValue> 
      </allowedValueList> 
    </stateVariable>  
    <stateVariable> 
      <name>variableName</name>  
      <dataType type="dt2:vendor data type">string</dataType>  
      <defaultValue>default value</defaultValue> 
    </stateVariable> 
  </serviceStateTable> 
</scpd>

3. 设备控制(描述之后即可发生)

控制点发送控制命令给设备

通过 soap 协议实现,:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
POST path control URL HTTP/1.0  
HOST: hostname:portNumber  
CONTENT-LENGTH: bytes in body  
CONTENT-TYPE: text/xml; charset="utf-8"  
USER-AGENT: OS/version UPnP/2.0 product/version  
SOAPACTION: "urn:schemas-upnp-org:service:serviceType:v#actionName"

<?xml version="1.0" encoding="utf-8"?>

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">  
  <s:Body> 
    <u:actionName xmlns:u="urn:schemas-upnp-org:service:serviceType:v">  
      <argumentName>in arg value</argumentName>  
      <!-- other in args and their values  go here, if any --> 
    </u:actionName> 
  </s:Body> 
</s:Envelope>

返回:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
HTTP/1.0 200 OK  
CONTENT-TYPE: text/xml; charset="utf-8"  
DATE: when response was generated  
SERVER: OS/version UPnP/2.0 product/version  
CONTENT-LENGTH: bytes in body

<?xml version="1.0" encoding="utf-8"?>

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">  
  <s:Body> 
    <u:actionNameResponse xmlns:u="urn:schemas-upnp-org:service:serviceType:v">  
      <argumentName>out arg value</argumentName>  
      <!-- other out args and their values go here, if any --> 
    </u:actionNameResponse> 
  </s:Body> 
</s:Envelope>

4. 设备事件(描述之后即可发生)

设备状态更改,通知订阅监听事件的的控制点

5. 展示(描述之后即可发生)

控制点在获取设备的描述信息后比如url,就可以通过网页的形式展示信息

我们用来穿透内网的用到的就是步骤 1、2、3,接下来我们就通过代码来演示一下

代码演示

为了演示,在局域网内部搭建了一个子网,这个子网用于模拟内网,外边的局域网用于模拟外网。内外网各有一台机器 a、b。

首先获取机器 a 的 ip,执行如下命令,获取到 a 的 ip 为 192.168.0.100

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ ifconfig |grep "inet"
	inet 127.0.0.1 netmask 0xff000000
	inet6 ::1 prefixlen 128
	inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1
	inet6 fe80::aede:48ff:fe00:1122%en6 prefixlen 64 scopeid 0x5
	inet6 fe80::828:a93c:e028:17a6%en1 prefixlen 64 secured scopeid 0x7
	inet 192.168.0.100 netmask 0xffffff00 broadcast 192.168.0.255
	inet6 fe80::e437:eff:fe97:3ff8%awdl0 prefixlen 64 scopeid 0x9
	inet6 fe80::e437:eff:fe97:3ff8%llw0 prefixlen 64 scopeid 0xa
	inet6 fe80::a4a:662a:3383:708f%utun0 prefixlen 64 scopeid 0x10
	inet6 fe80::6085:579d:d0b6:7aa1%utun1 prefixlen 64 scopeid 0x11

外网 b 的 ip 为 192.168.1.73,在内网中测试 b 联通性,访问正常

1
2
3
4
5
6
$ ping 192.168.1.73
PING 192.168.1.73 (192.168.1.73): 56 data bytes
64 bytes from 192.168.1.73: icmp_seq=0 ttl=63 time=1.706 ms
64 bytes from 192.168.1.73: icmp_seq=1 ttl=63 time=7.846 ms
64 bytes from 192.168.1.73: icmp_seq=2 ttl=63 time=1.561 ms
64 bytes from 192.168.1.73: icmp_seq=3 ttl=63 time=8.108 ms

在外网中测试内网 a 的联通性,无法访问,因为此时还未穿透内网,由外网无法访问内网的机器。

1
2
3
4
5
6
$ ping 192.168.0.100
PING 192.168.0.100 (192.168.0.100) 56(84) bytes of data.

^C
--- 192.168.0.100 ping statistics ---
100 packets transmitted, 0 received, 100% packet loss, time 101364ms

我们在机器 a 上运行以下测试代码,这段代码会在发现设备(也就是路由器)之后,将外网的 6666 端口映射到内网 192.168.0.100 的 8888端口,并监听 8888端口:

upnp 协议相关代码使用的是 btcd 中的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func TestDiscover(t *testing.T) {
	discovered := discoverUPnP()
	if discovered == nil {
		t.Fatalf("not discovered")
	}
	upnp, _ := discovered.(*upnp)
	t.Log(upnp.service)
	err:=upnp.AddMapping("tcp", 6666, 8888,
		"hcd listen port", 20*60)
	if err!=nil{
		t.Fatal(err)
	}
	extIP,_:=upnp.ExternalIP()
	t.Log("ExternalIP:",extIP)
	interIP,_:=upnp.internalAddress()
	t.Log("internalAddress:",interIP)


	l, err := net.Listen("tcp", ":8888")
	if err != nil {
		fmt.Println("listen error:", err)
		return
	}

	for {
		client, err := l.Accept()
		if err != nil {
			fmt.Println("accept error:", err)
			break
		}
		// start a new goroutine to handle
		// the new connection.
		t.Log("a client come:",client.RemoteAddr())
		break
	}
}

路由器的外网 ip 为 192.168.1.43,从 b 访问路由器 6666 端口,执行如下命令:

1
telnet 192.168.1.43  6666

机器 a 输出如下,说明外网的机器 b 成功穿透了内网访问到了机器 a。

1
2
3
4
natupnp_test.go:39: IGDv1-IP1
natupnp_test.go:46: ExternalIP: 192.168.1.143
natupnp_test.go:48: internalAddress: 192.168.0.100
natupnp_test.go:65: a client come: 192.168.1.73:57626