Several ways to send IP packets using Go

Original link: https://colobu.com/2023/05/13/send-IP-packets-in-Go/

We use net package in the Go standard library, it is easy to send UDP and TCP packets, and develop application layer programs based on them, such as HTTP, RPC and other frameworks and programs, and even we can use the official extension package golang.oef/x/net/iucmp , dedicated to sending and receiving icmp packets, however, sometimes we want to communicate at a lower level, at this time we need to use some additional libraries, or do some additional settings, the current related The introduction of IP layer packet sending and receiving technology is not well organized and introduced. This article tries to introduce several ways of sending and receiving IP packets.

Still, we introduce IPv4-related technologies, and IPv6 will be introduced in a separate chapter.

When doing Go network programming, I personally have a little suggestion for the choice of technology and commonly used scenarios: if the standard library can provide relevant functions, then use the standard library; otherwise, look at the official extension library golang.org/x/net Whether golang.org/x/net can meet the requirements; if not, then consider using syscall.Socket and gopackete ; if not, then check whether there are any third-party libraries that have implemented related functions. Of course, sometimes the last two considerations may be interchanged.

Why do we sometimes need to send and receive IP packets? Because we sometimes want to perform detailed settings or checks on the IPv4 header. As defined in the following IPv4 header:

Sometimes we want to set TOS, Identification, TTL, Options, we must be able to assemble IPv4 packet by ourselves and send it out; the same is true for reading.

Use the standard library

Explore with net.ListenPacket/net.ListenPacket

The standard library provides a method of reading and writing IP packets, which can achieve half of the reading and writing capabilities. It is implemented through func ListenPacket(network, address string) (PacketConn, error) function, where the network can be udp , udp4 , udp6 , unixgram , or ip:1 , ip:icmp such as ip plus protocol number or protocol name. The definition of protocol is in the http://www.iana.org/assignments/protocol-numbers document (you can also read it in /etc/protocols of the Linux host, but it may not be the latest), such as the ICMP protocol The number is 1, the protocol number of TCP is 6, the protocol number of UDP is 17, and the protocol numbers 253 and 254 are used for testing and so on.

If the network is udp , udp4 , udp6 , the bottom layer of the returned PacketConn is *net.UDPConn , if the network is prefixed with ip , then the returned PacketConn is *net.IPConn , in this case, you can use the explicit func ListenIP(network string, laddr *IPAddr) (*IPConn, error) .

Here is an example of a client using net.ListenPacket :


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

func main() {
conn, err := net.ListenPacket( “ip4:udp” , “127.0.0.1” ) // local address
if err != nil {
fmt.Println( “DialIP failed: “ , err)
return
}
data, _ := encodeUDPPacket( “127.0.0.1” , “192.168.0.1” , [] byte ( “hello world” )) // generate a UDP packet
if _, err := conn.WriteTo(data, &net.IPAddr{IP: net.ParseIP( “192.168.0.1” )}); err != nil {
panic (err)
}
buf := make ([] byte , 1024 )
n, peer, err := conn. ReadFrom(buf)
if err != nil {
panic (err)
}
fmt.Printf( “received response from %s: %s\n” , peer.String(), buf [8 :n])
}

This example initially generates a PacketConn , which is actually a *net.IPConn , but it should be noted that the conn here sends a UDP layer packet and does not include the IP layer. The following example defines the IP layer, which is only used Calculating checksum is actually useless:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four

func encodeUDPPacket(localIP, dstIP string , payload [] byte ) ([] byte , error) {
ip := &layers.IPv4{
SrcIP: net.ParseIP(localIP),
DstIP: net.ParseIP(dstIP),
Version: 4 ,
Protocol: layers.IPProtocolUDP,
}
udp := &layers.UDP{
SrcPort: layers.UDPPort (0 ),
DstPort: layers.UDPPort (8972 ),
}
udp.SetNetworkLayerForChecksum(ip)
buf := gopacket. NewSerializeBuffer()
opts := gopacket.SerializeOptions{
Compute Checksums: true ,
FixLengths: true ,
}
err := gopacket.SerializeLayers(buf, opts, udp, gopacket.Payload(payload))
return buf.Bytes(), err
}

Similarly, after the server reads the message, it only returns the protocol layer data under the IPv4 header, and the IPv4 header data is stripped:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three

func main() {
conn, err := net.ListenPacket( “ip4:udp” , “192.168.0.1” )
if err != nil {
panic (err)
}
buf := make ([] byte , 1024 )
for {
n, peer, err := conn. ReadFrom(buf)
if err != nil {
panic (err)
}
fmt.Printf( “received request from %s: %s\n” , peer.String(), buf [8 :n])
data, _ := encodeUDPPacket( “192.168.0.1” , “127.0.0.1” , [] byte ( “hello world” ))
_, err = conn.WriteTo(data, &net.IPAddr{IP: net.ParseIP( “127.0.0.1” )})
if err != nil {
panic (err)
}
}
}

Note that the data read by conn.ReadFrom(buf) here includes the UDP header, but not the IP header. The UDP header is 8 bytes, so buf[8:n] is the payload data.

If you look at the source code of the go standard library, you can see that after receiving the IP packet, Go will call stripIPv4Header to strip the IPv4 header:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
25
26
27

func (c *IPConn) readFrom(b [] byte ) ( int , *IPAddr, error) {
var addr *IPAddr
n, sa, err := c.fd.readFrom(b)
switch sa := sa.( type ) {
case *syscall.SockaddrInet4:
addr = &IPAddr{IP: sa.Addr [0 :]}
n = stripIPv4Header(n, b)
case *syscall.SockaddrInet6:
addr = &IPAddr{IP: sa.Addr [0 :], Zone: zoneCache.name( int (sa.ZoneId))}
}
return n, addr, err
}
func stripIPv4Header(n int , b [] byte ) int {
if len (b) < 20 {
return n
}
l := int (b [0 ] &0x0f ) << 2
if 20 > l || l > len (b) {
return n
}
if b [0 ]> >4 != 4 {
return n
}
copy (b, b[l:])
return n – l
}

Use ipv4.RawConn to send and receive IP packets

The easiest way is to use ipv4.NewRawConn(conn) to convert net.PacketConn to *ipv4.RawConn , as shown in the following client code:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four

func main() {
conn, err := net.ListenPacket( “ip4:udp” , “127.0.0.1” )
if err != nil {
fmt.Println( “DialIP failed: “ , err)
return
}
rc, err := ipv4. NewRawConn(conn)
if err != nil {
panic (err)
}
data, _ := encodeUDPPacket( “127.0.0.1” , “192.168.0.1” , [] byte ( “hello world” ))
if _, err := rc.WriteToIP(data, &net.IPAddr{IP: net.ParseIP( “192.168.0.1” )}); err != nil {
panic (err)
}
rbuf := make ([] byte , 1024 )
_, payload, _, err := rc. ReadFrom(rbuf)
if err != nil {
panic (err)
}
fmt.Printf( “received response: %s\n” , payload [8 :])
}

Note that encodeUDPPacket implementation here is different from the implementation in the above example, it contains the data of the ip header:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

func encodeUDPPacket(localIP, dstIP string , payload [] byte ) ([] byte , error) {
ip := &layers.IPv4{
}
udp := &layers.UDP{
}
udp.SetNetworkLayerForChecksum(ip)
buf := gopacket. NewSerializeBuffer()
opts := gopacket.SerializeOptions{
Compute Checksums: true ,
FixLengths: true ,
}
err := gopacket.SerializeLayers(buf, opts, ip, udp, gopacket.Payload(payload)) // Note that ip is included here
return buf.Bytes(), err
}

When reading data, ReadFrom will read ip header, ip payload (UDP packet), control message (UDP has no control message), so we can also read and analyze the returned IP header.

Use SyscallConn to read IP header

Use (*net.IPConn).SyscallConn() of the standard library to send UDP (or other ip protocol) packet data when writing data, but read IPv4 header when reading data.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

func main() {
conn, err := net.ListenPacket( “ip4:udp” , “127.0.0.1” )
if err != nil {
fmt.Println( “DialIP failed: “ , err)
return
}
sc, err := conn.(*net.IPConn).SyscallConn()
if err != nil {
panic (err)
}
var addr syscall.SockaddrInet4
copy (addr.Addr[:], net.ParseIP( “192.168.0.1” ).To4())
addr. Port = 8972
data, _ := encodeUDPPacket( “127.0.0.1” , “192.168.0.1” , [] byte ( “hello world” ))
err = sc.Write( func (fd uintptr ) bool {
// Write UDP packet to Socket
err := syscall. Sendto( int (fd), data, 0 , &addr)
if err != nil {
panic (err)
}
return err == nil
})
if err != nil {
panic (err)
}
var n int
buf := make ([] byte , 1024 )
err = sc. Read( func (fd uintptr ) bool {
var err error
n, err = syscall. Read( int (fd), buf)
if err != nil {
return false
}
return true
})
if err != nil {
panic (err)
}
iph, err := ipv4. ParseHeader(buf[:n])
if err != nil {
panic (err)
}
fmt.Printf( “received response from %s: %s\n” , iph.Src.String(), buf[ipv4.HeaderLen +8 :])
}

Why is there no way to set the IPv4 header when sending, but can read the IPv4 header when reading? This is related to the Socket used at the bottom layer. Note that when our standard library is built for IPConn, we use syscall.AF_INET and syscall.SOCK_RAW, sockets created by protocol. By default, we need to fill in the ip payload data (protocol data) , the kernel protocol stack will automatically generate the IP header, but the ip header will be read and returned when reading, so the behavior of Go is consistent with that of Socket. The standard library also strips off the IPv4 header for read and write consistency. up.

So why can ipv4.RawConn send IPv4 header data? This is because it sets the Socket:


1
2
3
4
5
6

func NewRawConn(c net. PacketConn) (*RawConn, error) {
so, ok := sockOpts[ssoHeaderPrepend]
return r, nil
}

The ssoHeaderPrepend option is to set IP_HDRINCL :


1

ssoHeaderPrepend: {Option: socket.Option{Level: iana.ProtocolIP, Name: unix.IP_HDRINCL, Len: 4 }},

So even if you don’t use ipv4.RawConn , you can also set *net.IPConn of the standard library to support hand-written IPv4 headers:


1

err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1 )

Of course, in order to support reading and writing IPv4 header at the same time, it is most convenient to convert to *ipv4.RawConn .

Use syscall.Socket to send and receive IP packets

The simplest, access system calls in other languages ​​like C, we can send and receive IPv4 packets. Sometimes when you develop network programs, you don’t have to worry about technical obstacles at all. The big deal is that we use the most primitive system calls to realize network communication.

The following example creates a Socket, here we do not use UDP protocol, in fact, you can transform it into UDP code.

Note that we need to set IP_HDRINCL to 1, we manually set the IPv4 header instead of letting the kernel protocol stack set it for us.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

func main() {
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
if err != nil {
fmt.Println( “socket failed:” , err)
return
}
defer syscall.Close(fd)
err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1 )
if err != nil {
panic (err)
}
// local address
addr := syscall.SockaddrInet4{Addr: [4 ] byte {127 , 0 , 0 , 1 }}
// Send a custom protocol packet
ip4 := &layers.IPv4{
SrcIP: net.ParseIP( “127.0.0.1” ),
DstIP: net.ParseIP( “192.168.0.1” ),
Version: 4 ,
TTL: 64 ,
Protocol: syscall.IPPROTO_RAW,
}
pbuf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
Compute Checksums: true ,
FixLengths: true ,
}
payload := [] byte ( “hello world” )
err = gopacket.SerializeLayers(pbuf, opts, ip4, gopacket.Payload(payload))
if err != nil {
fmt.Println( “serialize failed:” , err)
return
}
if err := syscall.Sendto(fd, pbuf.Bytes(), 0 , &addr); err != nil {
fmt.Println( “sendto failed:” , err)
return
}
buf := make ([] byte , 1024 )
for {
n, peer, err := syscall.Recvfrom(fd, buf, 0 )
if err != nil {
fmt.Println( “recvfrom failed:” , err)
return
}
raddr := net.IP(peer.(*syscall.SockaddrInet4).Addr[:]).String()
if raddr != “192.168.0.1” {
continue
}
iph, err := ipv4. ParseHeader(buf[:n])
if err != nil {
fmt.Println( “parse ipv4 header failed:” , err)
return
}
fmt.Printf( “received response from %s: %s\n” , raddr, string (buf[iph.Len:n]))
break
}
}
func htons(i uint16 ) uint16 {
return (i< <8 ) &0 xff00 | i> >8
}

The server-side code is as follows. Note that here we pay attention to the data packets of our program for value, and use bpf to filter, which will improve performance:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

func main() {
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
if err != nil {
fmt.Println( “socket failed:” , err)
return
}
defer syscall.Close(fd)
err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1 )
if err != nil {
panic (err)
}
filter. applyTo(fd)
// Receive custom protocol packets
buf := make ([] byte , 1024 )
for {
n, peer, err := syscall.Recvfrom(fd, buf, 0 )
if err != nil {
fmt.Println( “recvfrom failed:” , err)
return
}
iph, err := ipv4. ParseHeader(buf[:n])
if err != nil {
fmt.Println( “parse header failed:” , err)
return
}
if string (buf[iph.Len:n]) != “hello world” {
continue
}
fmt.Printf( “received request from %s: %s\n” , iph.Src.String(), string (buf[iph.Len:n]))
iph.Src, iph.Dst = iph.Dst, iph.Src
replayIpHeader, _ := iph.Marshal()
copy (buf[:iph.Len], replayIpHeader)
if err := syscall.Sendto(fd, buf[:n], 0 , peer); err != nil {
fmt.Println( “sendto failed:” , err)
return
}
}
}

Of course, you can also use a third-party library such as gopacket to send and receive IPv4 packets, but *ipv4.RawConn is enough for us, and we will use a third-party library whenever necessary, so we won’t introduce it here.

This article is transferred from: https://colobu.com/2023/05/13/send-IP-packets-in-Go/
This site is only for collection, and the copyright belongs to the original author.