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字節:

ip-header-2.jpg

這個子協議有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中的各字段:

tcp-header.jpg

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。其各參數代表:
第一個參數:

  1. syscall.AF_INET,表示服務器之間的網絡通信
  2. syscall.AF_UNIX表示同一台機器上的進程通信
  3. syscall.AF_INET6表示以IPv6的方式進行服務器之間的網絡通信
  4. 其他

第二個參數

  1. syscall.SOCK_RAW,表示使用原始套接字,可以構建傳輸層的協議頭部,啟用IP_HDRINCL的話,IP層的協議頭部也可以構造,就是上面區分的傳輸層socket和網絡層socket。
  2. syscall.SOCK_STREAM, 基於TCP的socket通信,應用層socket。
  3. syscall.SOCK_DGRAM, 基於UDP的socket通信,應用層socket。
  4. 其他

第三個參數
即ICMP章節提到的子協議號,操作系統內核發現接收到的IP header中的協議號與創建時填的協議號一樣時,就交給上層處理。

  1. IPPROTO_TCP 接收TCP協議的數據
  2. IPPROTO_IP 接收任何的IP數據包
  3. IPPROTO_UDP 接收UDP協議的數據
  4. IPPROTO_ICMP 接收ICMP協議的數據
  5. IPPROTO_RAW 只能用來發送IP數據包,不能接收數據。
  6. 其他

在另外台機器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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM