Developing lightweight computation at the DSG edge

Commit 77403bd6 authored by Cameron Sparr's avatar Cameron Sparr
Browse files

First commit of go-ping library

parent 5b436d02
# 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.
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()
}
File added
// 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
}