《零入门kubernetes网络实战》视频专栏地址
https://www.ixigua.com/7193641905282875942
本篇文章视频地址(稍后上传)
本篇文章主要是练习:
- 通过tun设备来实现跨主机通信
- 测试在宿主机-1上使用curl命令可以访问宿主机-2上的web服务;
- 请求有去,有回
- 测试在宿主机-1上使用ping命令 可以ping通 宿主机-2上的tun类型的虚拟网卡设备,
- 如ping通flannel99
- 请求有去,有回
本篇文章测试时提供的代码,属于helloworld级别的,基本属于最精简的。
再下一篇文章里,为大家介绍一个开源社区提供的VPN,稍微多了点功能。
1、原理介绍 |
1.1、原理图说明 |
如下图所示:
该图只画出了请求路线,回复路线反之即可。
该图主要分为上下两个框:
- 上面代表的是宿主机-1,对外的物理网卡是eth0,IP是10.211.55.122
- 下面代表的是宿主机-2,对外的物理网卡是eth0,IP是10.211.55.123
在宿主机-1中,使用tun-vpn-client创建了tun设备,如flannel99,IP是10.244.2.2
在宿主机-2中,使用tun=vpn-server创建了tun设备,如flannel99,IP是10.244.3.3
1.2、在宿主机-1上使用curl访问宿主机-2上的web服务的数据包请求路线: |
- curl请求创建的TCP类型的数据包,经过本机的网络协议栈,路由表,防火墙规则后,
- 进入到了网络虚拟设备flannel99中,flannel99自动给TCP数据包添加一层IP报文头,20个字节;
- 源地址是10.244.2.2
- 目的地址是10.244.3.3
- 协议类型是TCP
- 网络虚拟设备flannel99新创建的数据包,会自动的转发到/dev/net/tun文件描述符里
- tun-vpn-client会一直在读取/dev/net/tun文件描述符里的数据包,将读取的数据包通过本地的UDP连接发送到对端的UDP连接里;本地的UDP连接是通过eth0网卡发送过去的。
- UDP类型的数据包到达对端的eth0网卡后,会经过路由表,防火墙,网络协议栈会到达本机的UDP服务里;
- tun-vpn-server会一直从本机的UDP服务链接里接收数据包
- tun-vpn-server将接收到的UDP数据包,写入到/dev/net/tun文件描述符里,
- 当数据包到达/dev/net/tun文件描述符后,会自动的转发到网络虚拟设备flannel99里
- 网络虚拟设备flannel99会自动将数据包中的IP报文头删除掉,只保留curl创建的TCP数据包
- 当TCP数据包从flannel99设备出来后会经过路由表,防火墙规则,网络协议栈,最终web服务接收到TCP数据包,并进行反馈。
1.3、tun-vpn-client与tun-vpn-server |
1.3.1、名称说明? |
不要被名字误解,无所谓哪个是客户端,哪个是服务器端
请求是哪个发起的,就是客户端;
被请求方就是服务器端。
都是相对的。
1.3.2、实现原理说明 |
原理是一样的。
因此,我们只介绍一下tun-vpn-client的原理即可。
2、tun-vpn-client原理说明 |
2.1、代码如下 |
package main
import (
"bytes"
"fmt"
"github.com/vishvananda/netlink"
"log"
"net"
"os"
"syscall"
"time"
"unsafe"
)
const (
tunName = "flannel99"
tunDevice = "/dev/net/tun"
ifnameSize = 16
localAddr = "10.211.55.122:8285"
remoteAddr = "10.211.55.123:8285"
tunIP = "10.244.2.2"
)
type ifreqFlags struct {
IfrnName [ifnameSize]byte
IfruFlags uint16
}
func ioctl(fd int, request, argp uintptr) error {
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), request, argp)
if errno != 0 {
fmt.Errorf("ioctl failed with '%s'\n", errno)
return fmt.Errorf("ioctl failed with '%s'", errno)
}
return nil
}
func fromZeroTerm(s []byte) string {
return string(bytes.TrimRight(s, "\000"))
}
func OpenTun(name string) (*os.File, string, error) {
tun, err := os.OpenFile(tunDevice, os.O_RDWR|syscall.O_NONBLOCK, 0)
if err != nil {
fmt.Printf("OpenTun Failed! err:%v", err.Error())
return nil, "", err
}
var ifr ifreqFlags
copy(ifr.IfrnName[:len(ifr.IfrnName)-1], []byte(name+"\000"))
ifr.IfruFlags = syscall.IFF_TUN | syscall.IFF_NO_PI
err = ioctl(int(tun.Fd()), syscall.TUNSETIFF, uintptr(unsafe.Pointer(&ifr)))
if err != nil {
fmt.Printf("OpenTun Failed! err:%v\n", err.Error())
return nil, "", err
}
ifName := fromZeroTerm(ifr.IfrnName[:ifnameSize])
return tun, ifName, nil
}
func main() {
fmt.Printf("======>Now----Client----Tun---VPN---UDP<======\n")
tunFile, err := createTun()
if err != nil {
fmt.Printf("ICMP Listen Packet Failed! err:%v\n", err.Error())
return
}
defer tunFile.Close()
udpConn, err := createUDP()
if err != nil {
fmt.Printf("UDP conn Failed! err:%v\n", err.Error())
return
}
defer udpConn.Close()
go tunToUDP(udpConn, tunFile)
go udpToTun(udpConn, tunFile)
time.Sleep(time.Hour)
}
func createUDP() (*net.UDPConn, error) {
localAddr, err := net.ResolveUDPAddr("udp", localAddr)
if err != nil {
log.Fatalln("failed to get udp socket:", err)
return nil, err
}
conn, err := net.ListenUDP("udp", localAddr)
if err != nil {
log.Fatalln("failed to listen on udp socket:", err)
return nil, err
}
return conn, nil
}
func tunToUDP(udpConn *net.UDPConn, tunFile *os.File) {
packet := make([]byte, 1024*64)
size := 0
var err error
for {
if size, err = tunFile.Read(packet); err != nil {
return
}
b := packet[:size]
srcIP := GetSrcIP(b)
dstIP := GetDstIP(b)
fmt.Printf("tunToUDP--->Msg Protocol type: %v(1=ICMP, 6=TCP, 17=UDP)\tsrcIP:%v--->dstIP:%v", packet[9], srcIP, dstIP)
rAddr, err := net.ResolveUDPAddr("udp", remoteAddr)
if err != nil {
log.Fatalln("failed to get udp socket:", err)
return
}
if size, err = udpConn.WriteTo(packet[:size], rAddr); err != nil {
fmt.Println(err.Error())
return
}
fmt.Printf("tunToUDP--->Write Msg To UDP Conn OK! size:%d\n", size)
}
}
func udpToTun(udpConn *net.UDPConn, tunFile *os.File) {
var packet = make([]byte, 1024*64)
var size int
var err error
var addr net.Addr
for {
if size, addr, err = udpConn.ReadFrom(packet); err != nil {
continue
}
size, err = tunFile.Write(packet[:size])
if err != nil {
continue
}
fmt.Printf("udpToTun--->Write Msg To /dev/net/tun OK! size:%d\tsrcIP:%v\n", size, addr)
}
}
func createTun() (*os.File, error) {
err := addTun()
if err != nil {
return nil, err
}
err = configTun()
if err != nil {
return nil, err
}
tunFile, _, err := OpenTun(tunName)
if err != nil {
return nil, err
}
return tunFile, nil
}
func addTun() error {
la := netlink.LinkAttrs{
Name: tunName,
Index: 8,
MTU: 1500,
}
tun := netlink.Tuntap{
LinkAttrs: la,
Mode: netlink.TUNTAP_MODE_TUN,
}
l, err := netlink.LinkByName(tunName)
if err == nil {
netlink.LinkSetDown(l)
netlink.LinkDel(l)
}
err = netlink.LinkAdd(&tun)
if err != nil {
return err
}
return nil
}
func configTun() error {
l, err := netlink.LinkByName(tunName)
if err != nil {
return err
}
ip, err := netlink.ParseIPNet(fmt.Sprintf("%s/%d", tunIP, 24))
if err != nil {
return err
}
addr := &netlink.Addr{IPNet: ip, Label: ""}
if err = netlink.AddrAdd(l, addr); err != nil {
return err
}
err = netlink.LinkSetUp(l)
if err != nil {
return err
}
return nil
}
func GetSrcIP(packet []byte) string {
key := ""
if IsIPv4(packet) && len(packet) >= 20 {
key = GetIPv4Src(packet).To4().String()
}
return key
}
func IsIPv4(packet []byte) bool {
flag := packet[0] >> 4
return flag == 4
}
func GetDstIP(packet []byte) string {
key := ""
if IsIPv4(packet) && len(packet) >= 20 {
key = GetIPv4Dst(packet).To4().String()
}
return key
}
func GetIPv4Src(packet []byte) net.IP {
return net.IPv4(packet[12], packet[13], packet[14], packet[15])
}
func GetIPv4Dst(packet []byte) net.IP {
return net.IPv4(packet[16], packet[17], packet[18], packet[19])
}
2.2、主流程说明 |
2.3、tunToUDP函数原理说明 |
2.4、udpToTun函数原理说明 |
tun-vpn-server的原理跟tun-vpn-client一样,就不再介绍了。
2.5、本地编译,上传到测试服务器10.211.55.122 |
Makefile文件内容
build:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o tun-vpn-client main.go
scp:
scp tun-vpn-client root@10.211.55.122:/root
all:
make build && make scp
执行
make all
即可
3、tun-vpn-server原理说明 |
3.1、代码如下 |
package main
import (
"bytes"
"fmt"
"github.com/vishvananda/netlink"
"log"
"net"
"os"
"syscall"
"time"
"unsafe"
)
const (
tunName = "flannel99"
tunDevice = "/dev/net/tun"
ifnameSize = 16
localAddr = "10.211.55.123:8285"
remoteAddr = "10.211.55.122:8285"
tunIP = "10.244.3.3"
)
type ifreqFlags struct {
IfrnName [ifnameSize]byte
IfruFlags uint16
}
func ioctl(fd int, request, argp uintptr) error {
_, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), request, argp)
if errno != 0 {
fmt.Errorf("ioctl failed with '%s'\n", errno)
return fmt.Errorf("ioctl failed with '%s'", errno)
}
return nil
}
func fromZeroTerm(s []byte) string {
return string(bytes.TrimRight(s, "\000"))
}
func OpenTun(name string) (*os.File, string, error) {
tun, err := os.OpenFile(tunDevice, os.O_RDWR|syscall.O_NONBLOCK, 0)
if err != nil {
fmt.Printf("OpenTun Failed! err:%v", err.Error())
return nil, "", err
}
var ifr ifreqFlags
copy(ifr.IfrnName[:len(ifr.IfrnName)-1], []byte(name+"\000"))
ifr.IfruFlags = syscall.IFF_TUN | syscall.IFF_NO_PI
err = ioctl(int(tun.Fd()), syscall.TUNSETIFF, uintptr(unsafe.Pointer(&ifr)))
if err != nil {
fmt.Printf("OpenTun Failed! err:%v\n", err.Error())
return nil, "", err
}
ifName := fromZeroTerm(ifr.IfrnName[:ifnameSize])
return tun, ifName, nil
}
func main() {
fmt.Printf("======>Now----Server----Tun---VPN---UDP<======\n")
tunFile, err := createTun()
if err != nil {
fmt.Printf("ICMP Listen Packet Failed! err:%v\n", err.Error())
return
}
defer tunFile.Close()
udpConn, err := createUDP()
if err != nil {
fmt.Printf("UDP conn Failed! err:%v\n", err.Error())
return
}
defer udpConn.Close()
go tunToUDP(udpConn, tunFile)
go udpToTun(udpConn, tunFile)
time.Sleep(time.Hour)
}
func createUDP() (*net.UDPConn, error) {
localAddr, err := net.ResolveUDPAddr("udp", localAddr)
if err != nil {
log.Fatalln("failed to get udp socket:", err)
return nil, err
}
conn, err := net.ListenUDP("udp", localAddr)
if err != nil {
log.Fatalln("failed to listen on udp socket:", err)
return nil, err
}
return conn, nil
}
func tunToUDP(udpConn *net.UDPConn, tunFile *os.File) {
packet := make([]byte, 1024*64)
size := 0
var err error
for {
if size, err = tunFile.Read(packet); err != nil {
return
}
rAddr, err := net.ResolveUDPAddr("udp", remoteAddr)
if err != nil {
log.Fatalln("failed to get udp socket:", err)
return
}
if size, err = udpConn.WriteTo(packet[:size], rAddr); err != nil {
fmt.Println(err.Error())
return
}
fmt.Printf("tunToUDP--->Write Msg To UDP Conn OK! size:%d\n", size)
}
}
func udpToTun(udpConn *net.UDPConn, tunFile *os.File) {
var packet = make([]byte, 1024*64)
var size int
var err error
var addr net.Addr
for {
if size, addr, err = udpConn.ReadFrom(packet); err != nil {
continue
}
size, err = tunFile.Write(packet[:size])
if err != nil {
continue
}
fmt.Printf("udpToTun--->Write Msg To /dev/net/tun OK! size:%d\tsrcIP:%v\n", size, addr)
}
}
func createTun() (*os.File, error) {
err := addTun()
if err != nil {
return nil, err
}
err = configTun()
if err != nil {
return nil, err
}
tunFile, _, err := OpenTun(tunName)
if err != nil {
return nil, err
}
return tunFile, nil
}
func addTun() error {
la := netlink.LinkAttrs{
Name: tunName,
Index: 8,
MTU: 1500,
}
tun := netlink.Tuntap{
LinkAttrs: la,
Mode: netlink.TUNTAP_MODE_TUN,
}
l, err := netlink.LinkByName(tunName)
if err == nil {
netlink.LinkSetDown(l)
netlink.LinkDel(l)
}
err = netlink.LinkAdd(&tun)
if err != nil {
return err
}
return nil
}
func configTun() error {
l, err := netlink.LinkByName(tunName)
if err != nil {
return err
}
ip, err := netlink.ParseIPNet(fmt.Sprintf("%s/%d", tunIP, 24))
if err != nil {
return err
}
addr := &netlink.Addr{IPNet: ip, Label: ""}
if err = netlink.AddrAdd(l, addr); err != nil {
return err
}
err = netlink.LinkSetUp(l)
if err != nil {
return err
}
return nil
}
3.2、本地编译,上传到测试服务器10.211.55.123上 |
Makefile内容
build:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o tun-vpn-server main.go
scp:
scp tun-vpn-server root@10.211.55.123:/root
all:
make build && make scp
执行
make all
即可
4、web服务说明 |
简单提供了一个web服务
4.1、代码如下 |
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type Stu struct {
Age int
Msg string
}
func sayHello(w http.ResponseWriter, r *http.Request) {
stu := Stu{Age: 12, Msg: "hello world\nTun VPN\n"}
stuJson, e := json.Marshal(&stu)
if e != nil {
panic(e)
}
w.Write(stuJson)
fmt.Printf("Reply MSG:%v\n", string(stuJson))
}
func main() {
http.HandleFunc("/", sayHello)
fmt.Printf("App URL: http://10.244.3.3:9090\n")
err := http.ListenAndServe("10.244.3.3:9090", nil)
if err != nil {
fmt.Printf("http server failed, err:%v\n", err)
return
}
}
4.2、本地编译,上传到测试服务器10.211.55.123上 |
Makefile内容
build:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app main.go
scp:
scp app root@10.211.55.123:/root
all:
make build && make scp
执行
make all
即可
5、对端的路由设置 |
5.1、是否需要设置路由呢? |
如果宿主机-1和宿主机-2上的flannel99都是属于同一个网段,比方:
- 宿主机-1上的flannel99的IP是10.244.2.2
- 宿主机-2上的flannel99的IP是10.244.2.3
的话,是不需要单独设置路由的
而当前我们的测试用例中,两个宿主机上的flannel99的网络是不同的,
因此,需要设置路由。
5.2、在宿主机-1上(10.211.55.122) |
查看当前的路由表、并且设置数据包访问宿主机-2上flannel99网络的路由
route -n
route add -net 10.244.3.0/24 dev flannel99
route -n
5.3、在宿主机-2上(10.211.55.123) |
查看当前的路由表、并且设置数据包访问宿主机-1上flannel99网络的路由
route -n
route add -net 10.244.2.0/24 dev flannel99
route -n
可能会存在一种现象,新增的路由规则,经常被删除。
原因不在深挖了。手工重新添加即可。
生产中,可以对路由规则进行定时检测的。
6、测试 |
6.1、登录到10.211.55.122服务器上,启动tun-vpn-client服务 |
./tun-vpn-client
6.2、登录到10.211.55.123服务器上 |
6.2.1、启动tun-vpn-server服务 |
./tun-vpn-server
6.2.2、启动web服务 |
./app
6.3、重新登录10.211.55.122服务器上 |
6.3.1、curl命令测试 6.3.1.1、curl命令测试 |
curl 10.244.3.3:9090
tun-vpn-client与tun-vpn-server服务打印日志,
在启动服务的时候,已经截图了,
不再重复截图了。
6.3.1.2、抓包,wireshark分析 |
6.3.1.2.1、针对flannel99网卡抓包分析 |
tcpdump -nn -i flannel99 -w curl.pcap
抓取命令
tcp的三次握手
(注意下:图片中 “flannel99自动给tcp报文添加的IP报文头”,这种说法是有待研究的。仅供参考)
curl请求
web服务app的回复
四次挥手
6.3.1.2.2、针对eth0网卡抓包分析 |
tcpdump -nn udp -i eth0 -w curl.pcap
6.3.2、ping命令测试,测试是否可以ping通10.211.55.123节点上的flannel99网卡 |
ping 10.244.3.3
ping 10.244.3.3 -I flannel99
sysctl net.ipv4.conf.all.rp_filter
ping命令就不在注抓取数据包了。
大家可以自行抓取。
7、总结 |
- 本篇文章我们利用tun类型的虚拟网络设备实现了点对点的VPN组网,
- 并进行了curl命令,ping命令的测试;
- 支持TCP,ICMP协议的数据包传输。
- 观察原理图的话,你会发现
- 数据包在用户空间和内核空间之间来回的切换,这种方式性能肯定存在一定的问题。
- 未来会有其他网络形式进行代替。
如果本篇文章的内容,你已经了解了的话,
那么,恭喜你,flannel中的UDP模式,你已经掌握核心了。
点击 下面 返回 专栏目录 |