Go中原始套接字的深度實踐

1. 介紹

原始套接字(raw socket)是一種網絡套接字,容許直接發送/接收更底層的數據包而不須要任何傳輸層協議格式。日常咱們使用較多的套接字(socket)都是基於傳輸層,發送/接收的數據包都是不帶TCP/UDP等協議頭部的。
當使用套接字發送數據時,傳輸層在數據包前填充上面格式的協議頭部數據,而後整個發送到網絡層,接收時去掉協議頭部,把應用數據拋給上層。若是想本身封裝頭部或定義協議的話,就須要使用原始套接字,直接向網絡層發送數據包。
爲了便於後面理解,這裏統一稱應用數據爲 payload,協議頭部爲 header,套接字爲socket。因爲日常使用的socket是創建在傳輸層之上,而且不能夠自定義傳輸層協議頭部的socket,約定稱之爲應用層socket,它不須要關心TCP/UDP協議頭部如何封裝。這樣區分的目的是爲了理解raw socket在不一樣層所能作的事情。html

2. 傳輸層socket

根據上面的約定,咱們把基於網絡層IP協議上而且不能夠自定義IP協議頭部的socket,稱爲傳輸層socket,它須要關心傳輸層協議頭部如何封裝,不須要關心IP協議頭部如何封裝。它「理論上來講」是能夠攔截任何傳輸層的協議,也能夠任意自定義傳輸層協議,好比自定義個協議叫YCP,那麼它就和TCP/UDP/ICMP等協議同級。linux

2.1 ICMP

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

ip-header-2.jpg

這個子協議有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

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

本機監聽到以下: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中的各字段:

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/

相關文章
相關標籤/搜索