美文网首页
VPN学习笔记

VPN学习笔记

作者: zmj_action | 来源:发表于2023-02-19 22:45 被阅读0次

前言

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拦截并返回给打开它的上层程序,上层程序可以通过udptcpicmp 协议将原始的数据包发送到目标主机。

那么 当目标主机通过网络接受到数据包后再写入到 /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参数:

一起动手写一个VPN

VPN 原理以及实现

浅谈tun设备

Creating a mesh VPN tool for fun and learning

21张图带你了解什么是VPN

华为VPN学习指南

相关文章

  • 【分手礼物】献给朗读研习社5班同学的五款APP推荐

    今天给大家推荐4款学习成长类APP,分别是趁早、英语趣配音、91VPN、欧路词典、印象笔记。 一、趁早 推荐理由:...

  • 网络编程学习:vpn实现

    想了解vpn的实现原理,找了个大佬开源的vpn代码,分析学习该代码https://github.com/net-b...

  • VPN

    一、VPN概述 虚拟专用网络(Virtual Private Network,VPN)在VPN客户机与VPN网关之...

  • VPN技术专题系列目录

    VPN原理及实现1:VPN概念及要点 VPN原理及实现2:一般理论 VPN原理及实现3:隧道的一种实现 VPN原理...

  • 友盟 分享 Twitter 问题

    解决方法::::VPN。。。VPN。。。VPN。。。。坑了一天。 问题:[TwitterKit] did enco...

  • 网络协议 Day22 VPN、缓存、爬虫

    一、VPN 1. VPN 的全称是什么?VPN 的工作原理是什么(最重要的一点要说到)? VPN(Virtual ...

  • vpn的概念

    一、vpn是什么 vpn virtual private network 虚拟私有网络 首先大家要明白VPN是英文...

  • 学习资料

    python 基础语法学习:莫凡课堂 机器深度算法学习:Google在线课堂-机器学习(须VPN) python ...

  • 解决Charles不抓包

    背景:开启了Charles,VPN 解决方案: 先关闭VPN与Charles,首先打开Charles再用vpn 注...

  • 天才也要学习之VPN学习

    什么是VPN ? 个人理解是一个身份添加中心,比如,你在外面的时候需要连接公司内网,但是外网肯定是连不进去的,这个...

网友评论

      本文标题:VPN学习笔记

      本文链接:https://www.haomeiwen.com/subject/olewkdtx.html