diff --git a/README.md b/README.md index f2e1198..235baf6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,61 @@ # go-ping -ICMP Ping library for Go +[![GoDoc](https://godoc.org/github.com/sparrc/go-ping?status.svg)](https://godoc.org/github.com/sparrc/go-ping) + +ICMP Ping library for Go, inspired by +[go-fastping](https://github.com/tatsushid/go-fastping) + +Here is a very simple example that sends & receives 3 packets: + +```go + pinger, err := ping.NewPinger("www.google.com") + if err != nil { + panic(err) + } + pinger.Count = 3 + pinger.Run() // blocks until finished + stats := pinger.Statistics() // get send/receive/rtt stats +``` + +Here is an example that emulates the unix ping command: + +```go + pinger, err := ping.NewPinger("www.google.com") + if err != nil { + fmt.Printf("ERROR: %s\n", err.Error()) + return + } + pinger.OnRecv = func(pkt *ping.Packet) { + fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v\n", + pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt) + } + pinger.OnFinish = func(stats *ping.Statistics) { + fmt.Printf("\n--- %s ping statistics ---\n", stats.Addr) + fmt.Printf("%d packets transmitted, %d packets received, %v%% packet loss\n", + stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss) + fmt.Printf("round-trip min/avg/max/stddev = %v/%v/%v/%v\n", + stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt) + } + fmt.Printf("PING %s (%s):\n", pinger.Addr(), pinger.IPAddr()) + pinger.Run() +``` + +It sends ICMP packet(s) and waits for a response. If it receives a response, +it calls the "receive" callback. When it's finished, it calls the "finish" +callback. + +For a full ping example, see "cmd/ping/ping.go". + +## Note on Linux Support: + +This library attempts to send an +"unprivileged" ping via UDP. On linux, this must be enabled by setting + +``` +sysctl net.ipv4.ping_group_range=0 +``` + +If you do not wish to do this, you can set `pinger.SetPrivileged(true)` and +run as super-user. + +See [this blog](https://sturmflut.github.io/linux/ubuntu/2015/01/17/unprivileged-icmp-sockets-on-linux/) +and [the Go icmp library](https://godoc.org/golang.org/x/net/icmp) for more details. diff --git a/cmd/ping/ping.go b/cmd/ping/ping.go new file mode 100644 index 0000000..a26c044 --- /dev/null +++ b/cmd/ping/ping.go @@ -0,0 +1,72 @@ +package main + +import ( + "flag" + "fmt" + "time" + + "github.com/sparrc/go-ping" +) + +var usage = ` +Usage: + + ping [-c count] [-i interval] [-t timeout] host + +Examples: + + # ping google continuously + ping www.google.com + + # ping google 5 times + ping -c 5 www.google.com + + # ping google 5 times at 500ms intervals + ping -c 5 -i 500ms www.google.com + + # ping google for 10 seconds + ping -t 10s www.google.com +` + +func main() { + timeout := flag.Duration("t", time.Second*100000, "") + interval := flag.Duration("i", time.Second, "") + count := flag.Int("c", -1, "") + privileged := flag.Bool("privileged", false, "") + flag.Usage = func() { + fmt.Printf(usage) + } + flag.Parse() + + if flag.NArg() == 0 { + flag.Usage() + return + } + + host := flag.Arg(0) + pinger, err := ping.NewPinger(host) + if err != nil { + fmt.Printf("ERROR: %s\n", err.Error()) + return + } + + pinger.OnRecv = func(pkt *ping.Packet) { + fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v\n", + pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt) + } + pinger.OnFinish = func(stats *ping.Statistics) { + fmt.Printf("\n--- %s ping statistics ---\n", stats.Addr) + fmt.Printf("%d packets transmitted, %d packets received, %v%% packet loss\n", + stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss) + fmt.Printf("round-trip min/avg/max/stddev = %v/%v/%v/%v\n", + stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt) + } + + pinger.Count = *count + pinger.Interval = *interval + pinger.Timeout = *timeout + pinger.SetPrivileged(*privileged) + + fmt.Printf("PING %s (%s):\n", pinger.Addr(), pinger.IPAddr()) + pinger.Run() +} diff --git a/ping b/ping new file mode 100755 index 0000000..2866831 Binary files /dev/null and b/ping differ diff --git a/ping.go b/ping.go new file mode 100644 index 0000000..a138183 --- /dev/null +++ b/ping.go @@ -0,0 +1,540 @@ +// Package ping is an ICMP ping library seeking to emulate the unix "ping" +// command. +// +// Here is a very simple example that sends & receives 3 packets: +// +// pinger, err := ping.NewPinger("www.google.com") +// if err != nil { +// panic(err) +// } +// +// pinger.Count = 3 +// pinger.Run() // blocks until finished +// stats := pinger.Statistics() // get send/receive/rtt stats +// +// Here is an example that emulates the unix ping command: +// +// pinger, err := ping.NewPinger("www.google.com") +// if err != nil { +// fmt.Printf("ERROR: %s\n", err.Error()) +// return +// } +// +// pinger.OnRecv = func(pkt *ping.Packet) { +// fmt.Printf("%d bytes from %s: icmp_seq=%d time=%v\n", +// pkt.Nbytes, pkt.IPAddr, pkt.Seq, pkt.Rtt) +// } +// pinger.OnFinish = func(stats *ping.Statistics) { +// fmt.Printf("\n--- %s ping statistics ---\n", stats.Addr) +// fmt.Printf("%d packets transmitted, %d packets received, %v%% packet loss\n", +// stats.PacketsSent, stats.PacketsRecv, stats.PacketLoss) +// fmt.Printf("round-trip min/avg/max/stddev = %v/%v/%v/%v\n", +// stats.MinRtt, stats.AvgRtt, stats.MaxRtt, stats.StdDevRtt) +// } +// +// fmt.Printf("PING %s (%s):\n", pinger.Addr(), pinger.IPAddr()) +// pinger.Run() +// +// It sends ICMP packet(s) and waits for a response. If it receives a response, +// it calls the "receive" callback. When it's finished, it calls the "finish" +// callback. +// +// For a full ping example, see "cmd/ping/ping.go". +// +package ping + +import ( + "fmt" + "math" + "math/rand" + "net" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +const ( + timeSliceLength = 8 + protocolICMP = 1 + protocolIPv6ICMP = 58 +) + +var ( + ipv4Proto = map[string]string{"ip": "ip4:icmp", "udp": "udp4"} + ipv6Proto = map[string]string{"ip": "ip6:ipv6-icmp", "udp": "udp6"} +) + +// NewPinger returns a new Pinger struct pointer +func NewPinger(addr string) (*Pinger, error) { + ipaddr, err := net.ResolveIPAddr("ip", addr) + if err != nil { + return nil, err + } + + var ipv4 bool + if isIPv4(ipaddr.IP) { + ipv4 = true + } else if isIPv6(ipaddr.IP) { + ipv4 = false + } + + return &Pinger{ + ipaddr: ipaddr, + addr: addr, + Interval: time.Second, + Timeout: time.Second * 100000, + Count: -1, + + network: "udp", + ipv4: ipv4, + size: timeSliceLength, + + done: make(chan bool), + }, nil +} + +// Pinger represents ICMP packet sender/receiver +type Pinger struct { + // Interval is the wait time between each packet send. Default is 1s. + Interval time.Duration + + // Timeout specifies a timeout before ping exits, regardless of how many + // packets have been received. + Timeout time.Duration + + // Count tells pinger to stop after sending (and receiving) Count echo + // packets. If this option is not specified, pinger will operate until + // interrupted. + Count int + + // Debug runs in debug mode + Debug bool + + // Number of packets sent + PacketsSent int + + // Number of packets received + PacketsRecv int + + // rtts is all of the Rtts + rtts []time.Duration + + // OnRecv is called when Pinger receives and processes a packet + OnRecv func(*Packet) + + // OnFinish is called when Pinger exits + OnFinish func(*Statistics) + + // stop chan bool + done chan bool + + ipaddr *net.IPAddr + addr string + + ipv4 bool + source string + size int + sequence int + network string +} + +type packet struct { + bytes []byte + nbytes int +} + +// Packet represents a received and processed ICMP echo packet. +type Packet struct { + // Rtt is the round-trip time it took to ping. + Rtt time.Duration + + // IPAddr is the address of the host being pinged. + IPAddr *net.IPAddr + + // NBytes is the number of bytes in the message. + Nbytes int + + // Seq is the ICMP sequence number. + Seq int +} + +// Statistics represent the stats of a currently running or finished +// pinger operation. +type Statistics struct { + // PacketsRecv is the number of packets received. + PacketsRecv int + + // PacketsSent is the number of packets sent. + PacketsSent int + + // PacketLoss is the percentage of packets lost. + PacketLoss float64 + + // IPAddr is the address of the host being pinged. + IPAddr *net.IPAddr + + // Addr is the string address of the host being pinged. + Addr string + + // Rtts is all of the round-trip times sent via this pinger. + Rtts []time.Duration + + // MinRtt is the minimum round-trip time sent via this pinger. + MinRtt time.Duration + + // MaxRtt is the maximum round-trip time sent via this pinger. + MaxRtt time.Duration + + // AvgRtt is the average round-trip time sent via this pinger. + AvgRtt time.Duration + + // StdDevRtt is the standard deviation of the round-trip times sent via + // this pinger. + StdDevRtt time.Duration +} + +// SetIPAddr sets the ip address of the target host. +func (p *Pinger) SetIPAddr(ipaddr *net.IPAddr) { + p.ipaddr = ipaddr + p.addr = ipaddr.String() +} + +// IPAddr returns the ip address of the target host. +func (p *Pinger) IPAddr() *net.IPAddr { + return p.ipaddr +} + +// SetAddr resolves and sets the ip address of the target host, addr can be a +// DNS name like "www.google.com" or IP like "127.0.0.1". +func (p *Pinger) SetAddr(addr string) error { + ipaddr, err := net.ResolveIPAddr("ip4:icmp", addr) + if err != nil { + return err + } + p.addr = addr + p.ipaddr = ipaddr + return nil +} + +// Addr returns the string ip address of the target host. +func (p *Pinger) Addr() string { + return p.addr +} + +// SetPrivileged sets the type of ping pinger will send. +// false means pinger will send an "unprivileged" UDP ping. +// true means pinger will send a "privileged" raw ICMP ping. +// NOTE: setting to true requires that it be run with super-user privileges. +func (p *Pinger) SetPrivileged(privileged bool) { + if privileged { + p.network = "ip" + } else { + p.network = "udp" + } +} + +// Privileged returns whether pinger is running in privileged mode. +func (p *Pinger) Privileged() bool { + return p.network == "ip" +} + +// Run runs the pinger. This is a blocking function that will exit when it's +// done. If Count or Interval are not specified, it will run continuously until +// it is interrupted. +func (p *Pinger) Run() { + p.run() +} + +func (p *Pinger) run() { + var conn *icmp.PacketConn + if p.ipv4 { + if conn = p.listen(ipv4Proto[p.network], p.source); conn == nil { + return + } + } else { + if conn = p.listen(ipv6Proto[p.network], p.source); conn == nil { + return + } + } + defer conn.Close() + defer p.finish() + + var wg sync.WaitGroup + recv := make(chan *packet, 5) + wg.Add(1) + go p.recvICMP(conn, recv, &wg) + + err := p.sendICMP(conn) + if err != nil { + fmt.Println(err.Error()) + } + + timeout := time.NewTicker(p.Timeout) + interval := time.NewTicker(p.Interval) + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + signal.Notify(c, syscall.SIGTERM) + + for { + select { + case <-c: + close(p.done) + case <-p.done: + wg.Wait() + return + case <-timeout.C: + close(p.done) + wg.Wait() + return + case <-interval.C: + err = p.sendICMP(conn) + if err != nil { + fmt.Println("FATAL: ", err.Error()) + } + case r := <-recv: + err := p.processPacket(r) + if err != nil { + fmt.Println("FATAL: ", err.Error()) + } + default: + if p.Count > 0 && p.PacketsRecv >= p.Count { + close(p.done) + wg.Wait() + return + } + } + } +} + +func (p *Pinger) finish() { + handler := p.OnFinish + if handler != nil { + s := p.Statistics() + handler(s) + } +} + +// Statistics returns the statistics of the pinger. This can be run while the +// pinger is running or after it is finished. OnFinish calls this function to +// get it's finished statistics. +func (p *Pinger) Statistics() *Statistics { + loss := float64(p.PacketsSent-p.PacketsRecv) / float64(p.PacketsSent) * 100 + var min, max, total time.Duration + if len(p.rtts) > 0 { + min = p.rtts[0] + max = p.rtts[0] + } + for _, rtt := range p.rtts { + if rtt < min { + min = rtt + } + if rtt > max { + max = rtt + } + total += rtt + } + s := Statistics{ + PacketsSent: p.PacketsSent, + PacketsRecv: p.PacketsRecv, + PacketLoss: loss, + Rtts: p.rtts, + Addr: p.addr, + IPAddr: p.ipaddr, + MaxRtt: max, + MinRtt: min, + } + if len(p.rtts) > 0 { + s.AvgRtt = total / time.Duration(len(p.rtts)) + var sumsquares time.Duration + for _, rtt := range p.rtts { + sumsquares += (rtt - s.AvgRtt) * (rtt - s.AvgRtt) + } + s.StdDevRtt = time.Duration(math.Sqrt( + float64(sumsquares / time.Duration(len(p.rtts))))) + } + return &s +} + +func (p *Pinger) recvICMP( + conn *icmp.PacketConn, + recv chan<- *packet, + wg *sync.WaitGroup, +) { + defer wg.Done() + for { + select { + case <-p.done: + return + default: + bytes := make([]byte, 512) + conn.SetReadDeadline(time.Now().Add(time.Millisecond * 100)) + n, _, err := conn.ReadFrom(bytes) + if err != nil { + if neterr, ok := err.(*net.OpError); ok { + if neterr.Timeout() { + // Read timeout + continue + } else { + close(p.done) + return + } + } + } + + recv <- &packet{bytes: bytes, nbytes: n} + } + } +} + +func (p *Pinger) processPacket(recv *packet) error { + var bytes []byte + var proto int + if p.ipv4 { + if p.network == "ip" { + bytes = ipv4Payload(recv.bytes) + } else { + bytes = recv.bytes + } + proto = protocolICMP + } else { + bytes = recv.bytes + proto = protocolIPv6ICMP + } + + var m *icmp.Message + var err error + if m, err = icmp.ParseMessage(proto, bytes[:recv.nbytes]); err != nil { + return fmt.Errorf("Error parsing icmp message") + } + + if m.Type != ipv4.ICMPTypeEchoReply && m.Type != ipv6.ICMPTypeEchoReply { + // Not an echo reply, ignore it + return nil + } + + outPkt := &Packet{ + Nbytes: recv.nbytes, + IPAddr: p.ipaddr, + } + + switch pkt := m.Body.(type) { + case *icmp.Echo: + outPkt.Rtt = time.Since(bytesToTime(pkt.Data[:timeSliceLength])) + outPkt.Seq = pkt.Seq + p.PacketsRecv += 1 + default: + // Very bad, not sure how this can happen + return fmt.Errorf("Error, invalid ICMP echo reply. Body type: %T, %s", + pkt, pkt) + } + + p.rtts = append(p.rtts, outPkt.Rtt) + handler := p.OnRecv + if handler != nil { + handler(outPkt) + } + + return nil +} + +func (p *Pinger) sendICMP(conn *icmp.PacketConn) error { + var typ icmp.Type + if p.ipv4 { + typ = ipv4.ICMPTypeEcho + } else { + typ = ipv6.ICMPTypeEchoRequest + } + + var dst net.Addr = p.ipaddr + if p.network == "udp" { + dst = &net.UDPAddr{IP: p.ipaddr.IP, Zone: p.ipaddr.Zone} + } + + t := timeToBytes(time.Now()) + if p.size-timeSliceLength != 0 { + t = append(t, byteSliceOfSize(p.size-timeSliceLength)...) + } + bytes, err := (&icmp.Message{ + Type: typ, Code: 0, + Body: &icmp.Echo{ + ID: rand.Intn(65535), + Seq: p.sequence, + Data: t, + }, + }).Marshal(nil) + if err != nil { + return err + } + + for { + if _, err := conn.WriteTo(bytes, dst); err != nil { + if neterr, ok := err.(*net.OpError); ok { + if neterr.Err == syscall.ENOBUFS { + continue + } + } + } + p.PacketsSent += 1 + p.sequence += 1 + break + } + return nil +} + +func (p *Pinger) listen(netProto string, source string) *icmp.PacketConn { + conn, err := icmp.ListenPacket(netProto, source) + if err != nil { + fmt.Printf("Error listening for ICMP packets: %s\n", err.Error()) + close(p.done) + return nil + } + return conn +} + +func byteSliceOfSize(n int) []byte { + b := make([]byte, n) + for i := 0; i < len(b); i++ { + b[i] = 1 + } + + return b +} + +func ipv4Payload(b []byte) []byte { + if len(b) < ipv4.HeaderLen { + return b + } + hdrlen := int(b[0]&0x0f) << 2 + return b[hdrlen:] +} + +func bytesToTime(b []byte) time.Time { + var nsec int64 + for i := uint8(0); i < 8; i++ { + nsec += int64(b[i]) << ((7 - i) * 8) + } + return time.Unix(nsec/1000000000, nsec%1000000000) +} + +func isIPv4(ip net.IP) bool { + return len(ip.To4()) == net.IPv4len +} + +func isIPv6(ip net.IP) bool { + return len(ip) == net.IPv6len +} + +func timeToBytes(t time.Time) []byte { + nsec := t.UnixNano() + b := make([]byte, 8) + for i := uint8(0); i < 8; i++ { + b[i] = byte((nsec >> ((7 - i) * 8)) & 0xff) + } + return b +}