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,然后發送xxx
payload。
客戶端代碼如下:
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/