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 }