VPN学习笔记
前言
VPN技术是企业比较常用的通信技术,如果一个企业的分公司和总部的互访,或者出差员工需要访问总部的网络,都会使用VPN技术。
VPN是一类技术的统称,随着技术的发展,产生了多种可以实现VPN解决方案,如IPSec VPN、GRE VPN、L2TP VPN和SSL VPN等。但这些VPN解决方案都有两个共同的基本特点:
- 主要应用于通过公共的Internet进行远程网络连接,满足了远程网络连接的便捷性;
- 不是直接通过公共的Internet来传输数据,而是会采取各种安全保密技术(或称“隧道”技术),使得人们担心的安全问题也随之得到解决。
0x00 VPN产生的背景
image.png
如图所示,企业的总部和分支机构位于不同区域(比如位于不同的国家或城市),当分支机构员工需访问总部服务器的时候,数据传输要经过Internet。由于Internet中存在多种不安全因素,则当分支机构的员工向总部服务器发送访问请求时,报文容易被网络中的黑客窃取或篡改。最终造成数据泄密、重要数据被破坏等后果。
那么有没有一种技术既能实现总部和分部间的互通,也能够保证数据传输的安全性呢?
答案是当然有。
为了防止信息泄露,可以在总部和分支机构之间搭建一条物理专网连接。
image.png
物理专线:
- 物理专用信道就是在服务商到用户之间铺设一条专用的线路,线路只给用户独立使用,其他数据不能进入此线路。例如SDH、MSTP
确实能解决当前的问题,但是价格比较昂贵(总行,分行银行使用)
那么有没有成本也比较低的方案呢?
有,那就是通过公网建立私有数据通道,例如VPN
image.png
虚拟专线:
- 虚拟专用信道通过公网建立私有数据通道,例如VPN
总结:
- 走公网,但是公网不安全,你的隐私可能会被别人偷窥
- 租用专线的方式,很贵
- 使用VPN的方式,安全又不贵
0x01 什么是VPN
VPN(Virtual Private Network) ,即虚拟个人网络,是指在公共网络中建立虚拟专用通信网络,基本原理是利用隧道技术,把要传输的袁术协议数据包封装在隧道协议中进行传输
0x02 VPN分类
站点到站点VPN(企业总部与分支机构,企业与合作伙伴)
- GRE(Generic Routing Encapsulation,通过路由封装)
- IPSec(Internet protocol security,Internet 安全协议)
-
MPLS(Multi-Protocol Label Switching,多协议标签交换)
image.png
远程访问VPN(出差员工访问企业内部资源,移动用户访问企业内部)
- IPSec
- PPTP(Point-to-Point Tunneling Protocol,点对点隧道协议)
- L2TP(Layer 2 Tunneling Protocol,二层隧道协议) + IPSec
-
SSL VPN (open VPN)
image.png
0x03 VPN如何工作的
1、 GRE协议
GRE generic routing encapsulation 通用理由封装,是简单VPN,GRE是三层隧道协议,采用了Tunnel隧道技术
报文格式🔗:
image.png
GRE 工作原理:
image.png
image.png
- 首先没有做NAT,192.168.1.1 是不能访问 192.168.2.1
- GRE封装数据包,产生新的IP头部(S:100.1.1.1 D:100.1.2.2)送到目的之后拆包
- 可以看到原始报文在隧道的一端进行封装,封装后的数据在公网上传输,在隧道另一端进行解封装,从而实现了数据的传输。
优点:支持IP网络最为承载网络,支持多种协议,支持IP组播,配置简单,容易部署
缺点:缺少保护功能,不能执行如认证,加密,以及数据完整性检查
因为安全上的限制,GRE通常不能用作一个完整的VPN解决方案,然而它可以和其他的解决方案结合使用,例如IPSec,来产生一个极强大的,具有扩展性的VPN实施方案
2、IPSec协议
前面说了,使用GRE不安全,所以接下来我们看一种十分安全的VPN,IPSec VPN。基于 IP 协议的安全隧道协议,有如下特点:
IPSec不是一个单独的协议,而是包括一组协议,包括AH(Authentication Header 认证头)协议,ESP(Eccapsulating Security Payload 封装有效载荷)协议,IKE(Internet key Exchange,秘钥管理协议),以及用户身份认证和数据加密的一系列算法
更详细介绍:刘超<<趣谈网络协议>>,<<华为VPN指南>>
0x04 Tun设备
那么在代码层面 VPN 是如何实现的呢?VPN隧道的实现依赖于Linux内核提供的tun虚拟网络接口,tun设备一端连着内核网络协议栈,另一端连着用户态程序,用户态程序可以通过文件句柄的形式操作tun设备,tun设备对应的设备文件为 /dev/net/tun。当内核发送一个数据包给tun设备时,tun设备会把该数据包转给用户态程序,即用户态程序通过文件句柄就能读到tun设备过来的数据包;用户态程序对该文件句柄的写操作也会通过tun设备转换成一个数据报文传给内核网络协议栈。
image.png
// 创建tun0设备
[root@dev tun]$ ip tuntap add dev tun0 mod tun
[root@dev tun]$ ip link show | grep tun
5: tun0: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 500
// 配置IP并up
[root@dev tun]$ ifconfig tun0 1.1.1.101/24 up
[root@dev tun]$ ifconfig tun0
tun0: flags=4241<UP,POINTOPOINT,NOARP,MULTICAST> mtu 1500
inet 1.1.1.101 netmask 255.255.255.0 destination 1.1.1.101
unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 500 (UNSPEC)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
[root@dev tun]$ route -n | grep tun
Destination Gateway Genmask Flags Metric Ref Use Iface
1.1.1.0 0.0.0.0 255.255.255.0 U 0 0 0 tun0
目的是1.2.3.0 的包,直接通过tun0转发数据包
代码实现:
用户态程序通过 icmp 协议将原始的数据包发送到目标主机。当目标主机通过网络接受到数据包后再写入到 /dev/net/tun 设备中,/dev/net/tun 再将数据包注入到内核的网络协议栈按照正常到达的数据包来处理
package main
import (
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"log"
"os"
"syscall"
"unsafe"
)
const (
// tun设备文件
tunDeviceFile = "/dev/net/tun"
// tun设备名称
tunDeviceName = "tun0"
// 默认MTU为1500,此处用作数据buffer大小
defaultMTU = 1500
)
// ifReq 表示网络接口相关请求
type ifReq struct {
// name字段可以用来存储接口的名称(例如"eth0")
name [16]byte
// flags字段可以用来存储与该接口相关的各种标志或设置
flags uint16
}
func main() {
// 打开tun设备文件
tun, err := os.OpenFile(tunDeviceFile, os.O_RDWR, 0)
if err != nil {
log.Printf("OpenFile error: %s", err.Error())
return
}
defer tun.Close()
log.Printf("open tun file %s success", tunDeviceFile)
// ioctl设置
// IFF_TUN 和 IFF_TAP, TUNSETIF F定义在了 linux/if_tun.h 这个头文件中
// IFF_TUN 和 IFF_TAP 则表示是要使用 tun 类型还是 tap 类型的虚拟网卡
// TUNSETIFF 这个常量是告诉 ioctl 要完成虚拟网卡的注册,
// IFF_NO_PI - 不需要提供包的信息
var ir = ifReq{
flags: syscall.IFF_TUN | syscall.IFF_NO_PI,
}
copy(ir.name[:], tunDeviceName)
// 执行系统调用。传递给第一个参数的是 SYS_IOCTL 常量, 表明正在进行 ioctl 系统调用。
// 第二个参数是名为 tun 的对象的文件描述符,
// 第三个参数是 TUNSETIFF 常量, 用于设置 TUN/TAP 设备的接口。
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, tun.Fd(), syscall.TUNSETIFF, uintptr(unsafe.Pointer(&ir)))
if errno != 0 {
log.Printf("ioctl error: expect 0 but got %d", errno)
return
}
log.Printf("ioctl success")
buffer := make([]byte, defaultMTU)
for {
// tun 读取数据到 buffer
n, err := tun.Read(buffer)
if err != nil {
log.Printf("read data from tun error: %s", err.Error())
return
}
// %x是十六进制输出数据
log.Printf("read %d bytes data from tun device: %x", n, buffer[:n])
/* ICMP数据处理部分start */
// 前20个字节是IP首部,因此解析ICMP报文是从第21字节开始
// 注意程序的下标是从0开始
const protocolICMP = 1
message, err := icmp.ParseMessage(protocolICMP, buffer[20:n])
if err != nil {
log.Printf("icmp ParseMessage error")
return
}
// 修改ICMP报文类型为0,结合ICMP代码字段(echo request报文时为0)
// 表示将该报文改为了echo reply
const typeEchoReply = 0
message.Type = ipv4.ICMPType(typeEchoReply)
// Marshal过程中会对ICMP的校验和重新计算
icmpBytes, err := message.Marshal(nil)
if err != nil {
log.Printf("icmp message marshal error: %s", err.Error())
return
}
log.Printf("icmp bytes length: %d", len(icmpBytes))
/* ICMP数据处理部分end */
/* IP首部数据处理部分start */
// 前20个字节是IP首部,IP首部解析时只解析到第20字节数据
ipv4Header, err := ipv4.ParseHeader(buffer[:20])
if err != nil {
log.Printf("ipv4 ParseHeader error: %s", err.Error())
return
}
// 回程报文交换源、目的地址
ipv4Header.Src, ipv4Header.Dst = ipv4Header.Dst, ipv4Header.Src
// Marshal过程中会对IP首部的校验和重新计算
ipv4HeaderBytes, err := ipv4Header.Marshal()
if err != nil {
log.Printf("ipv4 header marshal error: %s", err.Error())
return
}
log.Printf("ipv4 header length: %d", len(ipv4HeaderBytes))
/* IP首部数据处理部分end */
// 把IP首部数据和ICMP数据拼接起来组成完成的ICMP echo reply三层报文
reply := append(ipv4HeaderBytes, icmpBytes...)
log.Printf("reply: %x", reply)
// 将ICMP echo reply报文写入tun设备
n, err = tun.Write(reply)
if err != nil {
log.Printf("write data to tun device error: %s", err.Error())
return
}
log.Printf("write %d bytes data to tun device", n)
}
}
image.png
当创建了虚拟网卡tun设备后,发到这个网卡的数据包会被/dev/net/tun拦截并返回给打开它的上层程序,上层程序可以通过udp,tcp,icmp 协议将原始的数据包发送到目标主机。
那么 当目标主机通过网络接受到数据包后再写入到 /dev/net/tun 设备中,/dev/net/tun 再将数据包注入到内核的网络协议栈按照正常到达的数据包来处理 这样就可以实现一个简单的VPN
0x05 简单simple VPN实现
实现思路:
node1上数据包到了tun1后,tun1设备会把该数据包转到simple VPN程序中,如果simple VPN以UDP方式把数据从node1发到node2上的simple VPN,node2上simple VPN收到数据后再把该数据写入node2上的tun1,这就相当于node1上从tun1进来的数据会从node2上的tun1出来。回程报文也类似流程,这样就把两个节点的tun1设备打通了
image.png
创建一个tun设备tun0,给该tun设备配置IP并up:
// node1
// 创建tun1设备
ip tuntap add dev tun1 mod tun
// 配置IP并UP
ifconfig tun1 1.2.3.100/24 up
tun1: flags=4241<UP,POINTOPOINT,NOARP,MULTICAST> mtu 1500
inet 1.2.3.100 netmask 255.255.255.0 destination 1.2.3.200
unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 500 (UNSPEC)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
// 查看路由
route -n
1.2.3.0 0.0.0.0 255.255.255.0 U 0 0 0 tun1
// node2
// 创建tun1设备
ip tuntap add dev tun1 mod tun
// 配置IP并UP
ifconfig tun1 1.2.3.200/24 up
tun1: flags=4241<UP,POINTOPOINT,NOARP,MULTICAST> mtu 1500
inet 1.2.3.200 netmask 255.255.255.0 destination 1.2.3.200
unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 500 (UNSPEC)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 (0.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
// 查看路由
route -n
1.2.3.0 0.0.0.0 255.255.255.0 U 0 0 0 tun1
代码实现
package main
import (
"flag"
"fmt"
"log"
"net"
"os"
"syscall"
"unsafe"
)
const (
// tun设备文件
tunDeviceFile = "/dev/net/tun"
// tun设备名称
tunDeviceName = "tun1"
// 默认MTU为1500,此处用作数据buffer大小
// 一个IP头(20字节)和一个UDP头(8)字节。如果f设置1500,
// 加上IP和UDP头的28字节数据,到达宿主机eth0的时候最大报文会超过eth0的MTU,eth0会把该数据包丢弃
defaultMTU = 1472
udpPort = 8285
)
var dstUDPHost = flag.String("d", "127.0.0.1:8285", "destination UDP host")
type ifReq struct {
name [16]byte
flags uint16
}
func main() {
flag.Parse()
// 初始化tun设备
tun, err := InitTunDevice(SendUDPData)
if err != nil {
log.Printf("initTunDevice error: %s", err.Error())
return
}
defer tun.Close()
log.Printf("initTunDevice tun with file %s success", tunDeviceFile)
handler := func(data []byte) {
n, err := tun.Write(data)
if err != nil {
log.Printf("handler write data to tun device error: %s", err.Error())
return
}
log.Printf("handler write %d bytes data to tun device", n)
}
UDPServer(handler)
}
// InitTunDevice 初始化tun设备
// 调用方负责关闭文件句柄
func InitTunDevice(udpSend func([]byte) (int, error)) (*os.File, error) {
// 打开tun设备文件
tun, err := os.OpenFile(tunDeviceFile, os.O_RDWR, 0)
if err != nil {
return nil, fmt.Errorf("os.OpenFile error: %s", err.Error())
}
// ioctl设置
var ir = ifReq{
flags: syscall.IFF_TUN | syscall.IFF_NO_PI,
}
copy(ir.name[:], tunDeviceName)
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, tun.Fd(), syscall.TUNSETIFF, uintptr(unsafe.Pointer(&ir)))
if errno != 0 {
return nil, fmt.Errorf("ioctl error: expect 0 but got %d", errno)
}
log.Printf("ioctl success")
go func() {
buffer := make([]byte, defaultMTU)
for {
n, err := tun.Read(buffer)
if err != nil {
log.Printf("read data from tun error: %s", err.Error())
return
}
log.Printf("read %d bytes data from tun device", n)
num, err := udpSend(buffer[:n])
if err != nil {
log.Printf("udpSend error: %s", err.Error())
return
}
log.Printf("udp send %d bytes data", num)
}
}()
return tun, nil
}
// UDPServer 接收UDP数据
func UDPServer(handler func(data []byte)) {
updServerHost := fmt.Sprintf(":%d", udpPort)
conn, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: udpPort,
})
if err != nil {
log.Fatalf("net.Listen error: %s", err.Error())
}
defer conn.Close()
log.Printf("udp listen on: %s", updServerHost)
var buffer = make([]byte, defaultMTU)
for {
n, _, err := conn.ReadFromUDP(buffer)
if err != nil {
fmt.Printf("conn.ReadFromUDP err:[%v]\n", err)
}
defer conn.Close()
log.Printf("read %d bytes data from udp", n)
// 接收数据
go handler(buffer[:n])
}
}
// SendUDPData 发送udp数据
func SendUDPData(data []byte) (int, error) {
serverAddr, err := net.ResolveUDPAddr("udp", *dstUDPHost)
if err != nil {
log.Fatalln("failed to resolve server addr:", err)
}
conn, err := net.DialUDP("udp", nil, serverAddr)
if err != nil {
return 0, fmt.Errorf("net.Dial error: %s", err.Error())
}
return conn.Write(data)
}
// 在node1 执行
go run main.go -d 10.128.128.28:8285
// 在node2 执行
go run main.go -d 10.128.128.140:8285
验证
验证下ping功能:
// node1
$ ping -c 1 1.2.3.200
PING 1.2.3.200 (1.2.3.200) 56(84) bytes of data.
64 bytes from 1.2.3.200: icmp_seq=1 ttl=62 time=1.05 ms
--- 1.2.3.200 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.053/1.053/1.053/0.000 ms
// node2
$ ping -c 1 1.2.3.100
PING 1.2.3.100 (1.2.3.100) 56(84) bytes of data.
64 bytes from 1.2.3.100: icmp_seq=1 ttl=62 time=0.564 ms
--- 1.2.3.100 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.564/0.564/0.564/0.000 ms
0x06参数:
Creating a mesh VPN tool for fun and learning
华为VPN学习指南