In the previous chapter, you wrote TCP and UDP applications using the standard library. In this chapter, you will use this knowledge to build system network tools. The objective of writing these tools is to gain a better understanding of how easy it is to so using the capability of the Go standard library. This surfaces the fact that the standard library provides a lot of capabilities, enabling developers to build all kinds of network-related applications.
In this chapter, you will get a good understanding of the following:
In this section, you will write an application that provides ping-like functionality. The code can be found inside the chapter10/ping folder.
The application uses the icmppackage provided by the Go standard library, and the documentation can be found at https://pkg.go.dev/golang.org/x/net/icmp. As outlined in the documentation, this package provides functions to manipulate the ICMPv4/6, which is based on RFC 792 and RFC 4443.
To provide ping-like functionality, the code uses the Internet Control Message Protocol (IMCP), which is part of the IP stack that all networking stacks use, which means that any computer that uses an IP stack can respond to ICMP requests unless it is disabled. The IP network stack has the capability to respond to an ICMP request regardless of where it is running. The ICMP is part of the IP stack, which is normally used for error reporting and network diagnostics.
Code Walkthrough
You are going to dive into the sample code to understand how the whole thing works. The application starts off by calling the Ping() function to ping a single domain. In this example, it will ping for the golang.orgdomain.
func main() {
addr := "golang.org"
dst, dur, err := Ping(addr)
if err != nil {
panic(err)
}
log.Printf("Ping %s (%s): %s
", addr, dst, dur)
}
The function performs a number of operations. Let's take a look at the code section by section. The following code snippet calls icmp.ListenPacket(), which is part of the golang.org/x/net standard library package. This opens a local socket that will be used for ICMP communication with the remote host.
The opened socket is used only for ICMP communication, which means the socket can only understand ICMP network packets. When the local socket has been successfully opened, the code must resolve the IP address of the domain that the application wants to ping. The following code uses the net.ResolveIPAddr()function call to resolve the domain to its respective IP address:
dst, err := net.ResolveIPAddr("ip4", addr)
if err != nil {
panic(err)
return nil, 0, err
}
Now that you have opened a local socket connection for ICMP and resolved the IP address of the destination domain, the next step is to initialize the ICMP packet and send it off to the destination, as shown in the following code snippets:
// Prepare new ICMP message
m := icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & 0xffff,
Seq: 1,
Data: []byte(""),
},
}
The icmp.Messagestruct defines the information that will be sent as an ICMP packet to the destination, which is defined inside the golang.org/x/net package and looks like the following:
// A Message represents an ICMP message.
type Message struct {
Type Type // type, either ipv4.ICMPType or ipv6.ICMPType
Code int // code
Checksum int // checksum
Body MessageBody // body
}
The ICMP packet can contain different kinds of ICMP parameters, and this can be specified using the Type field. Here, you use the ipv4.ICMPTypeEcho type. The following are the available types provided in Go:
Once the type has been defined, the next field that needs to contain information is the Body field. Here you use icmp.Echo, which will contain echo requests:
type Echo struct {
ID int // identifier
Seq int // sequence number
Data []byte // data
}
Data is converted to the byte format using the Marshal(..)function and is then sent out to a destination by using the WriteTo(b,dst)function.
...
// Marshal the data
b, err := m.Marshal(nil)
if err != nil {
return dst, 0, err
}
...
// Send ICMP packet now
n, err := c.WriteTo(b, dst)
The last step is to read and parse the response message obtained from the server, as shown here:
Reading the packet is performed when calling the ReadFrom(..)function with the result stored inside the reply variable. The reply variable contains a sequence of bytes, which is the ICMP response. To make it easy to read and manipulate the data, you use the ParseMessage(..)function specifying the ICMP format type of ICMPv4. The return value will be of type Message struct.
Once you have parsed the code, you check the response type that is received, as shown in the following snippet:
In this section, you learned to open and use local socket connections to send and receive data when using ICMP provided in the standard library. You also learned how to parse and print the response like how a ping utility normally does.
DNS Server
Using the knowledge from the previous chapter on writing a UDP server, you will write a DNS server. The aim of this section is not to write a full-blown DNS server, but rather to show how to use UDP to write it. The DNS server is a DNS forwarder that uses other publicly available DNS servers to perform the DNS lookup functionality, or you can think of it as a DNS server proxy.
Running a DNS Server
The code is located inside the chapter10/dnsserver folder. Compile the code as follows:
go build -o dns cmd/main.go
Run it by executing the dns executable:
./dns
You get the following message when the app starts up successfully:
2022/03/14 22:17:15 Starting up DNS server on port 8090
The DNS server is now ready to serve DNS requests on port 8090.
To test the DNS server, use dig as follows:
dig @localhost -p 8090 golang.org
You get DNS output from dig, something like the following:
Now that you have successfully run and used the DNS server, in the next section you will look at how to write the code.
DNS Forwarder
In this section, you will use a DNS forwarder that is based on UDP to forward the query to an external DNS server and use the response to report back to the client. In your code, you’ll use Google’s public DNS server 8.8.8.8 to perform the query.
The first thing the code will do is to create a local UDP server that listens on port 8090, as shown here:
The local UDP server waits for incoming DNS requests. Once it receives an incoming UDP request, it is processed by handleRequest(). You saw in the previous section that the way to read a UDP request is to call the ReadFromUDP(..)function, as shown here:
func (s *Server) handleRequest() error {
msg, clientAddr, err := s.readRequest()
...
}
func (s *Server) readRequest() (dnsmessage.Message, *net.UDPAddr, error) {
buf := make([]byte, 1024)
_, addr, err := s.conn.ReadFromUDP(buf)
...
}
The readRequest()function, on receiving the incoming request, proceeds to unpack the data using the built-in golang.org/x/n/dns package, as shown here:
func (s *Server) readRequest() (dnsmessage.Message, *net.UDPAddr, error) {
...
var msg dnsmessage.Message
err = msg.Unpack(buf)
...
}
The unpacked data is now stored in a dnsmessage.Message struct that has the following declaration:
type Message struct {
Header
Questions []Question
Answers []Resource
Authorities []Resource
Additionals []Resource
}
The code successfully unpacks the data from the incoming request. The next step is to send the same request to the DNS forwarder and process the response to be forwarded back to the client. The ResolveDNS(..)function sends the newly created dnsmessage.Message struct to the DNS forwarder and processes the received response.
On receiving a response from the DNS forwarder, the handleRequest(..)function sends either a DNS normal response or an error message, depending on the returned value from ResolveDNS(..).
func (s *Server) handleRequest() error {
...
rMsg, err := s.resolver.ResolveDNS(msg)
if err != nil {
s.sendResponseWithError(clientAddr, msg, err)
...
}
...
return s.sendResponse(clientAddr, rMsg)
}
The sendResponse(..)function just packs the received message from the DNS forwarder and sends it back to the client.
func (s *Server) sendResponseWithError(clientAddr *net.UDPAddr, msg dnsmessage.Message, err error) {
...
err = s.sendResponse(clientAddr, msg)
...
}
func (s *Server) sendResponse(addr *net.UDPAddr, message dnsmessage.Message) error {
packed, err := message.Pack()
...
_, err = s.conn.WriteToUDP(packed, addr)
}
Pack and Unpack
In the previous section, you looked at how requests are processed by unpacking the response and then packing and sending it back as a DNS response to a client. In this section, you will look at the structure of the DNS data.
An incoming request comes in a byte, which is unpacked or converted to a Messagestruct.
type Message struct {
Header
Questions []Question
Answers []Resource
Authorities []Resource
Additionals []Resource
}
The Header field contains the following structure, which corresponds to the header to the DNS protocol:
type Header struct {
ID uint16
Response bool
OpCode OpCode
Authoritative bool
Truncated bool
RecursionDesired bool
RecursionAvailable bool
RCode RCode
}
The Resourcestruct is used in the Answers, Authorities, and Additionals fields as follows:
type Resource struct {
Header ResourceHeader
Body ResourceBody
}
The Questionsfield contains information about the DNS information that the client is requesting, while the Answers field contains the response to the questions. Figure 10-1 shows what the dnsmessage.Message struct contains when it unpacks data from an incoming request to query google.com using dig with the following command:
dig @localhost -p 8090 google.com
A screenshot of system networking DNS data code includes the message, header, Id, response, Opcode, truncated, questions, name, type, class, answers, header, and body.
Figure 10-1
dnsmessage.Message with DNS query data
Figure 10-2 shows the response received from the DNS forwarder when the bytes are unpacked. As you can see, the Answers field is populated with the answer to the query.
A screenshot of system networking DNS data code includes the message, header, Id, response, Opcode, truncated, questions, name, type, class, answers, header, and body.
Figure 10-2
dnsmessage.Message with DNS response data
Summary
In this chapter, you learn more details about using UDP. One of the features of the IP stack is to check the availability of a server using the ICMP protocol. You also learned about using UDP to write a DNS forwarder server that uses the net/dns package standard library to process DNS requests and responses. You now have a better understanding of the features of the standard library than the capability that is provided; at the same time, it shows how versatile the libraries are in allowing us to develop useful network tools.