Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,23 @@ Some sample programs are provided in `cmd/`:

- [x] IPv4 and IPv6 support
- [x] Unicast and multicast support
- [x] Privileged and unprivileged (UDP) support
- [x] configurable retry amount and timeout duration
- [x] configurable payload size (and content)
- [x] round trip time measurement

## Usage

For privileged ICMP (requires root or CAP_NET_RAW):
```go
pinger, err := ping.New("0.0.0.0", "::")
```

For unprivileged ICMP (works on Linux and Darwin if allowed):
```go
pinger, err := ping.NewUDP("0.0.0.0", "::")
```

## Contribute

Simply fork and create a pull-request. We'll try to respond in a timely
Expand Down
28 changes: 26 additions & 2 deletions pinger.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,25 @@ type Pinger struct {
// New creates a new Pinger. This will open the raw socket and start the
// receiving logic. You'll need to call Close() to cleanup.
func New(bind4, bind6 string) (*Pinger, error) {
return NewPinger("ip4:icmp", bind4, "ip6:ipv6-icmp", bind6)
}

// NewUDP creates a new Pinger using unprivileged UDP sockets.
// Currently only Darwin and Linux support this.
func NewUDP(bind4, bind6 string) (*Pinger, error) {
return NewPinger("udp4", bind4, "udp6", bind6)
}

// NewPinger creates a new Pinger with the given network types and bind addresses.
// For non-privileged ICMP, use "udp4" and "udp6".
func NewPinger(network4, bind4, network6, bind6 string) (*Pinger, error) {
// open sockets
conn4, err := connectICMP("ip4:icmp", bind4)
conn4, err := connectICMP(network4, bind4)
if err != nil {
return nil, err
}

conn6, err := connectICMP("ip6:ipv6-icmp", bind6)
conn6, err := connectICMP(network6, bind6)
if err != nil {
if conn4 != nil {
conn4.Close()
Expand All @@ -67,6 +79,18 @@ func New(bind4, bind6 string) (*Pinger, error) {
SequenceCounter: &sequence,
requests: make(map[uint32]request),
}

// For unprivileged sockets, we must use the port number as ID
if conn4 != nil {
if addr, ok := conn4.LocalAddr().(*net.UDPAddr); ok {
pinger.Id = uint16(addr.Port)
}
} else if conn6 != nil {
if addr, ok := conn6.LocalAddr().(*net.UDPAddr); ok {
pinger.Id = uint16(addr.Port)
}
}

pinger.SetPayloadSize(56)

if conn4 != nil {
Expand Down
132 changes: 66 additions & 66 deletions pinger_linux.go
Original file line number Diff line number Diff line change
@@ -1,66 +1,66 @@
package ping
import (
"errors"
"os"
"reflect"
"syscall"
"golang.org/x/net/icmp"
)
// getFD gets the system file descriptor for an icmp.PacketConn
func getFD(c *icmp.PacketConn) (uintptr, error) {
v := reflect.ValueOf(c).Elem().FieldByName("c").Elem()
if v.Elem().Kind() != reflect.Struct {
return 0, errors.New("invalid type")
}
fd := v.Elem().FieldByName("conn").FieldByName("fd")
if fd.Elem().Kind() != reflect.Struct {
return 0, errors.New("invalid type")
}
pfd := fd.Elem().FieldByName("pfd")
if pfd.Kind() != reflect.Struct {
return 0, errors.New("invalid type")
}
return uintptr(pfd.FieldByName("Sysfd").Int()), nil
}
func (pinger *Pinger) SetMark(mark uint) error {
conn4, ok := pinger.conn4.(*icmp.PacketConn)
if !ok {
return errors.New("invalid connection type")
}
fd, err := getFD(conn4)
if err != nil {
return err
}
err = os.NewSyscallError(
"setsockopt",
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(mark)),
)
if err != nil {
return err
}
conn6, ok := pinger.conn6.(*icmp.PacketConn)
if !ok {
return errors.New("invalid connection type")
}
fd, err = getFD(conn6)
if err != nil {
return err
}
return os.NewSyscallError(
"setsockopt",
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(mark)),
)
}
package ping

import (
"errors"
"os"
"reflect"
"syscall"

"golang.org/x/net/icmp"
)

// getFD gets the system file descriptor for an icmp.PacketConn
func getFD(c *icmp.PacketConn) (uintptr, error) {
v := reflect.ValueOf(c).Elem().FieldByName("c").Elem()
if v.Elem().Kind() != reflect.Struct {
return 0, errors.New("invalid type")
}

fd := v.Elem().FieldByName("conn").FieldByName("fd")
if fd.Elem().Kind() != reflect.Struct {
return 0, errors.New("invalid type")
}

pfd := fd.Elem().FieldByName("pfd")
if pfd.Kind() != reflect.Struct {
return 0, errors.New("invalid type")
}

return uintptr(pfd.FieldByName("Sysfd").Int()), nil
}

func (pinger *Pinger) SetMark(mark uint) error {
conn4, ok := pinger.conn4.(*icmp.PacketConn)
if !ok {
return errors.New("invalid connection type")
}

fd, err := getFD(conn4)
if err != nil {
return err
}

err = os.NewSyscallError(
"setsockopt",
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(mark)),
)

if err != nil {
return err
}

conn6, ok := pinger.conn6.(*icmp.PacketConn)
if !ok {
return errors.New("invalid connection type")
}

fd, err = getFD(conn6)
if err != nil {
return err
}

return os.NewSyscallError(
"setsockopt",
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(mark)),
)
}
49 changes: 46 additions & 3 deletions pinger_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package ping

import (
"fmt"
"net"
"os"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestPinger(t *testing.T) {
func TestPinger_Privileged(t *testing.T) {
if os.Getenv("RUN_PRIVILEGED") != "1" {
t.Skip("Skipping privileged test. Run with RUN_PRIVILEGED=1 and administrative rights.")
}
assert := assert.New(t)
require := require.New(t)

Expand All @@ -18,9 +23,47 @@ func TestPinger(t *testing.T) {
require.NotNil(pinger)
defer pinger.Close()

for _, target := range []string{"127.0.0.1", "::1"} {
for _, target := range []string{"8.8.8.8"} {
fmt.Printf("Pinging (privileged) %s...\n", target)
rtt, err := pinger.PingAttempts(&net.IPAddr{IP: net.ParseIP(target)}, time.Second, 2)
assert.NoError(err, target)
assert.NotZero(rtt, target)
assert.True(rtt >= 0, target)
}
}

func TestPinger_Unprivileged(t *testing.T) {
// Note: Windows doesn't actually support unprivileged ICMP via "udp4",
// but we can still test if our code initializes and handles the types.
// This will likely fail to bind on Windows, so we check the error.
assert := assert.New(t)
require := require.New(t)

pinger, err := NewUDP("0.0.0.0", "::")
if err != nil {
t.Skipf("Skipping unprivileged test (likely not supported on this OS or environment): %v", err)
return
}
require.NotNil(pinger)
defer pinger.Close()

for _, target := range []string{"8.8.8.8"} {
fmt.Printf("Pinging (unprivileged) %s...\n", target)
rtt, err := pinger.PingAttempts(&net.IPAddr{IP: net.ParseIP(target)}, time.Second, 2)
assert.NoError(err, target)
assert.True(rtt >= 0, target)
}
}

func TestIDHandling(t *testing.T) {
// Test if NewPinger correctly picks up the port as ID when using UDP
p, err := NewPinger("udp4", "0.0.0.0", "", "") // Bind to 0.0.0.0
if err != nil {
t.Skipf("Could not bind UDP socket for ID test: %v", err)
return
}
defer p.Close()

require.NotEqual(t, uint16(os.Getpid()), p.Id, "In UDP mode, ID should be the port, not PID")
require.NotZero(t, p.Id, "Port-based ID should not be zero")
fmt.Printf("Pinger ID (port): %d, PID was: %d\n", p.Id, os.Getpid())
}
9 changes: 8 additions & 1 deletion receiving.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ func (pinger *Pinger) receiver(proto int, conn net.PacketConn) {
break // socket gone
}
} else {
pinger.receive(proto, rb[:n], source.(*net.IPAddr).IP, time.Now())
var ip net.IP
switch addr := source.(type) {
case *net.IPAddr:
ip = addr.IP
case *net.UDPAddr:
ip = addr.IP
}
pinger.receive(proto, rb[:n], ip, time.Now())
}
}

Expand Down
6 changes: 5 additions & 1 deletion sending.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,11 @@ func (pinger *Pinger) sendRequest(destination *net.IPAddr, req request) (uint32,
req.init()

// send request
_, err = conn.WriteTo(wb, destination)
var dst net.Addr = destination
if _, ok := conn.LocalAddr().(*net.UDPAddr); ok {
dst = &net.UDPAddr{IP: destination.IP, Zone: destination.Zone}
}
_, err = conn.WriteTo(wb, dst)
lock.Unlock()

// send failed, need to remove request from list
Expand Down