github.com/google/cloudprober@v0.11.3/probes/ping/ping.go (about)

     1  // Copyright 2017-2021 The Cloudprober Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  /*
    16  Package ping implements a fast ping prober. It sends ICMP pings to a list of
    17  targets and reports statistics on packets sent, received and latency
    18  experienced.
    19  
    20  This ping implementation supports two types of sockets: Raw and datagram ICMP
    21  sockets.
    22  
    23  Raw sockets require root privileges and all the ICMP noise is copied on all raw
    24  sockets opened for ICMP. We have to deal with the unwanted ICMP noise.
    25  
    26  On the other hand, datagram ICMP sockets are unprivileged and implemented in
    27  such a way that kernel copies only relevant packets on them. Kernel assigns a
    28  local port for such sockets and rewrites ICMP id of the outgoing packets to
    29  match that port number. Incoming ICMP packets' ICMP id is matched with the
    30  local port to find the correct destination socket.
    31  
    32  More about these sockets: http://lwn.net/Articles/420800/
    33  Note: On some linux distributions these sockets are not enabled by default; you
    34  can enable them by doing something like the following:
    35        sudo sysctl -w net.ipv4.ping_group_range="0 5000"
    36  */
    37  package ping
    38  
    39  import (
    40  	"context"
    41  	"encoding/binary"
    42  	"errors"
    43  	"fmt"
    44  	"math/rand"
    45  	"net"
    46  	"strconv"
    47  	"strings"
    48  	"sync"
    49  	"time"
    50  
    51  	"github.com/google/cloudprober/logger"
    52  	"github.com/google/cloudprober/metrics"
    53  	"github.com/google/cloudprober/probes/options"
    54  	configpb "github.com/google/cloudprober/probes/ping/proto"
    55  	"github.com/google/cloudprober/targets/endpoint"
    56  	"github.com/google/cloudprober/validators"
    57  	"github.com/google/cloudprober/validators/integrity"
    58  )
    59  
    60  const (
    61  	protocolICMP     = 1
    62  	protocolIPv6ICMP = 58
    63  	dataIntegrityKey = "data-integrity"
    64  	icmpHeaderSize   = 8
    65  	minPacketSize    = icmpHeaderSize + timeBytesSize // 16
    66  	maxPacketSize    = 9001                           // MTU
    67  )
    68  
    69  type result struct {
    70  	sent, rcvd        int64
    71  	latency           metrics.Value
    72  	validationFailure *metrics.Map
    73  }
    74  
    75  // icmpConn is an interface wrapper for *icmp.PacketConn to allow testing.
    76  type icmpConn interface {
    77  	read(buf []byte) (n int, peer net.Addr, recvTime time.Time, err error)
    78  	write(buf []byte, peer net.Addr) (int, error)
    79  	setReadDeadline(deadline time.Time)
    80  	close()
    81  }
    82  
    83  // Probe implements a ping probe type that sends ICMP ping packets to the targets and reports
    84  // back statistics on packets sent, received and the rtt.
    85  type Probe struct {
    86  	name string
    87  	opts *options.Options
    88  	c    *configpb.ProbeConf
    89  	l    *logger.Logger
    90  
    91  	// book-keeping params
    92  	ipVer             int
    93  	targets           []endpoint.Endpoint
    94  	results           map[string]*result
    95  	conn              icmpConn
    96  	runCnt            uint64
    97  	target2addr       map[string]net.Addr
    98  	ip2target         map[[16]byte]string
    99  	useDatagramSocket bool
   100  	statsExportFreq   int // Export frequency
   101  }
   102  
   103  // Init initliazes the probe with the given params.
   104  func (p *Probe) Init(name string, opts *options.Options) error {
   105  	p.name = name
   106  	p.opts = opts
   107  	if err := p.initInternal(); err != nil {
   108  		return err
   109  	}
   110  	return p.listen()
   111  }
   112  
   113  // Helper function to initialize internal data structures, used by tests.
   114  func (p *Probe) initInternal() error {
   115  	c, ok := p.opts.ProbeConf.(*configpb.ProbeConf)
   116  	if !ok {
   117  		return errors.New("no ping config")
   118  	}
   119  	p.c = c
   120  
   121  	if p.l = p.opts.Logger; p.l == nil {
   122  		p.l = &logger.Logger{}
   123  	}
   124  
   125  	if p.c.GetPayloadSize() < timeBytesSize {
   126  		return fmt.Errorf("payload_size (%d) cannot be smaller than %d", p.c.GetPayloadSize(), timeBytesSize)
   127  	}
   128  	if p.c.GetPayloadSize() > maxPacketSize-icmpHeaderSize {
   129  		return fmt.Errorf("payload_size (%d) cannot be bigger than %d", p.c.GetPayloadSize(), maxPacketSize-icmpHeaderSize)
   130  	}
   131  
   132  	if err := p.configureIntegrityCheck(); err != nil {
   133  		return err
   134  	}
   135  
   136  	p.statsExportFreq = int(p.opts.StatsExportInterval.Nanoseconds() / p.opts.Interval.Nanoseconds())
   137  	if p.statsExportFreq == 0 {
   138  		p.statsExportFreq = 1
   139  	}
   140  
   141  	// Unlike other probes, for ping probe, we need to know the IP version to
   142  	// craft appropriate ICMP packets. We default to IPv4.
   143  	p.ipVer = 4
   144  	if p.opts.IPVersion != 0 {
   145  		p.ipVer = p.opts.IPVersion
   146  	}
   147  
   148  	p.results = make(map[string]*result)
   149  	p.ip2target = make(map[[16]byte]string)
   150  	p.target2addr = make(map[string]net.Addr)
   151  	p.useDatagramSocket = p.c.GetUseDatagramSocket()
   152  
   153  	// Update targets run peiodically as well.
   154  	p.updateTargets()
   155  
   156  	return nil
   157  }
   158  
   159  // Adds an integrity validator if data integrity checks are not disabled.
   160  func (p *Probe) configureIntegrityCheck() error {
   161  	if p.c.GetDisableIntegrityCheck() {
   162  		return nil
   163  	}
   164  
   165  	for _, v := range p.opts.Validators {
   166  		if v.Name == dataIntegrityKey {
   167  			p.l.Warningf("Not adding data-integrity validator as there is already a validator with the name \"%s\": %v", dataIntegrityKey, v)
   168  			return nil
   169  		}
   170  	}
   171  
   172  	iv, err := integrity.PatternNumBytesValidator(timeBytesSize, p.l)
   173  	if err != nil {
   174  		return err
   175  	}
   176  
   177  	v := &validators.Validator{
   178  		Name:     dataIntegrityKey,
   179  		Validate: func(input *validators.Input) (bool, error) { return iv.Validate(input.ResponseBody) },
   180  	}
   181  
   182  	p.opts.Validators = append(p.opts.Validators, v)
   183  
   184  	return nil
   185  }
   186  
   187  func (p *Probe) listen() error {
   188  	var err error
   189  	p.conn, err = newICMPConn(p.opts.SourceIP, p.ipVer, p.useDatagramSocket)
   190  	return err
   191  }
   192  
   193  func (p *Probe) updateTargets() {
   194  	p.targets = p.opts.Targets.ListEndpoints()
   195  
   196  	for _, target := range p.targets {
   197  		for _, al := range p.opts.AdditionalLabels {
   198  			al.UpdateForTarget(target)
   199  		}
   200  
   201  		// Update results map:
   202  		p.updateResultForTarget(target.Name)
   203  
   204  		ip, err := p.opts.Targets.Resolve(target.Name, p.ipVer)
   205  		if err != nil {
   206  			p.l.Warning("Bad target: ", target.Name, ". Err: ", err.Error())
   207  			p.target2addr[target.Name] = nil
   208  			continue
   209  		}
   210  
   211  		var a net.Addr
   212  		if p.useDatagramSocket {
   213  			a = &net.UDPAddr{IP: ip}
   214  		} else {
   215  			a = &net.IPAddr{IP: ip}
   216  		}
   217  		p.target2addr[target.Name] = a
   218  		p.ip2target[ipToKey(ip)] = target.Name
   219  	}
   220  }
   221  
   222  func (p *Probe) updateResultForTarget(t string) {
   223  	if _, ok := p.results[t]; ok {
   224  		return
   225  	}
   226  
   227  	var latencyValue metrics.Value
   228  	if p.opts.LatencyDist != nil {
   229  		latencyValue = p.opts.LatencyDist.Clone()
   230  	} else {
   231  		latencyValue = metrics.NewFloat(0)
   232  	}
   233  
   234  	p.results[t] = &result{
   235  		latency:           latencyValue,
   236  		validationFailure: validators.ValidationFailureMap(p.opts.Validators),
   237  	}
   238  }
   239  
   240  // Match first 8-bits of the run ID with the first 8-bits of the ICMP sequence number.
   241  // For raw sockets we also match ICMP id with the run ID. For datagram sockets, kernel
   242  // does that matching for us. It rewrites the ICMP id of the outgoing packets with the
   243  // fake local port selected at the time of the socket creation and uses the same criteria
   244  // to forward incoming packets to the sockets.
   245  func matchPacket(runID, pktID, pktSeq uint16, datagramSocket bool) bool {
   246  	return runID>>8 == pktSeq>>8 && (datagramSocket || pktID == runID)
   247  }
   248  
   249  func (p *Probe) sendPackets(runID uint16, tracker chan bool) {
   250  	seq := runID & uint16(0xff00)
   251  	packetsSent := int32(0)
   252  
   253  	// Allocate a byte buffer of the size: ICMP Header Size (8) + Payload Size
   254  	// We re-use the same memory space for all outgoing packets.
   255  	pktbuf := make([]byte, icmpHeaderSize+p.c.GetPayloadSize())
   256  
   257  	for {
   258  		for _, target := range p.targets {
   259  			if p.target2addr[target.Name] == nil {
   260  				p.l.Debug("Skipping unresolved target: ", target.Name)
   261  				continue
   262  			}
   263  			p.prepareRequestPacket(pktbuf, runID, seq, time.Now().UnixNano())
   264  			if _, err := p.conn.write(pktbuf, p.target2addr[target.Name]); err != nil {
   265  				p.l.Warning(err.Error())
   266  				continue
   267  			}
   268  			tracker <- true
   269  			p.results[target.Name].sent++
   270  		}
   271  
   272  		packetsSent++
   273  		if packetsSent >= p.c.GetPacketsPerProbe() {
   274  			break
   275  		}
   276  		seq++
   277  		time.Sleep(time.Duration(p.c.GetPacketsIntervalMsec()) * time.Millisecond)
   278  	}
   279  	close(tracker)
   280  }
   281  
   282  type rcvdPkt struct {
   283  	id, seq uint16
   284  	data    []byte
   285  	tsUnix  int64
   286  	target  string
   287  }
   288  
   289  // We use it to keep track of received packets in recvPackets.
   290  type packetKey struct {
   291  	target string
   292  	seqNo  uint16
   293  }
   294  
   295  func (p *Probe) recvPackets(runID uint16, tracker chan bool) {
   296  	// Number of expected packets: p.c.GetPacketsPerProbe() * len(p.targets)
   297  	received := make(map[packetKey]bool, int(p.c.GetPacketsPerProbe())*len(p.targets))
   298  	outstandingPkts := 0
   299  	p.conn.setReadDeadline(time.Now().Add(p.opts.Timeout))
   300  	pktbuf := make([]byte, maxPacketSize)
   301  	for {
   302  		// To make sure that we have picked up all the packets sent by the sender, we
   303  		// use a tracker channel. Whenever sender successfully sends a packet, it notifies
   304  		// on the tracker channel. Within receiver we keep a count of outstanding packets
   305  		// and we increment this count  whenever there is an element on tracker channel.
   306  		//
   307  		// First run or we have received all the known packets so far. Check the tracker
   308  		// to see if there is another packet to pick up.
   309  		if outstandingPkts == 0 {
   310  			if _, ok := <-tracker; ok {
   311  				outstandingPkts++
   312  			} else {
   313  				// tracker has been closed and there are no outstanding packets.
   314  				return
   315  			}
   316  		}
   317  
   318  		// Read packet from the socket
   319  		pktLen, peer, recvTime, err := p.conn.read(pktbuf)
   320  
   321  		if err != nil {
   322  			p.l.Warning(err.Error())
   323  			// if it's a timeout, return immediately.
   324  			if neterr, ok := err.(*net.OpError); ok && neterr.Timeout() {
   325  				return
   326  			}
   327  			continue
   328  		}
   329  		if pktLen < minPacketSize {
   330  			p.l.Warning("packet too small: size (", strconv.FormatInt(int64(pktLen), 10), ") < minPacketSize (16), from peer: ", peer.String())
   331  			continue
   332  		}
   333  
   334  		// recvTime should never be zero:
   335  		// -- On Unix systems, recvTime comes from the sockets.
   336  		// -- On Non-Unix systems, read() call returns recvTime based on when
   337  		//    packet was received by cloudprober.
   338  		if recvTime.IsZero() {
   339  			p.l.Info("didn't get fetch time from the connection (SO_TIMESTAMP), using current time")
   340  			recvTime = time.Now()
   341  		}
   342  
   343  		var ip net.IP
   344  		if p.useDatagramSocket {
   345  			ip = peer.(*net.UDPAddr).IP
   346  		} else {
   347  			ip = peer.(*net.IPAddr).IP
   348  		}
   349  		target := p.ip2target[ipToKey(ip)]
   350  		if target == "" {
   351  			p.l.Debug("Got a packet from a peer that's not one of my targets: ", peer.String())
   352  			continue
   353  		}
   354  
   355  		if !validEchoReply(p.ipVer, pktbuf[0]) {
   356  			p.l.Warning("Not a valid ICMP echo reply packet from: ", target)
   357  			continue
   358  		}
   359  
   360  		var pkt = rcvdPkt{
   361  			tsUnix: recvTime.UnixNano(),
   362  			target: target,
   363  			// ICMP packet body starts from the 5th byte
   364  			id:   binary.BigEndian.Uint16(pktbuf[4:6]),
   365  			seq:  binary.BigEndian.Uint16(pktbuf[6:8]),
   366  			data: pktbuf[8:pktLen],
   367  		}
   368  
   369  		rtt := time.Duration(pkt.tsUnix-bytesToTime(pkt.data)) * time.Nanosecond
   370  
   371  		// check if this packet belongs to this run
   372  		if !matchPacket(runID, pkt.id, pkt.seq, p.useDatagramSocket) {
   373  			p.l.Info("Reply ", pkt.String(rtt), " Unmatched packet, probably from the last probe run.")
   374  			continue
   375  		}
   376  
   377  		key := packetKey{pkt.target, pkt.seq}
   378  		// Check if we have already seen this packet.
   379  		if received[key] {
   380  			p.l.Info("Duplicate reply ", pkt.String(rtt), " (DUP)")
   381  			continue
   382  		}
   383  		received[key] = true
   384  
   385  		// Read a "good" packet, where good means that it's one of the replies that
   386  		// we were looking for.
   387  		outstandingPkts--
   388  
   389  		// Update probe result
   390  		result := p.results[pkt.target]
   391  
   392  		if p.opts.Validators != nil {
   393  			failedValidations := validators.RunValidators(p.opts.Validators, &validators.Input{ResponseBody: pkt.data}, result.validationFailure, p.l)
   394  
   395  			// If any validation failed, return now, leaving the success and latency
   396  			// counters unchanged.
   397  			if len(failedValidations) > 0 {
   398  				p.l.Debug("Target:", pkt.target, " ping.recvPackets: failed validations: ", strings.Join(failedValidations, ","), ".")
   399  				continue
   400  			}
   401  		}
   402  
   403  		result.rcvd++
   404  		result.latency.AddFloat64(rtt.Seconds() / p.opts.LatencyUnit.Seconds())
   405  	}
   406  }
   407  
   408  // Probe run ID is eventually used to identify packets of a particular probe run. To avoid
   409  // assigning packets to the wrong probe run, it's important that we pick run id carefully:
   410  //
   411  // We use first 8 bits of the ICMP sequence number to match packets belonging to a probe
   412  // run (rest of the 8-bits are used for actual sequencing of the packets). These first 8-bits
   413  // of the sequence number are derived from the first 8-bits of the run ID. To make sure that
   414  // nearby probe runs' sequence numbers don't have the same first 8-bits, we derive these bits
   415  // from a running counter.
   416  //
   417  // For raw sockets, we have to also make sure (reduce the probablity) that two different probes
   418  // (not just probe runs) don't get the same ICMP id (for datagram sockets that's not an issue as
   419  // ICMP id is assigned by the kernel and kernel makes sure its unique). To achieve that we
   420  // randomize the last 8-bits of the run ID and use run ID as ICMP id.
   421  func (p *Probe) newRunID() uint16 {
   422  	return uint16(p.runCnt)<<8 + uint16(rand.Intn(0x00ff))
   423  }
   424  
   425  // runProbe is called by Run for each probe interval. It does the following on
   426  // each run:
   427  //   * Resolve targets if target resolve interval has elapsed.
   428  //   * Increment run count (runCnt).
   429  //   * Get a new run ID.
   430  //   * Starts a goroutine to receive packets.
   431  //   * Send packets.
   432  func (p *Probe) runProbe() {
   433  	// Resolve targets if target resolve interval has elapsed.
   434  	if (p.runCnt % uint64(p.c.GetResolveTargetsInterval())) == 0 {
   435  		p.updateTargets()
   436  	}
   437  	p.runCnt++
   438  	runID := p.newRunID()
   439  	wg := new(sync.WaitGroup)
   440  	tracker := make(chan bool, int(p.c.GetPacketsPerProbe())*len(p.targets))
   441  	wg.Add(1)
   442  	go func() {
   443  		defer wg.Done()
   444  		p.recvPackets(runID, tracker)
   445  	}()
   446  	p.sendPackets(runID, tracker)
   447  	wg.Wait()
   448  }
   449  
   450  // Start starts the probe and writes back the data on the provided channel.
   451  // Probe should have been initialized with Init() before calling Start on it.
   452  func (p *Probe) Start(ctx context.Context, dataChan chan *metrics.EventMetrics) {
   453  	if p.conn == nil {
   454  		p.l.Critical("Probe has not been properly initialized yet.")
   455  	}
   456  	defer p.conn.close()
   457  
   458  	ticker := time.NewTicker(p.opts.Interval)
   459  	defer ticker.Stop()
   460  
   461  	for ts := range ticker.C {
   462  		// Don't run another probe if context is canceled already.
   463  		select {
   464  		case <-ctx.Done():
   465  			return
   466  		default:
   467  		}
   468  
   469  		p.runProbe()
   470  		p.l.Debugf("%s: Probe finished.", p.name)
   471  		if (p.runCnt % uint64(p.statsExportFreq)) != 0 {
   472  			continue
   473  		}
   474  		for _, target := range p.targets {
   475  			result := p.results[target.Name]
   476  			em := metrics.NewEventMetrics(ts).
   477  				AddMetric("total", metrics.NewInt(result.sent)).
   478  				AddMetric("success", metrics.NewInt(result.rcvd)).
   479  				AddMetric("latency", result.latency).
   480  				AddLabel("ptype", "ping").
   481  				AddLabel("probe", p.name).
   482  				AddLabel("dst", target.Name)
   483  
   484  			em.LatencyUnit = p.opts.LatencyUnit
   485  
   486  			for _, al := range p.opts.AdditionalLabels {
   487  				em.AddLabel(al.KeyValueForTarget(target.Name))
   488  			}
   489  
   490  			if p.opts.Validators != nil {
   491  				em.AddMetric("validation_failure", result.validationFailure)
   492  			}
   493  
   494  			p.opts.LogMetrics(em)
   495  			dataChan <- em
   496  		}
   497  	}
   498  }