Go中原始套接字的深度实践
1. 介绍
原始套接字(raw socket)是一种网络套接字,允许直接发送/接收更底层的数据包而不需要任何传输层协议格式。平常我们使用较多的套接字(socket)都是基于传输层,发送/接收的数据包都是不带TCP/UDP等协议头部的。
当使用套接字发送数据时,传输层在数据包前填充上面格式的协议头部数据,然后整个发送到网络层,接收时去掉协议头部,把应用数据抛给上层。如果想自己封装头部或定义协议的话,就需要使用原始套接字,直接向网络层发送数据包。
为了便于后面理解,这里统一称应用数据为 payload,协议头部为 header,套接字为socket。由于平常使用的socket是建立在传输层之上,并且不可以自定义传输层协议头部的socket,约定称之为应用层socket,它不需要关心TCP/UDP协议头部如何封装。这样区分的目的是为了理解raw socket在不同层所能做的事情。
2. 传输层socket
根据上面的约定,我们把基于网络层IP协议上并且不可以自定义IP协议头部的socket,称为传输层socket,它需要关心传输层协议头部如何封装,不需要关心IP协议头部如何封装。它“理论上来说”是可以拦截任何传输层的协议,也可以任意自定义传输层协议,比如自定义个协议叫YCP,那么它就和TCP/UDP/ICMP等协议同级。
2.1 ICMP
ICMP协议是一个“错误侦测与回报机制”,其目的是检测网路的连线状况﹐确保连线的准确性﹐就是我们经常使用的Ping命令。我们在Go中实践下,来拦截Ping命令产生的数据流量:
func main() {
netaddr, _ := net.ResolveIPAddr("ip4", "172.17.0.3")
conn, _ := net.ListenIP("ip4:icmp", netaddr)
for {
buf := make([]byte, 1024)
n, addr, _ := conn.ReadFrom(buf)
msg,_:=icmp.ParseMessage(1,buf[0:n])
fmt.Println(n, addr, msg.Type,msg.Code,msg.Checksum)
}
}
代码中ListenIP是Go提供的来监听IP网络层流量的API,第一个参数是网络层协议,其实只有IP协议,它可以分为ipV4或ipV6。冒号后面的是子协议,表示监听的是网络层中icmp协议的流量,这个子协议在IP header中字段Protocol(下面的8位协议)体现出,IP header一般也是20字节:

这个子协议有200多种,在Go中目前只支持常见几个:icmp,igmp,tcp,udp,ipv6-icmp。
运行程序,在另外个机器里ping 172.17.0.3:
root@43b16fbeea3d:~# ping 172.17.0.3
PING 172.17.0.3 (172.17.0.3) 56(84) bytes of data.
64 bytes from 172.17.0.3: icmp_seq=1 ttl=64 time=0.078 ms
64 bytes from 172.17.0.3: icmp_seq=2 ttl=64 time=0.085 ms
64 bytes from 172.17.0.3: icmp_seq=3 ttl=64 time=0.389 ms
本机监听到Ping如下:
root@2de84a6c1fed:/go/src/github.com/mushroomsir/blog/examples/001/transport# go run main.go
64 172.17.0.2 echo 0 15729
64 172.17.0.2 echo 0 47698
64 172.17.0.2 echo 0 56243
64 172.17.0.2 echo 0 2072
64 172.17.0.2 echo 0 62072
2.2 TCP
监控TCP只需要把ICMP换成TCP即可,表示监听的是网络层中TCP协议的流量:
func main() {
netaddr, _ := net.ResolveIPAddr("ip4", "172.17.0.3")
conn, _ := net.ListenIP("ip4:tcp", netaddr)
for {
buf := make([]byte, 1480)
n, addr, _ := conn.ReadFrom(buf)
tcpheader:=NewTCPHeader(buf[0:n])
fmt.Println(n,addr,tcpheader)
}
}
因为监控的是TCP流量,所以数据都会有TCP的header。NewTCPHeader是一个分析TCP header的struct,在示例代码中有。当运行这段程序时,是可以监控到所有到达本机172.17.0.3这块网卡的数据的。在另外台机器运行:
root@43b16fbeea3d:~# curl 172.17.0.3:80
curl: (7) Failed to connect to 172.17.0.3 port 80: Connection refused
或者
root@43b16fbeea3d:~# curl 172.17.0.3:8000
curl: (7) Failed to connect to 172.17.0.3 port 8000: Connection refused
本机监听到如下:
root@2de84a6c1fed:/go/src/github.com/mushroomsir/blog/examples/001/transporttcp# go run main.go tcp.go
40 172.17.0.2 Source=54482 Destination=80 SeqNum=3189186693 AckNum=0 DataOffset=10 Reserved=0 ECN=0 Ctrl=2 Window=29200 Checksum=22614 Urgent=[] Options=%!v(MISSING)
40 172.17.0.2 Source=56928 Destination=8000 SeqNum=2042858949 AckNum=0 DataOffset=10 Reserved=0 ECN=0 Ctrl=2 Window=29200 Checksum=22614 Urgent=[] Options=%!v(MISSING)
可以看到本机已经成功拦截到来自172.17.0.2的请求。TCP header中Source是源端口,Destination是目标端口,
因为监听的是IPv4协议上的所有TCP流量,所以不管目标端口是80或8000,都能接收到。直接用浏览器访问也是可以的:
40 172.17.0.1 Source=34830 Destination=8020 SeqNum=2212492703 AckNum=0 DataOffset=10 Reserved=0 ECN=0 Ctrl=2 Window=29200 Checksum=22613 Urgent=[] Options=%!v(MISSING)
但结果和curl一样报错,因为本机虽然监听到了,但并没有做任何处理,比如TCP三次握手都没有完成。如果想自己封装个TCP,那就必须按照TCP协议完成三次握手,只处理本端口的流量数据等。下图是TCP header中的各字段:

2.3 传输层协议
让我们来自定义个传输层YCP协议,参考TCP Header,定义YCP Header,然后发送xxxpayload。
客户端代码如下:
func main() {
local := "127.0.0.1"
remote := "172.17.0.3"
conn, _ := net.Dial("ip4:tcp", remote)
ycpHeader:= util.TCPHeader{
Source: 17663,
Destination: 8020,
SeqNum: 2,
AckNum: 0,
DataOffset: 5,
Reserved: 0,
ECN: 0,
Ctrl: 2,
Window: 0xaaaa,
Checksum: 0,
Urgent: 99,
}
data := ycpHeader.Marshal()
ycpHeader.Checksum = util.Csum(data, to4byte(local), to4byte(remote))
data = ycpHeader.Marshal()
data=append(data,[]byte("xxx")...)
conn.Write(data)
}
服务端代码如下:
func main() {
netaddr, _ := net.ResolveIPAddr("ip4", "172.17.0.3")
conn, _ := net.ListenIP("ip4:tcp", netaddr)
for {
buf := make([]byte, 1480)
n, addr, _ := conn.ReadFrom(buf)
ycpheader := util.NewTCPHeader(buf[0:20])
fmt.Println(n, addr, ycpheader, string(buf[20:23]))
}
}
启动服务端,然后运行客户端,服务端输出:
root@2de84a6c1fed:/go/src/github.com/mushroomsir/blog/examples/001/transportcustom/server#
go run main.go
23 172.17.0.2 Source=17663 Destination=8020 SeqNum=2 AckNum=0 DataOffset=5 Reserved=0 ECN=0 Ctrl=2 Window=43690 Checksum=30058 Urgent=99 xxx
可以看到Urgent是99,发送时故意定义的大值,然后playload是xxx。
3. 网络层socket
3.1 使用Go库
根据上面的约定,我们把基于网络层IP协议上并且可以自定义IP协议头部的socket,称为网络层socket,它需要关心IP协议头部如何封装,不需要关心以太网帧的头部和尾部如何封装。来看下面例子:
func main() {
netaddr, _ := net.ResolveIPAddr("ip4", "172.17.0.3")
conn, _ := net.ListenIP("ip4:tcp", netaddr)
ipconn,_:=ipv4.NewRawConn(conn)
for {
buf := make([]byte, 1480)
hdr, payload, controlMessage, _ := ipconn.ReadFrom(buf)
fmt.Println("ipheader:",hdr,controlMessage)
tcpheader:=NewTCPHeader(payload)
fmt.Println("tcpheader:",tcpheader)
}
}
相比传输层socket而言,需要把传输层拿到的socket转成网络层ip的socket,也就是代码中的NewRawConn,这个函数主要是给这个raw socket启用IP_HDRINCL选项。如果启用的话就会在payload前面提供ip header数据。 然后解析IP header信息:
其IP的payload=TCP Header+ TCP payload
所以还需要解析TCP header。然后在另外台机器curl验证下:
root@43b16fbeea3d:~# curl 172.17.0.3:8000
curl: (7) Failed to connect to 172.17.0.3 port 8000: Connection refused
本机监听输出:
root@2de84a6c1fed:/go/src/github.com/mushroomsir/blog/examples/001/network# go run main.go tcp.go
ipheader: ver=4 hdrlen=20 tos=0x0 totallen=60 id=0xd7d1 flags=0x2 fragoff=0x0 ttl=64 proto=6 cksum=0xac3 src=172.17.0.2 dst=172.17.0.3 <nil>
tcpheader: Source=56968 Destination=8000 SeqNum=1824143864 AckNum=0 DataOffset=10 Reserved=0 ECN=0 Ctrl=2 Window=29200 Checksum=22614 Urgent=[] Options=%!v(MISSING)
^Csignal: interrupt
3.2 系统调用
如果觉得Go库使用起来有限制的话,还可以用system call的方式调用:
func main() {
fd, _ := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_TCP)
f := os.NewFile(uintptr(fd), fmt.Sprintf("fd %d", fd))
for {
buf := make([]byte, 1500)
f.Read(buf)
ip4header, _ := ipv4.ParseHeader(buf[:20])
fmt.Println("ipheader:", ip4header)
tcpheader := util.NewTCPHeader(buf[20:40])
fmt.Println("tcpheader:", tcpheader)
}
}
Go库本身也是利用syscall.Socket,来提供raw socket的能力,并封装了一层更易于使用的API。其各参数代表:
第一个参数:
- syscall.AF_INET,表示服务器之间的网络通信
- syscall.AF_UNIX表示同一台机器上的进程通信
- syscall.AF_INET6表示以IPv6的方式进行服务器之间的网络通信
- 其他
第二个参数
- syscall.SOCK_RAW,表示使用原始套接字,可以构建传输层的协议头部,启用IP_HDRINCL的话,IP层的协议头部也可以构造,就是上面区分的传输层socket和网络层socket。
- syscall.SOCK_STREAM, 基于TCP的socket通信,应用层socket。
- syscall.SOCK_DGRAM, 基于UDP的socket通信,应用层socket。
- 其他
第三个参数
即ICMP章节提到的子协议号,操作系统内核发现接收到的IP header中的协议号与创建时填的协议号一样时,就交给上层处理。
- IPPROTO_TCP 接收TCP协议的数据
- IPPROTO_IP 接收任何的IP数据包
- IPPROTO_UDP 接收UDP协议的数据
- IPPROTO_ICMP 接收ICMP协议的数据
- IPPROTO_RAW 只能用来发送IP数据包,不能接收数据。
- 其他
在另外台机器curl:
root@43b16fbeea3d:~# curl 172.17.0.3:8999
curl: (7) Failed to connect to 172.17.0.3 port 8999: Connection refused
本机监听输出:
root@2de84a6c1fed:/go/src/github.com/mushroomsir/blog/examples/001/network/systemcall# go run main.go
ipheader: ver=4 hdrlen=20 tos=0x0 totallen=60 id=0x4cb6 flags=0x2 fragoff=0x0 ttl=64 proto=6 cksum=0x95de src=172.17.0.2 dst=172.17.0.3
tcpheader: Source=49484 Destination=8999 SeqNum=3080655072 AckNum=0 DataOffset=10 Reserved=0 ECN=0 Ctrl=2 Window=29200 Checksum=22614 Urgent=[] Options=%!v(MISSING)
3.3 网络层协议
让我们来自定义个网络层YIP协议,参考IP Header,定义YIP Header,然后发送个空payload。
客户端代码如下:
func main() {
fd, _ := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
addr := syscall.SockaddrInet4{
Port: 0,
Addr: [4]byte{172, 17, 0, 3},
}
yipHeader := ipv4.Header{
Version: 4,
Len: 20,
TotalLen: 20, // 20 bytes for IP
TTL: 64,
Protocol: 6, // TCP
Dst: net.IPv4(172, 17, 0, 3),
Src: net.IPv4(172, 17, 0, 99),
}
payload, _ := yipHeader.Marshal()
syscall.Sendto(fd, payload, 0, &addr)
}
服务端代码如下:
func main() {
netaddr, _ := net.ResolveIPAddr("ip4", "172.17.0.3")
conn, _ := net.ListenIP("ip4:tcp", netaddr)
ipconn, _ := ipv4.NewRawConn(conn)
for {
buf := make([]byte, 1500)
hdr, payload, controlMessage, _ := ipconn.ReadFrom(buf)
fmt.Println("ipheader:", hdr,payload, controlMessage)
}
}
启动服务端监听,运行客户端程序,服务端输出:
root@2de84a6c1fed:/go/src/github.com/mushroomsir/blog/examples/001/networkcustom/server# go run main.go
ipheader: ver=4 hdrlen=20 tos=0x0 totallen=20 id=0x1363 flags=0x0 fragoff=0x0 ttl=64 proto=6 cksum=0xef9 src=172.17.0.99 dst=172.17.0.3 [] <nil>
在客户端示例中,头部填写了源IP是172.17.0.99,但实际当中笔者电脑并没有这个IP。假设同一个局域网中,利用raw socket就可以伪装成别人的IP去发消息。
4. 总结
基于Raw socket可以把UDP的流量伪装成TCP,这样就不会被ISP封杀。也可以伪装IP去DDOS别人,但基于安全的考虑:Windows并不允许通过Raw socket去发送TCP数据,UDP中的源IP也必须在本地网络接口中能找到才行,监听TCP的流量也是不允许的。
文中例子代码都在github-examples,有兴趣的同学可以自己尝试下。按照约定的划分,还有一层链路层socket,这个后面再写。
4.1 参考
http://man7.org/linux/man-pages/man7/raw.7.html
http://man7.org/linux/man-pages/man7/ip.7.html
https://github.com/golang/net
https://www.darkcoding.net/software/raw-sockets-in-go-link-layer/
Go中原始套接字的深度实践的更多相关文章
- Python中利用原始套接字进行网络编程的示例
Python中利用原始套接字进行网络编程的示例 在实验中需要自己构造单独的HTTP数据报文,而使用SOCK_STREAM进行发送数据包,需要进行完整的TCP交互. 因此想使用原始套接字进行编程,直接构 ...
- Linux Socket 原始套接字编程
对于linux网络编程来说,可以简单的分为标准套接字编程和原始套接字编程,标准套接字主要就是应用层数据的传输,原始套接字则是可以获得不止是应用层的其他层不同协议的数据.与标准套接字相区别的主要是要开发 ...
- 005.TCP--拼接TCP头部IP头部,实现TCP三次握手的第一步(Linux,原始套接字)
一.目的: 自己拼接IP头,TCP头,计算效验和,将生成的报文用原始套接字发送出去. 若使用tcpdump能监听有对方服务器的包回应,则证明TCP报文是正确的! 二.数据结构: TCP首部结构图: s ...
- 004.UDP--拼接UDP数据包,构造ip头和udp头通信(使用原始套接字)
一.大致流程: 建立一个client端,一个server端,自己构建IP头和UDP头,写入数据(hello,world!)后通过原始套接字(SOCK_RAW)将包发出去. server端收到数据后,打 ...
- 002.ICMP--拼接ICMP包,实现简单Ping程序(原始套接字)
一.大致流程: 将ICMP头和时间数据设置好后,通过创建好的原始套接字socket发出去.目的主机计算效验和后会将数据原样返回,用当前时间和返回的数据结算时间差,计算出rtt. 二.数据结构: ICM ...
- 原始套接字SOCK_RAW
原始套接字SOCK_RAW 实际上,我们常用的网络编程都是在应用层的报文的收发操作,也就是大多数程序员接触到的流式套接字(SOCK_STREAM)和数据包式套接字(SOCK_DGRAM).而这些数据包 ...
- 原始套接字(SOCK_RAW)
本文转载:http://www.cnblogs.com/duzouzhe/archive/2009/06/19/1506699.html,在此感谢 原始套接字(SOCK_RAW). 应用原始套接字,我 ...
- Python原始套接字编程
在实验中需要自己构造单独的HTTP数据报文,而使用SOCK_STREAM进行发送数据包,需要进行完整的TCP交互. 因此想使用原始套接字进行编程,直接构造数据包,并在IP层进行发送,即采用SOCK_R ...
- Linux系统编程(37)—— socket编程之原始套接字
原始套接字的特点 原始套接字(SOCK_RAW)可以用来自行组装IP数据包,然后将数据包发送到其他终端.也就是说原始套接字是基于IP数据包的编程(SOCK_PACKET是基于数据链路层的编程).另外, ...
随机推荐
- Vue、AngularJS 双向数据绑定解剖
数据与视图的绑定与同步,最终体现在对数据的读写处理过程中,也就是 Object.defineProperty() 定义的数据 set.get 函数中.Vue 中对于的函数为 defineReactiv ...
- 微软黑科技强力注入,.NET C#全面支持人工智能
微软黑科技强力注入,.NET C#全面支持人工智能,AI编程领域开始C#.Py--百花齐放 就像武侠小说中,一个普通人突然得到绝世高手的几十年内力注入,招式还没学,一身内力有点方 Introducin ...
- Python2和Python3比较分析
一直有看到网上有讨论Python2和Python3的比较,最近公司也在考虑是否在spark-python大数据开发环境中升级到python3.通过本篇博文记录Python2.7.13和Pthon3.5 ...
- C#本质论笔记
第一章 C#概述 1.1 Helo,World 学习一种新语言最好的办法就是动手写程序. C#编译器创建的.exe程序是一个程序集(Assembly),我们也可以创建能由另一个较大的程序 ...
- Java中static关键字和final关键字
static: 1. 修饰变量,方法 表示静态方法,静态变量. 2. static修饰代码块 static{ } 此种形式为静态代码块,用于初始化同时被final static修饰的变量.(当然,更常 ...
- Linnux入门之简介
一.Linux简介 Minix(教授实验) -> Linux(大三学生Linus)企鹅作为吉祥物 linux主要分为内核版本和发行版本 linux 内核版本 :官网下载:https://www. ...
- Servlet知识点总结
一, ServletAPI中有4个Java包: 1.javax.servlet:其中包含定义Servlet和Servlet容器之间契约的类和接口 2.javax.servlet.http:其中包含定义 ...
- Python学习 Part6:错误和异常
Python学习 Part6:错误和异常 两种不同类型的错误:语法错误和异常 1. 语法错误 语法错误,也被称作解析错误: >>> while True print('Hello w ...
- vue 单页应用拆分为多页应用
npm install glob --save-dev build.js---'./src/pages' 替换为自己实际的项目文件路径 utils.js--- webpack.base.conf.js ...
- php-msf 源码解读【转】
php-msf: https://github.com/pinguo/php-msf 百度脑图 - php-msf 源码解读: http://naotu.baidu.com/file/cc7b5a49 ...