前言
VPN技術(shù)是企業(yè)比較常用的通信技術(shù),如果一個(gè)企業(yè)的分公司和總部的互訪,或者出差員工需要訪問(wèn)總部的網(wǎng)絡(luò)弹灭,都會(huì)使用VPN技術(shù)。
VPN是一類技術(shù)的統(tǒng)稱揪垄,隨著技術(shù)的發(fā)展穷吮,產(chǎn)生了多種可以實(shí)現(xiàn)VPN解決方案,如IPSec VPN饥努、GRE VPN捡鱼、L2TP VPN和SSL VPN等。但這些VPN解決方案都有兩個(gè)共同的基本特點(diǎn):
- 主要應(yīng)用于通過(guò)公共的Internet進(jìn)行遠(yuǎn)程網(wǎng)絡(luò)連接酷愧,滿足了遠(yuǎn)程網(wǎng)絡(luò)連接的便捷性驾诈;
- 不是直接通過(guò)公共的Internet來(lái)傳輸數(shù)據(jù),而是會(huì)采取各種安全保密技術(shù)(或稱“隧道”技術(shù))溶浴,使得人們擔(dān)心的安全問(wèn)題也隨之得到解決乍迄。
0x00 VPN產(chǎn)生的背景
如圖所示,企業(yè)的總部和分支機(jī)構(gòu)位于不同區(qū)域(比如位于不同的國(guó)家或城市)士败,當(dāng)分支機(jī)構(gòu)員工需訪問(wèn)總部服務(wù)器的時(shí)候就乓,數(shù)據(jù)傳輸要經(jīng)過(guò)Internet。由于Internet中存在多種不安全因素拱烁,則當(dāng)分支機(jī)構(gòu)的員工向總部服務(wù)器發(fā)送訪問(wèn)請(qǐng)求時(shí)生蚁,報(bào)文容易被網(wǎng)絡(luò)中的黑客竊取或篡改。最終造成數(shù)據(jù)泄密戏自、重要數(shù)據(jù)被破壞等后果邦投。
那么有沒(méi)有一種技術(shù)既能實(shí)現(xiàn)總部和分部間的互通,也能夠保證數(shù)據(jù)傳輸?shù)陌踩阅兀?/p>
答案是當(dāng)然有擅笔。
為了防止信息泄露志衣,可以在總部和分支機(jī)構(gòu)之間搭建一條物理專網(wǎng)連接。
物理專線:
- 物理專用信道就是在服務(wù)商到用戶之間鋪設(shè)一條專用的線路猛们,線路只給用戶獨(dú)立使用念脯,其他數(shù)據(jù)不能進(jìn)入此線路。例如SDH弯淘、MSTP
確實(shí)能解決當(dāng)前的問(wèn)題绿店,但是價(jià)格比較昂貴(總行,分行銀行使用)
那么有沒(méi)有成本也比較低的方案呢?
有假勿,那就是通過(guò)公網(wǎng)建立私有數(shù)據(jù)通道借嗽,例如VPN
虛擬專線:
- 虛擬專用信道通過(guò)公網(wǎng)建立私有數(shù)據(jù)通道,例如VPN
總結(jié):
- 走公網(wǎng)转培,但是公網(wǎng)不安全恶导,你的隱私可能會(huì)被別人偷窺
- 租用專線的方式,很貴
- 使用VPN的方式浸须,安全又不貴
0x01 什么是VPN
VPN(Virtual Private Network) ,即虛擬個(gè)人網(wǎng)絡(luò)惨寿,是指在公共網(wǎng)絡(luò)中建立虛擬專用通信網(wǎng)絡(luò),基本原理是利用隧道技術(shù)删窒,把要傳輸?shù)脑g(shù)協(xié)議數(shù)據(jù)包封裝在隧道協(xié)議中進(jìn)行傳輸
0x02 VPN分類
站點(diǎn)到站點(diǎn)VPN(企業(yè)總部與分支機(jī)構(gòu)缤沦,企業(yè)與合作伙伴)
- GRE(Generic Routing Encapsulation,通過(guò)路由封裝)
- IPSec(Internet protocol security易稠,Internet 安全協(xié)議)
-
MPLS(Multi-Protocol Label Switching缸废,多協(xié)議標(biāo)簽交換)
遠(yuǎn)程訪問(wèn)VPN(出差員工訪問(wèn)企業(yè)內(nèi)部資源,移動(dòng)用戶訪問(wèn)企業(yè)內(nèi)部)
- IPSec
- PPTP(Point-to-Point Tunneling Protocol驶社,點(diǎn)對(duì)點(diǎn)隧道協(xié)議)
- L2TP(Layer 2 Tunneling Protocol企量,二層隧道協(xié)議) + IPSec
-
SSL VPN (open VPN)
0x03 VPN如何工作的
1、 GRE協(xié)議
GRE generic routing encapsulation 通用理由封裝亡电,是簡(jiǎn)單VPN届巩,GRE是三層隧道協(xié)議,采用了Tunnel隧道技術(shù)
報(bào)文格式??:
GRE 工作原理:
- 首先沒(méi)有做NAT份乒,192.168.1.1 是不能訪問(wèn) 192.168.2.1
- GRE封裝數(shù)據(jù)包恕汇,產(chǎn)生新的IP頭部(S:100.1.1.1 D:100.1.2.2)送到目的之后拆包
- 可以看到原始報(bào)文在隧道的一端進(jìn)行封裝,封裝后的數(shù)據(jù)在公網(wǎng)上傳輸或辖,在隧道另一端進(jìn)行解封裝瘾英,從而實(shí)現(xiàn)了數(shù)據(jù)的傳輸。
優(yōu)點(diǎn):支持IP網(wǎng)絡(luò)最為承載網(wǎng)絡(luò),支持多種協(xié)議,支持IP組播,配置簡(jiǎn)單,容易部署
缺點(diǎn):缺少保護(hù)功能,不能執(zhí)行如認(rèn)證,加密,以及數(shù)據(jù)完整性檢查
因?yàn)榘踩系南拗?GRE通常不能用作一個(gè)完整的VPN解決方案,然而它可以和其他的解決方案結(jié)合使用,例如IPSec,來(lái)產(chǎn)生一個(gè)極強(qiáng)大的,具有擴(kuò)展性的VPN實(shí)施方案
2颂暇、IPSec協(xié)議
前面說(shuō)了缺谴,使用GRE不安全,所以接下來(lái)我們看一種十分安全的VPN耳鸯,IPSec VPN湿蛔。基于 IP 協(xié)議的安全隧道協(xié)議县爬,有如下特點(diǎn):
IPSec不是一個(gè)單獨(dú)的協(xié)議阳啥,而是包括一組協(xié)議,包括AH(Authentication Header 認(rèn)證頭)協(xié)議财喳,ESP(Eccapsulating Security Payload 封裝有效載荷)協(xié)議察迟,IKE(Internet key Exchange,秘鑰管理協(xié)議),以及用戶身份認(rèn)證和數(shù)據(jù)加密的一系列算法
更詳細(xì)介紹:劉超<<趣談網(wǎng)絡(luò)協(xié)議>>,<<華為VPN指南>>
0x04 Tun設(shè)備
那么在代碼層面 VPN 是如何實(shí)現(xiàn)的呢卷拘?VPN隧道的實(shí)現(xiàn)依賴于Linux內(nèi)核提供的tun虛擬網(wǎng)絡(luò)接口喊废,tun設(shè)備一端連著內(nèi)核網(wǎng)絡(luò)協(xié)議棧祝高,另一端連著用戶態(tài)程序栗弟,用戶態(tài)程序可以通過(guò)文件句柄的形式操作tun設(shè)備,tun設(shè)備對(duì)應(yīng)的設(shè)備文件為 /dev/net/tun工闺。當(dāng)內(nèi)核發(fā)送一個(gè)數(shù)據(jù)包給tun設(shè)備時(shí)乍赫,tun設(shè)備會(huì)把該數(shù)據(jù)包轉(zhuǎn)給用戶態(tài)程序,即用戶態(tài)程序通過(guò)文件句柄就能讀到tun設(shè)備過(guò)來(lái)的數(shù)據(jù)包陆蟆;用戶態(tài)程序?qū)υ撐募浔膶懖僮饕矔?huì)通過(guò)tun設(shè)備轉(zhuǎn)換成一個(gè)數(shù)據(jù)報(bào)文傳給內(nèi)核網(wǎng)絡(luò)協(xié)議棧雷厂。
// 創(chuàng)建tun0設(shè)備
[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 的包,直接通過(guò)tun0轉(zhuǎn)發(fā)數(shù)據(jù)包
代碼實(shí)現(xiàn):
用戶態(tài)程序通過(guò) icmp
協(xié)議將原始的數(shù)據(jù)包發(fā)送到目標(biāo)主機(jī)叠殷。當(dāng)目標(biāo)主機(jī)通過(guò)網(wǎng)絡(luò)接受到數(shù)據(jù)包后再寫入到 /dev/net/tun
設(shè)備中改鲫,/dev/net/tun
再將數(shù)據(jù)包注入到內(nèi)核的網(wǎng)絡(luò)協(xié)議棧按照正常到達(dá)的數(shù)據(jù)包來(lái)處理
package main
import (
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"log"
"os"
"syscall"
"unsafe"
)
const (
// tun設(shè)備文件
tunDeviceFile = "/dev/net/tun"
// tun設(shè)備名稱
tunDeviceName = "tun0"
// 默認(rèn)MTU為1500,此處用作數(shù)據(jù)buffer大小
defaultMTU = 1500
)
// ifReq 表示網(wǎng)絡(luò)接口相關(guān)請(qǐng)求
type ifReq struct {
// name字段可以用來(lái)存儲(chǔ)接口的名稱(例如"eth0")
name [16]byte
// flags字段可以用來(lái)存儲(chǔ)與該接口相關(guān)的各種標(biāo)志或設(shè)置
flags uint16
}
func main() {
// 打開tun設(shè)備文件
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設(shè)置
// IFF_TUN 和 IFF_TAP, TUNSETIF F定義在了 linux/if_tun.h 這個(gè)頭文件中
// IFF_TUN 和 IFF_TAP 則表示是要使用 tun 類型還是 tap 類型的虛擬網(wǎng)卡
// TUNSETIFF 這個(gè)常量是告訴 ioctl 要完成虛擬網(wǎng)卡的注冊(cè)林束,
// IFF_NO_PI - 不需要提供包的信息
var ir = ifReq{
flags: syscall.IFF_TUN | syscall.IFF_NO_PI,
}
copy(ir.name[:], tunDeviceName)
// 執(zhí)行系統(tǒng)調(diào)用像棘。傳遞給第一個(gè)參數(shù)的是 SYS_IOCTL 常量, 表明正在進(jìn)行 ioctl 系統(tǒng)調(diào)用。
// 第二個(gè)參數(shù)是名為 tun 的對(duì)象的文件描述符,
// 第三個(gè)參數(shù)是 TUNSETIFF 常量, 用于設(shè)置 TUN/TAP 設(shè)備的接口壶冒。
_, _, 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 讀取數(shù)據(jù)到 buffer
n, err := tun.Read(buffer)
if err != nil {
log.Printf("read data from tun error: %s", err.Error())
return
}
// %x是十六進(jìn)制輸出數(shù)據(jù)
log.Printf("read %d bytes data from tun device: %x", n, buffer[:n])
/* ICMP數(shù)據(jù)處理部分start */
// 前20個(gè)字節(jié)是IP首部缕题,因此解析ICMP報(bào)文是從第21字節(jié)開始
// 注意程序的下標(biāo)是從0開始
const protocolICMP = 1
message, err := icmp.ParseMessage(protocolICMP, buffer[20:n])
if err != nil {
log.Printf("icmp ParseMessage error")
return
}
// 修改ICMP報(bào)文類型為0,結(jié)合ICMP代碼字段(echo request報(bào)文時(shí)為0)
// 表示將該報(bào)文改為了echo reply
const typeEchoReply = 0
message.Type = ipv4.ICMPType(typeEchoReply)
// Marshal過(guò)程中會(huì)對(duì)ICMP的校驗(yàn)和重新計(jì)算
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數(shù)據(jù)處理部分end */
/* IP首部數(shù)據(jù)處理部分start */
// 前20個(gè)字節(jié)是IP首部胖腾,IP首部解析時(shí)只解析到第20字節(jié)數(shù)據(jù)
ipv4Header, err := ipv4.ParseHeader(buffer[:20])
if err != nil {
log.Printf("ipv4 ParseHeader error: %s", err.Error())
return
}
// 回程報(bào)文交換源烟零、目的地址
ipv4Header.Src, ipv4Header.Dst = ipv4Header.Dst, ipv4Header.Src
// Marshal過(guò)程中會(huì)對(duì)IP首部的校驗(yàn)和重新計(jì)算
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首部數(shù)據(jù)處理部分end */
// 把IP首部數(shù)據(jù)和ICMP數(shù)據(jù)拼接起來(lái)組成完成的ICMP echo reply三層報(bào)文
reply := append(ipv4HeaderBytes, icmpBytes...)
log.Printf("reply: %x", reply)
// 將ICMP echo reply報(bào)文寫入tun設(shè)備
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)
}
}
當(dāng)創(chuàng)建了虛擬網(wǎng)卡tun設(shè)備后,發(fā)到這個(gè)網(wǎng)卡的數(shù)據(jù)包會(huì)被/dev/net/tun
攔截并返回給打開它的上層程序咸作,上層程序可以通過(guò)udp
锨阿,tcp
,icmp
協(xié)議將原始的數(shù)據(jù)包發(fā)送到目標(biāo)主機(jī)记罚。
那么 當(dāng)目標(biāo)主機(jī)通過(guò)網(wǎng)絡(luò)接受到數(shù)據(jù)包后再寫入到 /dev/net/tun
設(shè)備中群井,/dev/net/tun
再將數(shù)據(jù)包注入到內(nèi)核的網(wǎng)絡(luò)協(xié)議棧按照正常到達(dá)的數(shù)據(jù)包來(lái)處理 這樣就可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的VPN
0x05 簡(jiǎn)單simple VPN實(shí)現(xiàn)
實(shí)現(xiàn)思路:
node1上數(shù)據(jù)包到了tun1后,tun1設(shè)備會(huì)把該數(shù)據(jù)包轉(zhuǎn)到simple VPN程序中毫胜,如果simple VPN以UDP方式把數(shù)據(jù)從node1發(fā)到node2上的simple VPN书斜,node2上simple VPN收到數(shù)據(jù)后再把該數(shù)據(jù)寫入node2上的tun1,這就相當(dāng)于node1上從tun1進(jìn)來(lái)的數(shù)據(jù)會(huì)從node2上的tun1出來(lái)酵使〖黾回程報(bào)文也類似流程,這樣就把兩個(gè)節(jié)點(diǎn)的tun1設(shè)備打通了
創(chuàng)建一個(gè)tun設(shè)備tun0口渔,給該tun設(shè)備配置IP并up:
// node1
// 創(chuàng)建tun1設(shè)備
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
// 創(chuàng)建tun1設(shè)備
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
代碼實(shí)現(xiàn)
package main
import (
"flag"
"fmt"
"log"
"net"
"os"
"syscall"
"unsafe"
)
const (
// tun設(shè)備文件
tunDeviceFile = "/dev/net/tun"
// tun設(shè)備名稱
tunDeviceName = "tun1"
// 默認(rèn)MTU為1500样屠,此處用作數(shù)據(jù)buffer大小
// 一個(gè)IP頭(20字節(jié))和一個(gè)UDP頭(8)字節(jié)。如果f設(shè)置1500,
// 加上IP和UDP頭的28字節(jié)數(shù)據(jù)痪欲,到達(dá)宿主機(jī)eth0的時(shí)候最大報(bào)文會(huì)超過(guò)eth0的MTU悦穿,eth0會(huì)把該數(shù)據(jù)包丟棄
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設(shè)備
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設(shè)備
// 調(diào)用方負(fù)責(zé)關(guān)閉文件句柄
func InitTunDevice(udpSend func([]byte) (int, error)) (*os.File, error) {
// 打開tun設(shè)備文件
tun, err := os.OpenFile(tunDeviceFile, os.O_RDWR, 0)
if err != nil {
return nil, fmt.Errorf("os.OpenFile error: %s", err.Error())
}
// ioctl設(shè)置
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數(shù)據(jù)
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)
// 接收數(shù)據(jù)
go handler(buffer[:n])
}
}
// SendUDPData 發(fā)送udp數(shù)據(jù)
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 執(zhí)行
go run main.go -d 10.128.128.28:8285
// 在node2 執(zhí)行
go run main.go -d 10.128.128.140:8285
驗(yàn)證
驗(yàn)證下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參數(shù):
Creating a mesh VPN tool for fun and learning
華為VPN學(xué)習(xí)指南