pax_global_header00006660000000000000000000000064140705166560014524gustar00rootroot0000000000000052 comment=c50f4319d307d95d1e2b344c4504158705e919de arping-1.0.3/000077500000000000000000000000001407051665600130055ustar00rootroot00000000000000arping-1.0.3/.gitignore000066400000000000000000000000071407051665600147720ustar00rootroot00000000000000arping arping-1.0.3/LICENSE000066400000000000000000000021611407051665600140120ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014-2016 j-keck [jhyphenkeck@gmail.com] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. arping-1.0.3/README.md000066400000000000000000000021311407051665600142610ustar00rootroot00000000000000[![pkg.go.dev](https://godoc.org/github.com/j-keck/arping?status.svg)](https://pkg.go.dev/github.com/j-keck/arping) # arping arping is a native go library to ping a host per arp datagram, or query a host mac address The currently supported platforms are: Linux and BSD. ## Usage ### arping library * import this library per `import "github.com/j-keck/arping"` * export GOPATH if not already (`export GOPATH=$PWD`) * download the library `go get` * run it `sudo -E go run ` * or build it `go build` The library requires raw socket access. So it must run as root, or with appropriate capabilities under linux: `sudo setcap cap_net_raw+ep `. For api doc and examples see: [godoc](http://godoc.org/github.com/j-keck/arping) or check the standalone under 'cmd/arping/main.go'. ### arping executable To get a runnable pinger use `go get -u github.com/j-keck/arping/cmd/arping`. This will build the binary in $GOPATH/bin. arping requires raw socket access. So it must run as root, or with appropriate capabilities under Linux: `sudo setcap cap_net_raw+ep `. arping-1.0.3/arp_datagram.go000066400000000000000000000051421407051665600157600ustar00rootroot00000000000000package arping import ( "bytes" "encoding/binary" "net" ) const ( requestOper = 1 responseOper = 2 ) type arpDatagram struct { htype uint16 // Hardware Type ptype uint16 // Protocol Type hlen uint8 // Hardware address Length plen uint8 // Protocol address length oper uint16 // Operation 1->request, 2->response sha []byte // Sender hardware address, length from Hlen spa []byte // Sender protocol address, length from Plen tha []byte // Target hardware address, length from Hlen tpa []byte // Target protocol address, length from Plen } func newArpRequest( srcMac net.HardwareAddr, srcIP net.IP, dstMac net.HardwareAddr, dstIP net.IP) arpDatagram { return arpDatagram{ htype: uint16(1), ptype: uint16(0x0800), hlen: uint8(6), plen: uint8(4), oper: uint16(requestOper), sha: srcMac, spa: srcIP.To4(), tha: dstMac, tpa: dstIP.To4()} } func (datagram arpDatagram) Marshal() []byte { buf := new(bytes.Buffer) binary.Write(buf, binary.BigEndian, datagram.htype) binary.Write(buf, binary.BigEndian, datagram.ptype) binary.Write(buf, binary.BigEndian, datagram.hlen) binary.Write(buf, binary.BigEndian, datagram.plen) binary.Write(buf, binary.BigEndian, datagram.oper) buf.Write(datagram.sha) buf.Write(datagram.spa) buf.Write(datagram.tha) buf.Write(datagram.tpa) return buf.Bytes() } func (datagram arpDatagram) MarshalWithEthernetHeader() []byte { // ethernet frame header var ethernetHeader []byte ethernetHeader = append(ethernetHeader, datagram.tha...) ethernetHeader = append(ethernetHeader, datagram.sha...) ethernetHeader = append(ethernetHeader, []byte{0x08, 0x06}...) // arp return append(ethernetHeader, datagram.Marshal()...) } func (datagram arpDatagram) SenderIP() net.IP { return net.IP(datagram.spa) } func (datagram arpDatagram) SenderMac() net.HardwareAddr { return net.HardwareAddr(datagram.sha) } func (datagram arpDatagram) IsResponseOf(request arpDatagram) bool { return datagram.oper == responseOper && bytes.Equal(request.spa, datagram.tpa) && bytes.Equal(request.tpa, datagram.spa) } func parseArpDatagram(buffer []byte) arpDatagram { var datagram arpDatagram b := bytes.NewBuffer(buffer) binary.Read(b, binary.BigEndian, &datagram.htype) binary.Read(b, binary.BigEndian, &datagram.ptype) binary.Read(b, binary.BigEndian, &datagram.hlen) binary.Read(b, binary.BigEndian, &datagram.plen) binary.Read(b, binary.BigEndian, &datagram.oper) haLen := int(datagram.hlen) paLen := int(datagram.plen) datagram.sha = b.Next(haLen) datagram.spa = b.Next(paLen) datagram.tha = b.Next(haLen) datagram.tpa = b.Next(paLen) return datagram } arping-1.0.3/arping.go000066400000000000000000000134541407051665600146230ustar00rootroot00000000000000// Package arping is a native go library to ping a host per arp datagram, or query a host mac address // // The currently supported platforms are: Linux and BSD. // // // The library requires raw socket access. So it must run as root, or with appropriate capabilities under linux: // `sudo setcap cap_net_raw+ep `. // // // Examples: // // ping a host: // ------------ // package main // import ("fmt"; "github.com/j-keck/arping"; "net") // // func main(){ // dstIP := net.ParseIP("192.168.1.1") // if hwAddr, duration, err := arping.Ping(dstIP); err != nil { // fmt.Println(err) // } else { // fmt.Printf("%s (%s) %d usec\n", dstIP, hwAddr, duration/1000) // } // } // // // resolve mac address: // -------------------- // package main // import ("fmt"; "github.com/j-keck/arping"; "net") // // func main(){ // dstIP := net.ParseIP("192.168.1.1") // if hwAddr, _, err := arping.Ping(dstIP); err != nil { // fmt.Println(err) // } else { // fmt.Printf("%s is at %s\n", dstIP, hwAddr) // } // } // // // check if host is online: // ------------------------ // package main // import ("fmt"; "github.com/j-keck/arping"; "net") // // func main(){ // dstIP := net.ParseIP("192.168.1.1") // _, _, err := arping.Ping(dstIP) // if err == arping.ErrTimeout { // fmt.Println("offline") // }else if err != nil { // fmt.Println(err.Error()) // }else{ // fmt.Println("online") // } // } // package arping import ( "errors" "fmt" "io/ioutil" "log" "net" "os" "time" ) var ( // ErrTimeout error ErrTimeout = errors.New("timeout") verboseLog = log.New(ioutil.Discard, "", 0) timeout = time.Duration(500 * time.Millisecond) ) // Ping sends an arp ping to 'dstIP' func Ping(dstIP net.IP) (net.HardwareAddr, time.Duration, error) { if err := validateIP(dstIP); err != nil { return nil, 0, err } iface, err := findUsableInterfaceForNetwork(dstIP) if err != nil { return nil, 0, err } return PingOverIface(dstIP, *iface) } // PingOverIfaceByName sends an arp ping over interface name 'ifaceName' to 'dstIP' func PingOverIfaceByName(dstIP net.IP, ifaceName string) (net.HardwareAddr, time.Duration, error) { if err := validateIP(dstIP); err != nil { return nil, 0, err } iface, err := net.InterfaceByName(ifaceName) if err != nil { return nil, 0, err } return PingOverIface(dstIP, *iface) } // PingOverIface sends an arp ping over interface 'iface' to 'dstIP' func PingOverIface(dstIP net.IP, iface net.Interface) (net.HardwareAddr, time.Duration, error) { if err := validateIP(dstIP); err != nil { return nil, 0, err } srcMac := iface.HardwareAddr srcIP, err := findIPInNetworkFromIface(dstIP, iface) if err != nil { return nil, 0, err } broadcastMac := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} request := newArpRequest(srcMac, srcIP, broadcastMac, dstIP) sock, err := initialize(iface) if err != nil { return nil, 0, err } defer sock.deinitialize() type PingResult struct { mac net.HardwareAddr duration time.Duration err error } pingResultChan := make(chan PingResult, 1) go func() { // send arp request verboseLog.Printf("arping '%s' over interface: '%s' with address: '%s'\n", dstIP, iface.Name, srcIP) if sendTime, err := sock.send(request); err != nil { pingResultChan <- PingResult{nil, 0, err} } else { for { // receive arp response response, receiveTime, err := sock.receive() if err != nil { pingResultChan <- PingResult{nil, 0, err} return } if response.IsResponseOf(request) { duration := receiveTime.Sub(sendTime) verboseLog.Printf("process received arp: srcIP: '%s', srcMac: '%s'\n", response.SenderIP(), response.SenderMac()) pingResultChan <- PingResult{response.SenderMac(), duration, err} return } verboseLog.Printf("ignore received arp: srcIP: '%s', srcMac: '%s'\n", response.SenderIP(), response.SenderMac()) } } }() select { case pingResult := <-pingResultChan: return pingResult.mac, pingResult.duration, pingResult.err case <-time.After(timeout): sock.deinitialize() return nil, 0, ErrTimeout } } // GratuitousArp sends an gratuitous arp from 'srcIP' func GratuitousArp(srcIP net.IP) error { if err := validateIP(srcIP); err != nil { return err } iface, err := findUsableInterfaceForNetwork(srcIP) if err != nil { return err } return GratuitousArpOverIface(srcIP, *iface) } // GratuitousArpOverIfaceByName sends an gratuitous arp over interface name 'ifaceName' from 'srcIP' func GratuitousArpOverIfaceByName(srcIP net.IP, ifaceName string) error { if err := validateIP(srcIP); err != nil { return err } iface, err := net.InterfaceByName(ifaceName) if err != nil { return err } return GratuitousArpOverIface(srcIP, *iface) } // GratuitousArpOverIface sends an gratuitous arp over interface 'iface' from 'srcIP' func GratuitousArpOverIface(srcIP net.IP, iface net.Interface) error { if err := validateIP(srcIP); err != nil { return err } srcMac := iface.HardwareAddr broadcastMac := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff} request := newArpRequest(srcMac, srcIP, broadcastMac, srcIP) sock, err := initialize(iface) if err != nil { return err } defer sock.deinitialize() verboseLog.Printf("gratuitous arp over interface: '%s' with address: '%s'\n", iface.Name, srcIP) _, err = sock.send(request) return err } // EnableVerboseLog enables verbose logging on stdout func EnableVerboseLog() { verboseLog = log.New(os.Stdout, "", 0) } // SetTimeout sets ping timeout func SetTimeout(t time.Duration) { timeout = t } func validateIP(ip net.IP) error { // ip must be a valid V4 address if len(ip.To4()) != net.IPv4len { return fmt.Errorf("not a valid v4 Address: %s", ip) } return nil } arping-1.0.3/arping_bsd.go000066400000000000000000000050641407051665600154510ustar00rootroot00000000000000// +build darwin freebsd openbsd package arping import ( "errors" "fmt" "net" "os" "runtime" "syscall" "time" ) type BsdSocket struct { bpf *os.File bpfFd int buflen int } var bpfArpFilter = []syscall.BpfInsn{ // make sure this is an arp packet *syscall.BpfStmt(syscall.BPF_LD+syscall.BPF_H+syscall.BPF_ABS, 12), *syscall.BpfJump(syscall.BPF_JMP+syscall.BPF_JEQ+syscall.BPF_K, 0x0806, 0, 1), // if we passed all the tests, ask for the whole packet. *syscall.BpfStmt(syscall.BPF_RET+syscall.BPF_K, -1), // otherwise, drop it. *syscall.BpfStmt(syscall.BPF_RET+syscall.BPF_K, 0), } func initialize(iface net.Interface) (s *BsdSocket, err error) { s = &BsdSocket{} verboseLog.Println("search available /dev/bpfX") for i := 0; i <= 10; i++ { bpfPath := fmt.Sprintf("/dev/bpf%d", i) s.bpf, err = os.OpenFile(bpfPath, os.O_RDWR, 0666) if err != nil { verboseLog.Printf(" open failed: %s - %s\n", bpfPath, err.Error()) } else { verboseLog.Printf(" open success: %s\n", bpfPath) break } } s.bpfFd = int(s.bpf.Fd()) if s.bpfFd == -1 { return s, errors.New("unable to open /dev/bpfX") } if err := syscall.SetBpfInterface(s.bpfFd, iface.Name); err != nil { return s, err } if err := syscall.SetBpfImmediate(s.bpfFd, 1); err != nil { return s, err } s.buflen, err = syscall.BpfBuflen(s.bpfFd) if err != nil { return s, err } if err := syscall.SetBpf(s.bpfFd, bpfArpFilter); err != nil { return s, err } if err := syscall.FlushBpf(s.bpfFd); err != nil { return s, err } return s, nil } func (s *BsdSocket) send(request arpDatagram) (time.Time, error) { _, err := syscall.Write(s.bpfFd, request.MarshalWithEthernetHeader()) return time.Now(), err } func (s *BsdSocket) receive() (arpDatagram, time.Time, error) { buffer := make([]byte, s.buflen) n, err := syscall.Read(s.bpfFd, buffer) if err != nil { return arpDatagram{}, time.Now(), err } // // FreeBSD uses a different bpf header (bh_tstamp differ in it's size) // https://www.freebsd.org/cgi/man.cgi?bpf(4)#BPF_HEADER // var bpfHdrLength int if runtime.GOOS == "freebsd" { bpfHdrLength = 26 } else { bpfHdrLength = 18 } // skip bpf header + 14 bytes ethernet header var hdrLength = bpfHdrLength + 14 if n <= hdrLength || len(buffer) <= hdrLength { // amount of bytes read by socket is less than an ethernet header. clearly not what we look for return arpDatagram{}, time.Now(), fmt.Errorf("buffer with invalid length") } return parseArpDatagram(buffer[hdrLength:n]), time.Now(), nil } func (s *BsdSocket) deinitialize() error { return s.bpf.Close() } arping-1.0.3/arping_linux.go000066400000000000000000000024751407051665600160430ustar00rootroot00000000000000package arping import ( "fmt" "net" "syscall" "time" ) type LinuxSocket struct { sock int toSockaddr syscall.SockaddrLinklayer } func initialize(iface net.Interface) (s *LinuxSocket, err error) { s = &LinuxSocket{} s.toSockaddr = syscall.SockaddrLinklayer{Ifindex: iface.Index} // 1544 = htons(ETH_P_ARP) const proto = 1544 s.sock, err = syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, proto) return s, err } func (s *LinuxSocket) send(request arpDatagram) (time.Time, error) { return time.Now(), syscall.Sendto(s.sock, request.MarshalWithEthernetHeader(), 0, &s.toSockaddr) } func (s *LinuxSocket) receive() (arpDatagram, time.Time, error) { buffer := make([]byte, 128) socketTimeout := timeout.Nanoseconds() * 2 t := syscall.NsecToTimeval(socketTimeout) syscall.SetsockoptTimeval(s.sock, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &t) n, _, err := syscall.Recvfrom(s.sock, buffer, 0) if err != nil { return arpDatagram{}, time.Now(), err } if n <= 14 { // amount of bytes read by socket is less than an ethernet header. clearly not what we look for return arpDatagram{}, time.Now(), fmt.Errorf("buffer with invalid length") } // skip 14 bytes ethernet header return parseArpDatagram(buffer[14:n]), time.Now(), nil } func (s *LinuxSocket) deinitialize() error { return syscall.Close(s.sock) } arping-1.0.3/arping_test.go000066400000000000000000000032561407051665600156610ustar00rootroot00000000000000package arping import ( "net" "runtime" "strings" "testing" "time" ) func TestPingWithInvalidIP(t *testing.T) { ip := net.ParseIP("invalid") _, _, err := Ping(ip) if err == nil { t.Error("error expected") } validateInvalidV4AddrErr(t, err) } func TestPingWithV6IP(t *testing.T) { ip := net.ParseIP("fe80::e2cb:4eff:fed5:ca4e") _, _, err := Ping(ip) if err == nil { t.Error("error expected") } validateInvalidV4AddrErr(t, err) } func TestGratuitousArpWithInvalidIP(t *testing.T) { ip := net.ParseIP("invalid") err := GratuitousArp(ip) if err == nil { t.Error("error expected") } validateInvalidV4AddrErr(t, err) } func TestGratuitousArpWithV6IP(t *testing.T) { ip := net.ParseIP("fe80::e2cb:4eff:fed5:ca4e") err := GratuitousArp(ip) if err == nil { t.Error("error expected") } validateInvalidV4AddrErr(t, err) } func TestGoroutinesDoesNotLeak(t *testing.T) { ip := net.ParseIP("127.0.0.1") SetTimeout(time.Duration(10 * time.Millisecond)) spawnNumGoroutines := 5 for i := 0; i < spawnNumGoroutines; i++ { _, _, err := Ping(ip) if err != ErrTimeout { t.Fatalf("timeout error expected, but not received - received err: %v", err) } } ok := make(chan bool, 1) go func() { for { if runtime.NumGoroutine() < spawnNumGoroutines { ok <- true return } time.Sleep(100 * time.Millisecond) } }() select { case <-ok: // ok case <-time.After(30 * time.Second): t.Fatalf("timeout waiting for goroutine cleanup - num goroutines: %d", runtime.NumGoroutine()) } } func validateInvalidV4AddrErr(t *testing.T, err error) { if !strings.Contains(err.Error(), "not a valid v4 Address") { t.Errorf("unexpected error: %s", err) } } arping-1.0.3/cmd/000077500000000000000000000000001407051665600135505ustar00rootroot00000000000000arping-1.0.3/cmd/arping/000077500000000000000000000000001407051665600150305ustar00rootroot00000000000000arping-1.0.3/cmd/arping/main.go000066400000000000000000000047441407051665600163140ustar00rootroot00000000000000// // command line arping utility which use the 'arping' library // // this utility need raw socket access, please run it // under FreeBSD: as root // under Linux: as root or with 'cap_net_raw' permission: sudo setcap cap_net_raw+ep // // // options: // -h: print help and exit // -v: verbose output // -U: unsolicited/gratuitous ARP mode // -i: interface name to use // -t: timeout - duration with unit - such as 100ms, 500ms, 1s ... // // // exit code: // 0: target online // 1: target offline // 2: error occurred - see command output // package main import ( "flag" "fmt" "github.com/j-keck/arping" "net" "os" "time" ) var ( helpFlag = flag.Bool("h", false, "print help and exit") verboseFlag = flag.Bool("v", false, "verbose output") gratuitousFlag = flag.Bool("U", false, "unsolicited/gratuitous ARP mode") ifaceNameFlag = flag.String("i", "", "interface name to use - autodetected if omitted") timeoutFlag = flag.Duration("t", 500*time.Millisecond, "timeout - such as 100ms, 500ms, 1s ...") ) func main() { flag.Parse() if *helpFlag { printHelpAndExit() } if *verboseFlag { arping.EnableVerboseLog() } arping.SetTimeout(*timeoutFlag) if len(flag.Args()) != 1 { fmt.Println("Parameter missing!") printHelpAndExit() } dstIP := net.ParseIP(flag.Arg(0)) var hwAddr net.HardwareAddr var durationNanos time.Duration var err error if *gratuitousFlag { if len(*ifaceNameFlag) > 0 { err = arping.GratuitousArpOverIfaceByName(dstIP, *ifaceNameFlag) } else { err = arping.GratuitousArp(dstIP) } } else { if len(*ifaceNameFlag) > 0 { hwAddr, durationNanos, err = arping.PingOverIfaceByName(dstIP, *ifaceNameFlag) } else { hwAddr, durationNanos, err = arping.Ping(dstIP) } } // ping timeout if err == arping.ErrTimeout { fmt.Println(err) os.Exit(1) } // ping failed if err != nil { fmt.Println(err) os.Exit(2) } if *gratuitousFlag { os.Exit(0) } // ping success durationMicros := durationNanos / 1000 var durationString string if durationMicros > 1000 { durationString = fmt.Sprintf("%d,%03d", durationMicros/1000, durationMicros%1000) } else { durationString = fmt.Sprintf("%d", durationMicros) } fmt.Printf("%s (%s) %s usec\n", dstIP, hwAddr, durationString) os.Exit(0) } func printHelpAndExit() { fmt.Printf("Usage: %s \n\n", os.Args[0]) flag.PrintDefaults() fmt.Printf("\nExit code:\n 0: target online\n 1: target offline\n 2: error occurred\n") os.Exit(2) } arping-1.0.3/go.mod000066400000000000000000000000511407051665600141070ustar00rootroot00000000000000module github.com/j-keck/arping go 1.12 arping-1.0.3/netutils.go000066400000000000000000000024421407051665600152050ustar00rootroot00000000000000package arping import ( "errors" "fmt" "net" ) func findIPInNetworkFromIface(dstIP net.IP, iface net.Interface) (net.IP, error) { addrs, err := iface.Addrs() if err != nil { return nil, err } for _, a := range addrs { if ipnet, ok := a.(*net.IPNet); ok { if ipnet.Contains(dstIP) { return ipnet.IP, nil } } } return nil, fmt.Errorf("iface: '%s' can't reach ip: '%s'", iface.Name, dstIP) } func findUsableInterfaceForNetwork(dstIP net.IP) (*net.Interface, error) { ifaces, err := net.Interfaces() if err != nil { return nil, err } isDown := func(iface net.Interface) bool { return iface.Flags&1 == 0 } hasAddressInNetwork := func(iface net.Interface) bool { if _, err := findIPInNetworkFromIface(dstIP, iface); err != nil { return false } return true } verboseLog.Println("search usable interface") logIfaceResult := func(msg string, iface net.Interface) { verboseLog.Printf("%10s: %6s %18s %s", msg, iface.Name, iface.HardwareAddr, iface.Flags) } for _, iface := range ifaces { if isDown(iface) { logIfaceResult("DOWN", iface) continue } if !hasAddressInNetwork(iface) { logIfaceResult("OTHER NET", iface) continue } logIfaceResult("USABLE", iface) return &iface, nil } return nil, errors.New("no usable interface found") }