This article is a part of “Programming with gopacket library”, which mainly focuses on manually constructing data link layer, network layer and transport layer packets to scan the IPv4 IP addresses of the whole network (in the example, mainland China) to see if the corresponding network is reachable. First, we need to know the IP addresses of the entire network. In fact, we can use fping to detect whether these IPs are connected, then we can quickly scan these IPs ourselves based on ICMP to find out which IP addresses are active on the entire network, and finally we can use tcp scan to scan the IPs of the entire network, and you can even scan the exposed Redis instances on the public network.

Please do not use the techniques presented in this article to do anything illegal. This article only shares the underlying (advanced) programming techniques of the network, not the business logic of the company, and does not involve any hacking.

Get the IP address of the whole public network

The global IP address block is assigned by IANA (Internet Assigned Numbers Authority) to the five major regional IP address allocation authorities around the world, which are listed below.

  • ARIN (American Registry for Internet Numbers) This organization is currently responsible for the assignment of IP addresses in North America. It is also responsible for assigning addresses to NSPs (Network Service Providers) worldwide.
  • RIPE (Reseaux IP Europeens) At present, this organization is mainly responsible for IP address allocation in Europe, the Middle East, Central Asia and other regions.
  • APNIC (Asia Pacific Network Information Center) This organization is currently responsible for IP address allocation in Asia and the Pacific region.
  • LACNIC (Latin America and the Caribbean Information Center) This organization is currently responsible for IP address assignment in the Latin America and the Caribbean region.
  • AFRINIC (African Network Information Centre) This organization is currently responsible for IP address assignment in the Africa region.

Top five global regional IP address allocation agencies

Some articles say there are also three regional centers, which should be an older term, and now there are five regional centers.

Although this article is about the public IP of the whole network, we still focus on our own public IP, which is the IP address assigned by APNIC. China Mobile, China Unicom, China Telecom and the former China Tietong, China Weitong, China Netcom and Education Network have all applied for a large number of network addresses, including a large number of IP addresses accumulated in the hands of cloud service providers such as Ali, Tencent, Baidu and Huawei.

These five regional centers provide a list of their assigned IP address ends and autonomous systems, and are publicly available, which you can obtain using the connection below.

1
2
3
4
5
https://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest
https://ftp.ripe.net/ripe/stats/delegated-ripencc-extended-latest
https://ftp.apnic.net/stats/apnic/delegated-apnic-extended-latest
https://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-extended-latest
https://ftp.afrinic.net/pub/stats/afrinic/delegated-afrinic-extended-latest

The most relevant to us are the addresses assigned by APNIC in Asia Pacific region.

By filtering, we can get the IP addresses assigned to mainland China.

1
2
3
#!/bin/bash
wget -c http://ftp.apnic.net/stats/apnic/delegated-apnic-latest
cat delegated-apnic-latest | awk -F '|' '/CN/&&/ipv4/ {print $4 "/" 32-log($5)/log(2)}' | cat > ipv4.txt

This ipv4.txt file contains the IP network segment 1 assigned to mainland China.

1
2
3
4
5
6
7
8
9
1.0.1.0/24
1.0.2.0/23
1.0.8.0/21
1.0.32.0/19
1.1.0.0/24
1.1.2.0/23
1.1.4.0/22
1.1.8.0/24
1.1.9.0/24

Now that we have all the IP addresses on the public network ready, the next step is to detect whether these IP addresses are alive or not. Because it is impossible to use all the IP addresses applied for, and even the IPs used may be offline or blocked, so not all IPs may be connected, and our example in this article is to find out these alive IPs quickly.

Bulk scan with fping

The most common tool we use to check whether a host is alive or not is ping.

A ping is a network tool used to test whether packets can reach a particular host over the IP protocol. ping works by sending an ICMP request echo packet to the target host and waiting to receive an echo response packet. The program estimates the rate of lost packets (packet loss rate) and the packet round-trip time (network latency) in terms of time and number of successful responses.

In December 1983, Mike Muuss wrote the first such program to facilitate the investigation of the root cause of IP network problems when they occurred. Because the program operated similarly to the active sonar of a submarine, he named it after the sound of sonar.

When we want to know whether a host is alive or not, we often say “ping its IP address and see if we can ping it”.

Linux’s ping tool supports the use of UDP or TCP for probing live, in addition to the ICMP protocol, because most network programs use the UDP and TCP protocols, so the use of these two protocols is more in line with the business network protocols, after all, network devices are likely not to handle ICMP and TCP/UDP the same way, for example, TCP program switches can Hash to select the next-hop port based on a five-tuple.

Although the ping tool is suitable for detecting hosts alive, it can only detect one target IP at a time, in our scenario in this paper, we want to detect very many Ip addresses, and detecting them one by one would take a huge amount of time, so we will use another tool: fping.

fping is similar to a ping operation, but with much better performance when pinging multiple hosts. fping has a very long history: Roland Schemers released its first version in 1992, and since then it has become the standard network diagnostic and statistical tool.

Here is a scan of the 8.8.8.8/24 network segment.

Scan the 8.8.8.8/24 segment using fping

Based on the fping function, we can probe each line of the ipv4.txt file segment by segment and output the probe results.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main
import (
    "bufio"
    "fmt"
    "os"
    "os/exec"
    "strings"
    "github.com/kataras/golog"
)
func main() {
    err := exec.Command("/bin/bash", "ip.sh").Run()
    if err != nil {
        golog.Fatal(err)
    }
    defer os.Remove("ipv4.txt")
    defer os.Remove("delegated-apnic-latest")
    ipList, err := os.Open("ipv4.txt")
    if err != nil {
        panic(err)
    }
    scanner := bufio.NewScanner(ipList)
    for scanner.Scan() {
        netmask := scanner.Text()
        fping(netmask)
    }
}

First we get all the assigned IP address segments of the continent through a script and store them in the ipv4.txt file. Each line in this file contains one IP address segment, and we use bufio.Scanner to scan it line by line. Every time we get an address segment, we call the fping function to process it.

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

func fping(netmask string) {
    fpingCmd := "fping -a -g " + netmask + " -C 1 -i 2 -H 32 -q -t 200 2>&1"
    cmd := exec.Command("/bin/bash", "-c", fpingCmd)
    r, err := cmd.StdoutPipe()
    if err != nil {
        return
    }
    err = cmd.Start()
    if err != nil {
        return
    }
    scanner := bufio.NewScanner(r)
    for scanner.Scan() {
        line := scanner.Text()
        if strings.Contains(line, ": -") {
            continue
        }
        if strings.Contains(line, "ICMP Time Exceeded") {
            continue
        }
        fmt.Println(line)
    }
    cmd.Wait()
}

Here we use exec.Command to call the fping command and scan one segment at a time, if the IP address is timeout or not working, we ignore it and print out only the surviving IP addresses.

Implementing ICMP scanning by ourselves

Although using fping has better performance than ping for multi-host probing, it still does not meet our needs. We want to continuously scan the entire network for IP address survivability, so this time we have to write our own program.

We can use the same probing protocol as ping and fping, send ICMP probe packets, if there is an ICMP Reply back, we think the network is through.

We use manual construction of data link layer frames to construct this sent data from the bottom.

Data link layer needs to set the mac address, the local host address we can get out (although there may be more than one NIC, but we have to pick out the actual route to use the Mac address of that NIC), according to the way the network is processed, we do not need to know the Mac address of the target address, just fill in the Mac address of our gateway, the gateway will propagate the probe packet through the routing protocol The gateway will propagate the probe packet out through the routing protocol and finally reach the target address, or it will be dropped in the middle. (If pinging a host on the same LAN, you need to fill in the Mac address of the target host, not the Mac address of the gateway)

Data link layer needs to set the mac address, the local host address we can get (although there may be more than one NIC, but we have to pick the actual route to use the Mac address of that NIC), according to the way the network is processed, we do not need to know the Mac address of the target address, just fill in the Mac address of our gateway, the gateway will propagate the probe packet through the routing protocol The gateway will propagate the probe packet out through the routing protocol and finally reach the target address, or it will be dropped in the middle. (If pinging a host on the same LAN, you need to fill in the Mac address of the target host, not the Mac address of the gateway)

1
2
3
4
5
eth := layers.Ethernet{
    SrcMAC:       s.iface.HardwareAddr,
    DstMAC:       *s.gwHardwareAddr,
    EthernetType: layers.EthernetTypeIPv4,
}

So the first step is that we need to find out the local and gateway Mac address. The protocol to find this address is arp protocol and we need to construct arp packet and process the return result of arp.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
// getHwAddr gets the hardware address of the gateway by sending an ARP request.
func (s *Scanner) getHwAddr() (net.HardwareAddr, error) {
    start := time.Now()
    arpDst := s.gw
    // prepare the layers to send for an ARP request.
    eth := layers.Ethernet{
        SrcMAC:       s.iface.HardwareAddr,
        DstMAC:       net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
        EthernetType: layers.EthernetTypeARP,
    }
    arp := layers.ARP{
        AddrType:          layers.LinkTypeEthernet,
        Protocol:          layers.EthernetTypeIPv4,
        HwAddressSize:     6,
        ProtAddressSize:   4,
        Operation:         layers.ARPRequest,
        SourceHwAddress:   []byte(s.iface.HardwareAddr),
        SourceProtAddress: []byte(s.src),
        DstHwAddress:      []byte{0, 0, 0, 0, 0, 0},
        DstProtAddress:    []byte(arpDst),
    }
    // send a single ARP request packet (we never retry a send)
    if err := s.sendPackets(ð, &arp); err != nil {
        return nil, err
    }
    // wait 3 seconds for an ARP reply.
    for {
        if time.Since(start) > time.Second*3 {
            return nil, errors.New("timeout getting ARP reply")
        }
        data, _, err := s.handle.ReadPacketData()
        if err == pcap.NextErrorTimeoutExpired {
            continue
        } else if err != nil {
            return nil, err
        }
        packet := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.NoCopy)
        if arpLayer := packet.Layer(layers.LayerTypeARP); arpLayer != nil {
            arp := arpLayer.(*layers.ARP)
            if net.IP(arp.SourceProtAddress).Equal(net.IP(arpDst)) {
                return net.HardwareAddr(arp.SourceHwAddress), nil
            }
        }
    }
}
// sendPackets sends a packet with the given layers.
func (s *Scanner) sendPackets(l ...gopacket.SerializableLayer) error {
    if err := gopacket.SerializeLayers(s.buf, s.opts, l...); err != nil {
        return err
    }
    return s.handle.WritePacketData(s.buf.Bytes())
}

To construct these underlying network packets (frames), we commonly use the gopacket library, and use it to send and receive packets. The function above starts by constructing the contents of layers.Ethernet and layers.ARP, then calls sendPackets to send them out, and then calls s.handle.ReadPacketData() to read the arp return, which contains the gateway’s Mac address.

In fact, we define a Scanner that facilitates us to handle the whole logic. When this Scanner is initialized, the local IP address, local Mac address, and gateway Mac address used for probing are prepared.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
// Scanner represents a ICMP scanner. It contains a pcap handle and
// other information that is needed to scan the network.
type Scanner struct {
    // iface is the network interface on which to scan.
    iface *net.Interface
    // gw is the gateway address.
    gw net.IP
    // gwHardwareAddr is the gateway hardware address.
    gwHardwareAddr *net.HardwareAddr
    // src is the source IP address.
    src net.IP
    // handle is the pcap handle.
    handle *pcap.Handle
    // opts and buf allow us to easily serialize packets in the send() method.
    opts gopacket.SerializeOptions
    buf  gopacket.SerializeBuffer
}
// NewScanner creates a new Scanner.
func NewScanner() *Scanner {
    s := &Scanner{
        opts: gopacket.SerializeOptions{
            FixLengths:       true,
            ComputeChecksums: true,
        },
        buf: gopacket.NewSerializeBuffer(),
    }
    router, err := routing.New()
    if err != nil {
        log.Fatal(err)
    }
    // figure out the route by using the IP.
    iface, gw, src, err := router.Route(net.ParseIP("114.114.114.114"))
    if err != nil {
        log.Fatal(err)
    }
    s.gw, s.src, s.iface = gw, src, iface
    // open the handle for reading/writing.
    handle, err := pcap.OpenLive(iface.Name, 100, true, pcap.BlockForever)
    if err != nil {
        log.Fatal(err)
    }
    s.handle = handle
    gwHwAddr, err := s.getHwAddr()
    if err != nil {
        log.Fatal(err)
    }
    s.gwHardwareAddr = &gwHwAddr
    log.Infof("scanning with interface %v, gateway %v, src %v, hwaddr: %v", iface.Name, gw, src, gwHwAddr)
    return s
}

Here is a trick, generally servers may have multiple NICs and more Ip addresses, so which NIC and local IP address to use when probing? You can use router.Route to get the local NIC, gateway, local IP, etc. used to access the public network. In this example, we visit the well-known 114.114.114.114 public network address as the target address, save this information for backup, then call getHwAddr to get the Mac address of the gateway.

gopacket opens a device for reading and writing network data via pcap.OpenLive, and we have all this ready when we create Scanner.

Now everything is ready, we just need to pass the destination IP to be probed to it and let it do the probing. Here we implement a Scan method.

1
2
3
4
5
6
7
8
// Scan scans the network and returns a channel that contains the
// IP addresses of the hosts that respond to ICMP echo requests.
func (s *Scanner) Scan(input chan []string) (output chan string) {
    output = make(chan string, 1024*1024)
    go s.recv(output)
    go s.send(input)
    return output
}

input is a channel, into which the user can pass the target IP to be probed. It will return an output channel, which contains the active target IPs.

Then the whole logic is clear: start a goroutine to send ICMP packets, start a goroutine to receive ICMP, there is no blocking between the two, and the performance is naturally very good.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// send sends a single ICMP echo request packet for each ip in the input channel.
func (s *Scanner) send(input chan []string) error {
    id := uint16(os.Getpid())
    seq := uint16(0)
    for ips := range input {
        for _, ip := range ips {
            dstIP := net.ParseIP(ip)
            if dstIP == nil {
                continue
            }
            dstIP = dstIP.To4()
            if dstIP == nil {
                continue
            }
            // construct all the network layers we need.
            eth := layers.Ethernet{
                SrcMAC:       s.iface.HardwareAddr,
                DstMAC:       *s.gwHardwareAddr,
                EthernetType: layers.EthernetTypeIPv4,
            }
            ip4 := layers.IPv4{
                SrcIP:    s.src,
                DstIP:    dstIP.To4(),
                Version:  4,
                TTL:      64,
                Protocol: layers.IPProtocolICMPv4,
            }
            icmpLayer := layers.ICMPv4{
                TypeCode: layers.CreateICMPv4TypeCode(layers.ICMPv4TypeEchoRequest, 0),
                Id:       id,
                Seq:      seq,
            }
            seq++
            err := s.sendPackets(ð, &ip4, &icmpLayer)
            if err != nil {
                log.Error(err)
            }
        }
    }
    return nil
}

Sending data constructs layers.Ethernet, layers.IPv4, layers.ICMPv4 packets, each ICMPv4 packet probes a destination IP, which is only responsible for sending.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// recv receives ICMP echo reply packets and sends the IP addresses
func (s *Scanner) recv(output chan string) {
    defer close(output)
    // set the filter to only receive ICMP echo reply packets.
    s.handle.SetBPFFilter("dst host " + s.src.To4().String() + " and icmp")
    for {
        // read in the next packet.
        data, _, err := s.handle.ReadPacketData()
        if err == pcap.NextErrorTimeoutExpired {
            continue
        } else if errors.Is(err, io.EOF) {
            // log.Infof("error reading packet: %v", err)
            return
        } else if err != nil {
            log.Infof("error reading packet: %v", err)
            continue
        }
        packet := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.NoCopy)
        // find the packets we care about, and print out logging
        // information about them.  All others are ignored.
        if net := packet.NetworkLayer(); net == nil {
            // log.Info("packet has no network layer")
            continue
        } else if ipLayer := packet.Layer(layers.LayerTypeIPv4); ipLayer == nil {
            // log.Info("packet has not ip layer")
            continue
        } else if ip, ok := ipLayer.(*layers.IPv4); !ok {
            continue
        } else if icmpLayer := packet.Layer(layers.LayerTypeICMPv4); icmpLayer == nil {
            // log.Info("packet has not icmp layer")
            continue
        } else if icmp, ok := icmpLayer.(*layers.ICMPv4); !ok {
            // log.Info("packet is not icmp")
            continue
        } else if icmp.TypeCode.Type() == layers.ICMPv4TypeEchoReply {
            // log.Info("packet is not icmp")
            select {
            case output <- ip.SrcIP.String():
            default:
            }
        } else {
            // log.Info("ignoring useless packet")
        }
    }
}

The receiving logic has a trick, it filters the packet in the kernel state by BPFFilter, and the value concerns our ICMP return packet. The format is the same as in the tcpdump tool.

After it reads the ICMP packet, it writes the target address to output.

Basically, using less than one CPU resource, in about one and a half hours all the public IPs on the mainland were probed.

Implement your own TCP scan and find the exposed Redis port IP address

Well, the above technique is good and can quickly scan the whole network for public addresses, but we want to go a step further and use TCP to scan the whole network for public IPs, and we scan specific -ports in order to see if a service is exposed on the public IP. This kind of scanning is often done by security companies, but this time we implemented it manually.

This time we scan port 6379, which is the default port for redis. Although having this port open on the server does not mean that Redis services are deployed, it is very likely that they are. For security reasons, these exposed Redis services should be set to AUTH, and should even be set to allow specific IP access through the firewall.

With the TCP approach to probing, we use the first two steps of the three handshakes: first send a syn packet, and the other side may return a sync+ack packet (if the port is open) or an rst packet (if the port is not open, or if the connection is denied), or nothing is returned. We will only focus on the first two.

In fact, the code logic and the above ICMP program is almost too much, get the gateway Mac address and other ways and the above and the example is not much different. We still define a Scanner type, but with more information about the local port and remote port.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Scanner is the main struct that holds all the state for the scanner.
type Scanner struct {
    // iface is the interface to send packets on.
    iface *net.Interface
    // gw and src are the gateway and source IP addresses of the
    // interface we're scanning.
    gw, src          net.IP
    gwHardwareAddr   *net.HardwareAddr
    srcPort, dstPort int
    // handle is the pcap handle that we use to receive packets.
    handle *pcap.Handle
    // opts and buf allow us to easily serialize packets in the send()
    // method.
    opts gopacket.SerializeOptions
    buf  gopacket.SerializeBuffer
}

We want to collect the IP addresses that can be connected but the port is not open, and the IP addresses where the port is open, so the Scan method has been fine-tuned to support the output of both lists.

1
2
3
4
5
6
7
8
// Scan scans the network for open TCP ports.
func (s *Scanner) Scan(input chan []string) (connOutput, portOpenOutput chan string) {
    connOutput = make(chan string, 1024*1024)
    portOpenOutput = make(chan string, 1024*1024)
    go s.recv(connOutput, portOpenOutput)
    go s.send(input)
    return connOutput, portOpenOutput
}

When sending data we have to construct TCP syn packets.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// send sends packets to the network.
func (s *Scanner) send(input chan []string) error {
    for ips := range input {
        for _, ip := range ips {
            dstIP := net.ParseIP(ip)
            if dstIP == nil {
                continue
            }
            dstIP = dstIP.To4()
            if dstIP == nil {
                continue
            }
            // construct all the network layers we need.
            eth := layers.Ethernet{
                SrcMAC:       s.iface.HardwareAddr,
                DstMAC:       *s.gwHardwareAddr,
                EthernetType: layers.EthernetTypeIPv4,
            }
            ip4 := layers.IPv4{
                SrcIP:    s.src,
                DstIP:    dstIP.To4(),
                Version:  4,
                TTL:      64,
                Protocol: layers.IPProtocolTCP,
            }
            tcp := layers.TCP{
                SrcPort: layers.TCPPort(s.srcPort),
                DstPort: layers.TCPPort(s.dstPort),
                SYN:     true,
            }
            tcp.SetNetworkLayerForChecksum(&ip4)
            err := s.sendPackets(ð, &ip4, &tcp)
            if err != nil {
                log.Error(err)
            }
        }
    }
    return nil
}

When receiving data, we distinguish between TCP+ACK packets and RST packets.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
// recv receives packets from the network.
func (s *Scanner) recv(connOutput, portOpenOutput chan string) {
    defer close(connOutput)
    defer close(portOpenOutput)
    s.handle.SetBPFFilter("dst port " + strconv.Itoa(s.srcPort) + " and dst host " + s.src.To4().String())
    for {
        // read in the next packet.
        data, _, err := s.handle.ReadPacketData()
        if err == pcap.NextErrorTimeoutExpired {
            continue
        } else if errors.Is(err, io.EOF) {
            // log.Infof("error reading packet: %v", err)
            return
        } else if err != nil {
            log.Infof("error reading packet: %v", err)
            continue
        }
        packet := gopacket.NewPacket(data, layers.LayerTypeEthernet, gopacket.NoCopy)
        // find the packets we care about, and print out logging
        // information about them.  All others are ignored.
        if net := packet.NetworkLayer(); net == nil {
            // log.Info("packet has no network layer")
            continue
        } else if ipLayer := packet.Layer(layers.LayerTypeIPv4); ipLayer == nil {
            // log.Info("packet has not ip layer")
            continue
        } else if ip, ok := ipLayer.(*layers.IPv4); !ok {
            continue
        } else if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer == nil {
            // log.Info("packet has not tcp layer")
        } else if tcp, ok := tcpLayer.(*layers.TCP); !ok {
            continue
        } else if tcp.DstPort != layers.TCPPort(s.srcPort) {
            // log.Infof("dst port %v does not match", tcp.DstPort)
        } else if tcp.RST {
            select {
            case connOutput <- ip.SrcIP.String():
            default:
            }
        } else if tcp.SYN && tcp.ACK {
            select {
            case portOpenOutput <- ip.SrcIP.String():
            default:
            }
        } else {
            // log.Printf("ignoring useless packet")
        }
    }
}

The code structure is not very different from ICMP probing.

With the help of gopacket (libpcap) library, we can easily and efficiently implement a network-wide public IP scan, which is very meaningful for network probing and security checking. According to the above example, you can also easily implement the UDP method of detection, you may also want to practice.

Ref

  • https://colobu.com/2023/03/19/scan-all-IP-addresses-of-mainland-fastly-like-lightning/