4

像闪电一样快速扫描全网的IP地址

 1 year ago
source link: https://colobu.com/2023/03/19/scan-all-IP-addresses-of-mainland-fastly-like-lightning/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

新开一个Go编程系列,主要讲Go语言的高级编程技术,希望能像《Go并发编程指南》一样形成一个系列,不要断更。

这一篇是使用gopacket库编程的一部分,主要是通过手工构造数据链路层、网络层、传输层的包,实现扫描全网(示例中是中国大陆的) ipv4的IP地址,看看对应的网络是否可达。 首先我们需要知道全网的IP地址,其实我们可以使用fping探测这些IP是否连通,然后我们自己基于ICMP快速扫描这些IP,找出全网活跃的IP地址,最后我们使用tcp scan的方式扫描全网的IP,甚至你可以扫描公网上暴露的Redis实例。

请勿用本文介绍的技术做任何违法的事情。本文只分享网络底层(高级)的编程技术,不涉及公司的业务逻辑,不涉及任何黑盒行为。

获取全网公网的IP地址

全球IP地址块被IANA(Internet Assigned Numbers Authority)分配给全球五大地区性IP地址分配机构,它们分别是:

  • ARIN (American Registry for Internet Numbers)
    目前该机构主要负责北美地区的 IP地址分配。同时也负责为全球 NSP (Network Service Providers) 分配地址。
  • RIPE (Reseaux IP Europeens)
    目前该机构主要负责欧洲、中东、中亚等地区的 IP 地址分配。
  • APNIC (Asia Pacific Network Information Center)
    目前该机构负责亚洲、太平洋地区的 IP 地址分配。
  • LACNIC (Latin America and the Caribbean Information Center)
    目前该机构负责拉丁美洲和加勒比地区的 IP 地址分配。
  • AFRINIC (African Network Information Centre)
    目前该机构负责非洲区域的 IP 地址分配。

rirs.png

有些文章说还三大地区性中心,应该是比较古老的说法,现在是五达区域中心。

虽然本文中说的是全网的公网IP,但是我们还是聚焦我们国家自己的公网IP, 也就是由APNIC负责分配的IP地址。我国中国移动、中国联通、中国电信以及以前的中国铁通、中国卫通、中国网通、教育网等都申请了一大批的网络地址,包括现在阿里、腾讯、百度、华为等云服务商手里也积囤积了一大批的IP地址。

这五大区域中心提供了它们分配的IP地址端和自治系统列表,并且是公开的,你可以使用下面的连接获取:

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

和我们最相关的是亚洲太平洋地区APNIC分配的地址。虽然本文中说的是全网的公网IP,但是我们还是聚焦我们国家自己的公网IP, 也就是由APNIC负责分配的IP地址。我国中国移动、中国联通、中国电信以及以前的中国铁通、中国卫通、中国网通、教育网等都申请了一大批的网络地址,包括现在阿里、腾讯、百度、华为等云服务商手里也积囤积了一大批的IP地址。

通过过滤,我们可以得到分配给中国大陆的IP地址:

#!/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

这个ipv4.txt文件包含分配给中国大陆的IP网段1:

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

本文不涉及IP地址归属的问题。通过whois,我们能够查询某个网段隶属于某个运行商和省份,但是也不完全准确,可能会有IP地址借用等问题,所以有http://www.ipip.net/这样的服务商,通过其他一些手段,提供更精准的精确到县区市的IP归属信息。

现在公网所有的IP地址我们都准备好了,下一步就是探测这些IP地址是不是活着的。因为申请的IP地址不可能全部都使用,即使使用的IP也可能有关机或者被封禁的问题,所以不一定所有IP都能连接上,我们本文的例子,就是要把这些活着的IP快速找出来。

使用fping批量扫描

我们检查主机是否存活的最常用的工具就是ping。

ping是一种网络工具,用来测试数据包能否透过IP协议到达特定主机。ping的运作原理是向目标主机传出一个ICMP的请求回显数据包,并等待接收回显回应数据包。程序会按时间和成功响应的次数估算丢失数据包率(丢包率)和数据包往返时间(网络时延)。

在1983年12月,Mike Muuss编写了首个这样的程序,用于在IP网络出现问题时方便探查其根源。因为这个程序的运作原理与潜水艇的主动声纳相似,他便用声纳的声音来为程序取名。

当我们想知道某个主机是否存活时,常常说 “ping一下它的IP地址,看看是否能ping通”。

Linux的ping工具除了使用ICMP协议外,还支持使用UDP或者TCP的方式进行探活,因为大部分网络程序都使用的UDP和TCP协议,所以使用这两个协议更符合业务的网络协议,毕竟网络设备对ICMP和TCP/UDP处理很可能不是一样的,比如TCP程序交换机可以根据五元组进行哈希选择下一跳的端口。

虽然ping工具适合探测主机存活的场景,但是它一次只能探测一个目标IP,在我们本文中的场景中,我们想探测非常多的Ip地址,一个一个的探测的话不知道探到猴年马月了,所以我们会用到另外一个工具: fping

fping类似于 ping 操作,但在 ping 多个主机时性能要好得多。fping有着非常悠久的历史:Roland Schemers在1992年发布了它的第一个版本,并且从那时起,它已成为标准的网络诊断和统计工具 。

下面就是扫描 8.8.8.8/24网段的情况:

fping.png

基于fping的功能,我们可以逐网段的探测ipv4.txt文件中的每一行的网段,输出探测结果:

package main
import (
"bufio"
"fmt"
"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)

首先我们通过脚本得到大陆的所有分配的IP地址段,存入到ipv4.txt文件中。这个文件中每一行都包含一个IP地址段,我们使用bufio.Scanner逐行扫描。每得到一个地址段,就调用fping函数处理:

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()

这里我们使用exec.Command调用fping命令,一次扫描一个网段,如果是超时的或者不通的IP,我们忽略,只把存活的IP地址打印出来。

自己实现ICMP扫描

使用fping虽然相比ping对于多主机的探测方式性能更好,但是还是达不到我们的需求,我们想持续扫描全网的IP地址的存活性,这个时候我们就不得不编写我们自己的程序了。

我们可以采用和ping、fping的探测协议一样,发送ICMP探测包,如果有ICMP的Reply回来,我们就认为网络是通的。

我们采用手工构造数据链路层的帧,从底层构造这个发送的数据。

数据链路层需要设置mac地址, 本地主机的地址我们可以获取出来(虽然可能有多个网卡,但是我们要挑选出实际路由使用的那个网卡的Mac地址),根据网络处理的方式,我们不需要知道目标地址的Mac地址,只需要填写我们的网关的Mac地址即可,网关会通过路由协议把探测包传播出去,最终到达目标地址,或者中途被丢掉。(如果ping同一个局域网内的主机,则需要填写目标主机的Mac地址,而不是网关的 Mac地址)

eth := layers.Ethernet{
SrcMAC: s.iface.HardwareAddr,
DstMAC: *s.gwHardwareAddr,
EthernetType: layers.EthernetTypeIPv4,

所以第一部就是我们要把本地和网关的Mac地址找出来。找这个地址的协议是arp协议,我们需要构造arp包,处理arp的返回结果:

// 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.
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())

构造这些底层的网络包(帧),我们常用的是gopacket库,并且利用它发送和接收包。
上面这个函数开始构造了layers.Ethernetlayers.ARP的内容,然后调用sendPackets发送出去,然后调用s.handle.ReadPacketData()读取arp返回结果,返回结果中就包含了网关的Mac地址。

实际上,我们定义了一个Scanner,方便我们处理整个逻辑。这个Scanner初始化的时候,把探测使用的本地IP地址、本地Mac地址、网关Mac地址都准备好:

// 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

这里有一个技巧,一般服务器可能有多个网卡和更多的Ip地址,那么探测的时候使用哪一个网卡和本地IP地址呢?可以使用router.Route,得到访问公网锁使用的本地网卡、网关、本地IP等,这个例子中我们访问知名的114.114.114.114公网地址作为目标地址,把这些信息保存下来备用,接着调用getHwAddr获取网关的Mac地址。

gopacket通过pcap.OpenLive打开一个设备进行读写网络数据,我们在创建Scanner时候都把这些准备好。

现在万事俱备,只欠东风了,我们需要把要探测的目的IP传给它,让它进行探测。这里我们实现一个Scan方法:

// 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是一个channel, 用户可以把要探测的目标IP传入到这个channel中。它会返回一个output channel,这个channel中包含活跃的目标IP。

那么整个逻辑都很清晰了:启动一个goroutine发送ICMP包,启动一个goroutine接收ICMP,两者之间并没有阻塞,性能自然非常的好:

// 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

发送数据构造layers.Ethernetlayers.IPv4layers.ICMPv4包,每个ICMPv4包探测一个目标IP,它值负责发送。

// 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")
// 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")

接收的逻辑有个技巧,它通过BPFFilter对packet在内核态进行过滤,值关注我们的ICMP回包。 格式你参考tcpdump工具的格式就行,一样一样的。

它读取到ICMP的回包后,就把目标地址写入到output中。

基本上,使用不到一个CPU的资源,在一个半小时左右就把大陆所有的公网IP探测了一遍。

自己实现TCP扫描,并找出暴露Redis端口IP地址

嗯,上面的技术不错,可以很快的扫描全网的公网地址,但是我们还想进一步,使用TCP的方式扫描全网的公网IP,而且,我们扫描特定的·端口,以便看看是否在公网IP上暴露了某个服务。这种扫描经常是安全公司去做扫描,这次我们自己手工实现一把。
这次我们扫描6379端口,这是redis默认的端口。虽然服务器上开了这个端口并不意味着就步数了Redis服务,但是很大可能的确部署了Redis服务。出于安全的考虑,这些暴露的Redis服务应该都设置了AUTH才对,甚至应该通过防火墙设置值允许特定的IP访问才对。

采用TCP方式探测,我们使用三次握手中的前两步:先发送一个syn包,对方可能回一个sync+ack包(如果端口开启了)或者rst包(如果端口没有开启,或者拒绝连接的话),或者没有任何返回。我们值关注前两种。

其实代码逻辑和上面的ICMP程序差不多太多,获取网关Mac地址等方式和上面而例子没啥区别。我们还是定义了一个Scanner类型,但是多了本地端口和远程端口的信息:

// 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

我们想一次收集可以连通的但是端口未开启的IP地址,和端口已开启的IP地址,所以Scan方法作了微调,支持两个列表的输出:

// 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

发送数据时我们要构造TCP的syn包:

// 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

接收数据的时候,我们区分TCP+ACK包和RST包:

// 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())
// 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")

代码结构和ICMP的探测区别不大。

借助于gopacket (libpcap)库,我们可以轻松高效的实现全网公网IP扫描,这对于网络探测、安全检查都非常有意义。 根据上面的例子,你也很容易实现UDP方式的探测,你也不妨练习下。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK