原始套接字(raw socket)是一種網絡套接字,容許直接發送/接收更底層的數據包而不須要任何傳輸層協議格式。日常咱們使用較多的套接字(socket)都是基於傳輸層,發送/接收的數據包都是不帶TCP/UDP等協議頭部的。
當使用套接字發送數據時,傳輸層在數據包前填充上面格式的協議頭部數據,而後整個發送到網絡層,接收時去掉協議頭部,把應用數據拋給上層。若是想本身封裝頭部或定義協議的話,就須要使用原始套接字,直接向網絡層發送數據包。
爲了便於後面理解,這裏統一稱應用數據爲 payload,協議頭部爲 header,套接字爲socket。因爲日常使用的socket是創建在傳輸層之上,而且不能夠自定義傳輸層協議頭部的socket,約定稱之爲應用層socket,它不須要關心TCP/UDP協議頭部如何封裝。這樣區分的目的是爲了理解raw socket在不一樣層所能作的事情。html
根據上面的約定,咱們把基於網絡層IP協議上而且不能夠自定義IP協議頭部的socket,稱爲傳輸層socket,它須要關心傳輸層協議頭部如何封裝,不須要關心IP協議頭部如何封裝。它「理論上來講」是能夠攔截任何傳輸層的協議,也能夠任意自定義傳輸層協議,好比自定義個協議叫YCP,那麼它就和TCP/UDP/ICMP等協議同級。linux
ICMP協議是一個「錯誤偵測與回報機制」,其目的是檢測網路的連線情況﹐確保連線的準確性﹐就是咱們常用的Ping命令。咱們在Go中實踐下,來攔截Ping命令產生的數據流量:git
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字節:github
這個子協議有200多種,在Go中目前只支持常見幾個:icmp,igmp,tcp,udp,ipv6-icmp。
運行程序,在另外個機器裏ping 172.17.0.3:golang
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
監控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
本機監聽到以下:app
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中的各字段:
讓咱們來自定義個傳輸層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
。
根據上面的約定,咱們把基於網絡層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
若是以爲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。其各參數表明:
第一個參數:
第二個參數
第三個參數
即ICMP章節提到的子協議號,操做系統內核發現接收到的IP header中的協議號與建立時填的協議號同樣時,就交給上層處理。
在另外臺機器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)
讓咱們來自定義個網絡層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去發消息。
基於Raw socket能夠把UDP的流量假裝成TCP,這樣就不會被ISP封殺。也能夠假裝IP去DDOS別人,但基於安全的考慮:Windows並不容許經過Raw socket去發送TCP數據,UDP中的源IP也必須在本地網絡接口中能找到才行,監聽TCP的流量也是不容許的。
文中例子代碼都在github-examples,有興趣的同窗能夠本身嘗試下。按照約定的劃分,還有一層鏈路層socket,這個後面再寫。
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/