© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
N. TolaramSoftware Development with Gohttps://doi.org/10.1007/978-1-4842-8731-6_10

10. System Networking

Nanik Tolaram1  
(1)
Sydney, NSW, Australia
 

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:
  • Using the standard library to write network tools

  • The net/dns package

  • How DNS packs and unpacks messages

Source Code

The source code for this chapter is available from the https://github.com/Apress/Software-Development-Go repository.

Ping Utility

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 icmp package 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.

Compile the app as follows:
go build -o pinggoogle .
Run the app with root, as shown:
sudo ./pinggoogle
You will see the following output:
2022/01/21 00:07:09 Ping golang.org (142.250.66.241): 21.30063ms

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.org domain.
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.
func Ping(addr string) (*net.IPAddr, time.Duration, error) {
   // Listen for ICMP reply
   c, err := icmp.ListenPacket("ip4:icmp", ListenAddr)
   if err != nil {
       return nil, 0, err
   }
   defer c.Close()
   ...
}
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.Message struct 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:
const (
   ICMPTypeEchoReply              ICMPType = 0  // Echo Reply
   ICMPTypeDestinationUnreachable ICMPType = 3  // Destination Unreachable
   ICMPTypeRedirect               ICMPType = 5  // Redirect
   ICMPTypeEcho                   ICMPType = 8  // Echo
   ICMPTypeRouterAdvertisement    ICMPType = 9  // Router Advertisement
   ICMPTypeRouterSolicitation     ICMPType = 10 // Router Solicitation
   ICMPTypeTimeExceeded           ICMPType = 11 // Time Exceeded
   ICMPTypeParameterProblem       ICMPType = 12 // Parameter Problem
   ICMPTypeTimestamp              ICMPType = 13 // Timestamp
   ICMPTypeTimestampReply         ICMPType = 14 // Timestamp Reply
   ICMPTypePhoturis               ICMPType = 40 // Photuris
   ICMPTypeExtendedEchoRequest    ICMPType = 42 // Extended Echo Request
   ICMPTypeExtendedEchoReply      ICMPType = 43 // Extended Echo Reply
)
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:
   // Allocate 1500 byte for reading response
   reply := make([]byte, 1500)
   // Set deadline of 1 minute
   err = c.SetReadDeadline(time.Now().Add(1 * time.Minute))
   ...
   // Read from the connection
   n, peer, err := c.ReadFrom(reply)
   ...
   // Use ParseMessage to parsed the bytes received
   rm, err := icmp.ParseMessage(ICMPv4, reply[:n])
   if err != nil {
       return dst, 0, err
   }
   // Check for the type of ICMP result
   switch rm.Type {
   case ipv4.ICMPTypeEchoReply:
       return dst, duration, nil
   ...
   }

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:
switch rm.Type {
case ipv4.ICMPTypeEchoReply:
  return dst, duration, nil
default:
  return dst, 0, fmt.Errorf("got %+v from %v; want echo reply", rm, peer)
}

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:
; <<>> DiG 9.11.5-P4-5.1ubuntu2.1-Ubuntu <<>> @localhost -p 8090 golang.org
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26897
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;golang.org.                    IN      A
;; ANSWER SECTION:
golang.org.             294     IN      A       142.250.71.81
;; Query time: 6 msec
;; SERVER: ::1#8090(::1)
;; WHEN: Mon Mar 14 22:20:31 AEDT 2022
;; MSG SIZE  rcvd: 55
You can also use nslookup, as follows:
nslookup -port=8090 golang.org localhost

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:
func main() {
  dnsConfig := DNSConfig{
     ...
     port:         8090,
  }
  conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: dnsConfig.port})
  ...
}
Once it successfully opens port 8090, the next thing it will do is to open a connection to the external DNS server and start the server.
func main() {
  dnsConfig := DNSConfig{
     dnsForwarder: "8.8.8.8:53",
     ...
  }
  ...
  dnsFwdConn, err := net.Dial("udp", dnsConfig.dnsForwarder)
  ...
  dnsServer := dns.NewServer(conn, dns.NewUDPResolver(dnsFwdConn))
  ...
  dnsServer.Start()
}
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.
func (r *DNSResolver) ResolveDNS(msg dnsmessage.Message) (dnsmessage.Message, error) {
  packedMsg, err := msg.Pack()
  ...
  _, err = r.fwdConn.Write(packedMsg)
  ...
  resBuf := make([]byte, 1024)
  _, err = r.fwdConn.Read(resBuf)
  ...
  var resMsg dnsmessage.Message
  err = resMsg.Unpack(resBuf)
  ...
}
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 Message struct.
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 Resource struct is used in the Answers, Authorities, and Additionals fields as follows:
type Resource struct {
  Header ResourceHeader
  Body   ResourceBody
}
The Questions field 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.46.96