aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorMikio Hara <mikioh.mikioh@gmail.com>2014-12-03 09:34:28 +0900
committerMikio Hara <mikioh.mikioh@gmail.com>2014-12-03 09:34:28 +0900
commitfc168c3c5cfd0f4237144c6863b9585cc5d9a24f (patch)
tree1faa2151cda412c537022b0f972025cefece848e /internal
parent2a8857c36e9e894577a56683cb2cefb4d0be6472 (diff)
downloadnet-fc168c3c5cfd0f4237144c6863b9585cc5d9a24f.tar.gz
x/net/internal/icmp: add support for non-privileged ICMP endpoint, known as ping socket
This CL adds PacketConn struct that implements net.PacketConn interface. Update golang/go#9166 LGTM=iant R=iant CC=golang-codereviews https://golang.org/cl/182110043
Diffstat (limited to 'internal')
-rw-r--r--internal/icmp/endpoint.go108
-rw-r--r--internal/icmp/example_test.go54
-rw-r--r--internal/icmp/helper_unix.go87
-rw-r--r--internal/icmp/listen_stub.go33
-rw-r--r--internal/icmp/listen_unix.go98
-rw-r--r--internal/icmp/ping_test.go130
6 files changed, 510 insertions, 0 deletions
diff --git a/internal/icmp/endpoint.go b/internal/icmp/endpoint.go
new file mode 100644
index 0000000..5bf2cfe
--- /dev/null
+++ b/internal/icmp/endpoint.go
@@ -0,0 +1,108 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package icmp
+
+import (
+ "net"
+ "syscall"
+ "time"
+
+ "golang.org/x/net/ipv4"
+ "golang.org/x/net/ipv6"
+)
+
+var _ net.PacketConn = &PacketConn{}
+
+type ipc interface{}
+
+// A PacketConn represents a packet network endpoint that uses either
+// ICMPv4 or ICMPv6.
+type PacketConn struct {
+ c net.PacketConn
+ ipc // either ipv4.PacketConn or ipv6.PacketConn
+}
+
+func (c *PacketConn) ok() bool { return c != nil && c.c != nil }
+
+// IPv4PacketConn returns the ipv4.PacketConn of c.
+// It returns nil when c is not created as the endpoint for ICMPv4.
+func (c *PacketConn) IPv4PacketConn() *ipv4.PacketConn {
+ if !c.ok() {
+ return nil
+ }
+ p, _ := c.ipc.(*ipv4.PacketConn)
+ return p
+}
+
+// IPv6PacketConn returns the ipv6.PacketConn of c.
+// It returns nil when c is not created as the endpoint for ICMPv6.
+func (c *PacketConn) IPv6PacketConn() *ipv6.PacketConn {
+ if !c.ok() {
+ return nil
+ }
+ p, _ := c.ipc.(*ipv6.PacketConn)
+ return p
+}
+
+// ReadFrom reads an ICMP message from the connection.
+func (c *PacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
+ if !c.ok() {
+ return 0, nil, syscall.EINVAL
+ }
+ return c.c.ReadFrom(b)
+}
+
+// WriteTo writes the ICMP message b to dst.
+// Dst must be net.UDPAddr when c is a non-privileged
+// datagram-oriented ICMP endpoint. Otherwise it must be net.IPAddr.
+func (c *PacketConn) WriteTo(b []byte, dst net.Addr) (int, error) {
+ if !c.ok() {
+ return 0, syscall.EINVAL
+ }
+ return c.c.WriteTo(b, dst)
+}
+
+// Close closes the endpoint.
+func (c *PacketConn) Close() error {
+ if !c.ok() {
+ return syscall.EINVAL
+ }
+ return c.c.Close()
+}
+
+// LocalAddr returns the local network address.
+func (c *PacketConn) LocalAddr() net.Addr {
+ if !c.ok() {
+ return nil
+ }
+ return c.c.LocalAddr()
+}
+
+// SetDeadline sets the read and write deadlines associated with the
+// endpoint.
+func (c *PacketConn) SetDeadline(t time.Time) error {
+ if !c.ok() {
+ return syscall.EINVAL
+ }
+ return c.c.SetDeadline(t)
+}
+
+// SetReadDeadline sets the read deadline associated with the
+// endpoint.
+func (c *PacketConn) SetReadDeadline(t time.Time) error {
+ if !c.ok() {
+ return syscall.EINVAL
+ }
+ return c.c.SetReadDeadline(t)
+}
+
+// SetWriteDeadline sets the write deadline associated with the
+// endpoint.
+func (c *PacketConn) SetWriteDeadline(t time.Time) error {
+ if !c.ok() {
+ return syscall.EINVAL
+ }
+ return c.c.SetWriteDeadline(t)
+}
diff --git a/internal/icmp/example_test.go b/internal/icmp/example_test.go
new file mode 100644
index 0000000..0ef839c
--- /dev/null
+++ b/internal/icmp/example_test.go
@@ -0,0 +1,54 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package icmp_test
+
+import (
+ "log"
+ "net"
+ "os"
+
+ "golang.org/x/net/internal/iana"
+ "golang.org/x/net/internal/icmp"
+ "golang.org/x/net/ipv6"
+)
+
+func ExamplePacketConn_nonPrivilegedPing() {
+ c, err := icmp.ListenPacket("udp6", "fe80::1%en0")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer c.Close()
+
+ wm := icmp.Message{
+ Type: ipv6.ICMPTypeEchoRequest, Code: 0,
+ Body: &icmp.Echo{
+ ID: os.Getpid() & 0xffff, Seq: 1,
+ Data: []byte("HELLO-R-U-THERE"),
+ },
+ }
+ wb, err := wm.Marshal(nil)
+ if err != nil {
+ log.Fatal(err)
+ }
+ if _, err := c.WriteTo(wb, &net.UDPAddr{IP: net.ParseIP("ff02::1"), Zone: "en0"}); err != nil {
+ log.Fatal(err)
+ }
+
+ rb := make([]byte, 1500)
+ n, peer, err := c.ReadFrom(rb)
+ if err != nil {
+ log.Fatal(err)
+ }
+ rm, err := icmp.ParseMessage(iana.ProtocolIPv6ICMP, rb[:n])
+ if err != nil {
+ log.Fatal(err)
+ }
+ switch rm.Type {
+ case ipv6.ICMPTypeEchoReply:
+ log.Printf("got reflection from %v", peer)
+ default:
+ log.Printf("got %+v; want echo reply", rm)
+ }
+}
diff --git a/internal/icmp/helper_unix.go b/internal/icmp/helper_unix.go
new file mode 100644
index 0000000..d3c5529
--- /dev/null
+++ b/internal/icmp/helper_unix.go
@@ -0,0 +1,87 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package icmp
+
+import (
+ "net"
+ "syscall"
+)
+
+func sockaddr(family int, address string) (syscall.Sockaddr, error) {
+ switch family {
+ case syscall.AF_INET:
+ a, err := net.ResolveIPAddr("ip4", address)
+ if err != nil {
+ return nil, err
+ }
+ if len(a.IP) == 0 {
+ a.IP = net.IPv4zero
+ }
+ if a.IP = a.IP.To4(); a.IP == nil {
+ return nil, net.InvalidAddrError("non-ipv4 address")
+ }
+ sa := &syscall.SockaddrInet4{}
+ copy(sa.Addr[:], a.IP)
+ return sa, nil
+ case syscall.AF_INET6:
+ a, err := net.ResolveIPAddr("ip6", address)
+ if err != nil {
+ return nil, err
+ }
+ if len(a.IP) == 0 {
+ a.IP = net.IPv6unspecified
+ }
+ if a.IP.Equal(net.IPv4zero) {
+ a.IP = net.IPv6unspecified
+ }
+ if a.IP = a.IP.To16(); a.IP == nil || a.IP.To4() != nil {
+ return nil, net.InvalidAddrError("non-ipv6 address")
+ }
+ sa := &syscall.SockaddrInet6{ZoneId: zoneToUint32(a.Zone)}
+ copy(sa.Addr[:], a.IP)
+ return sa, nil
+ default:
+ return nil, net.InvalidAddrError("unexpected family")
+ }
+}
+
+func zoneToUint32(zone string) uint32 {
+ if zone == "" {
+ return 0
+ }
+ if ifi, err := net.InterfaceByName(zone); err == nil {
+ return uint32(ifi.Index)
+ }
+ n, _, _ := dtoi(zone, 0)
+ return uint32(n)
+}
+
+func last(s string, b byte) int {
+ i := len(s)
+ for i--; i >= 0; i-- {
+ if s[i] == b {
+ break
+ }
+ }
+ return i
+}
+
+const big = 0xFFFFFF
+
+func dtoi(s string, i0 int) (n int, i int, ok bool) {
+ n = 0
+ for i = i0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
+ n = n*10 + int(s[i]-'0')
+ if n >= big {
+ return 0, i, false
+ }
+ }
+ if i == i0 {
+ return 0, i, false
+ }
+ return n, i, true
+}
diff --git a/internal/icmp/listen_stub.go b/internal/icmp/listen_stub.go
new file mode 100644
index 0000000..1a9be6a
--- /dev/null
+++ b/internal/icmp/listen_stub.go
@@ -0,0 +1,33 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build nacl plan9 windows
+
+package icmp
+
+// ListenPacket listens for incoming ICMP packets addressed to
+// address. See net.Dial for the syntax of address.
+//
+// For non-privileged datagram-oriented ICMP endpoints, network must
+// be "udp4" or "udp6". The endpoint allows to read, write a few
+// limited ICMP messages such as echo request and echo reply.
+// Currently only Dariwn and Linux support this.
+//
+// Examples:
+// ListenPacket("udp4", "192.168.0.1")
+// ListenPacket("udp4", "0.0.0.0")
+// ListenPacket("udp6", "fe80::1%en0")
+// ListenPacket("udp6", "::")
+//
+// For privileged raw ICMP endpoints, network must be "ip4" or "ip6"
+// followed by a colon and an ICMP protocol number or name.
+//
+// Examples:
+// ListenPacket("ip4:icmp", "192.168.0.1")
+// ListenPacket("ip4:1", "0.0.0.0")
+// ListenPacket("ip6:ipv6-icmp", "fe80::1%en0")
+// ListenPacket("ip6:58", "::")
+func ListenPacket(network, address string) (*PacketConn, error) {
+ return nil, errOpNoSupport
+}
diff --git a/internal/icmp/listen_unix.go b/internal/icmp/listen_unix.go
new file mode 100644
index 0000000..5ae9c39
--- /dev/null
+++ b/internal/icmp/listen_unix.go
@@ -0,0 +1,98 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package icmp
+
+import (
+ "net"
+ "os"
+ "runtime"
+ "syscall"
+
+ "golang.org/x/net/internal/iana"
+ "golang.org/x/net/ipv4"
+ "golang.org/x/net/ipv6"
+)
+
+const sysIP_STRIPHDR = 0x17 // for now only darwin supports this option
+
+// ListenPacket listens for incoming ICMP packets addressed to
+// address. See net.Dial for the syntax of address.
+//
+// For non-privileged datagram-oriented ICMP endpoints, network must
+// be "udp4" or "udp6". The endpoint allows to read, write a few
+// limited ICMP messages such as echo request and echo reply.
+// Currently only Dariwn and Linux support this.
+//
+// Examples:
+// ListenPacket("udp4", "192.168.0.1")
+// ListenPacket("udp4", "0.0.0.0")
+// ListenPacket("udp6", "fe80::1%en0")
+// ListenPacket("udp6", "::")
+//
+// For privileged raw ICMP endpoints, network must be "ip4" or "ip6"
+// followed by a colon and an ICMP protocol number or name.
+//
+// Examples:
+// ListenPacket("ip4:icmp", "192.168.0.1")
+// ListenPacket("ip4:1", "0.0.0.0")
+// ListenPacket("ip6:ipv6-icmp", "fe80::1%en0")
+// ListenPacket("ip6:58", "::")
+func ListenPacket(network, address string) (*PacketConn, error) {
+ var family, proto int
+ switch network {
+ case "udp4":
+ family, proto = syscall.AF_INET, iana.ProtocolICMP
+ case "udp6":
+ family, proto = syscall.AF_INET6, iana.ProtocolIPv6ICMP
+ default:
+ i := last(network, ':')
+ switch network[:i] {
+ case "ip4":
+ proto = iana.ProtocolICMP
+ case "ip6":
+ proto = iana.ProtocolIPv6ICMP
+ }
+ }
+ var err error
+ var c net.PacketConn
+ switch family {
+ case syscall.AF_INET, syscall.AF_INET6:
+ s, err := syscall.Socket(family, syscall.SOCK_DGRAM, proto)
+ if err != nil {
+ return nil, os.NewSyscallError("socket", err)
+ }
+ defer syscall.Close(s)
+ if runtime.GOOS == "darwin" && family == syscall.AF_INET {
+ if err := syscall.SetsockoptInt(s, iana.ProtocolIP, sysIP_STRIPHDR, 1); err != nil {
+ return nil, os.NewSyscallError("setsockopt", err)
+ }
+ }
+ sa, err := sockaddr(family, address)
+ if err != nil {
+ return nil, err
+ }
+ if err := syscall.Bind(s, sa); err != nil {
+ return nil, os.NewSyscallError("bind", err)
+ }
+ f := os.NewFile(uintptr(s), "datagram-oriented icmp")
+ defer f.Close()
+ c, err = net.FilePacketConn(f)
+ default:
+ c, err = net.ListenPacket(network, address)
+ }
+ if err != nil {
+ return nil, err
+ }
+ switch proto {
+ case iana.ProtocolICMP:
+ return &PacketConn{c: c, ipc: ipv4.NewPacketConn(c)}, nil
+ case iana.ProtocolIPv6ICMP:
+ return &PacketConn{c: c, ipc: ipv6.NewPacketConn(c)}, nil
+ default:
+ return &PacketConn{c: c}, nil
+ }
+}
diff --git a/internal/icmp/ping_test.go b/internal/icmp/ping_test.go
new file mode 100644
index 0000000..8d71d6c
--- /dev/null
+++ b/internal/icmp/ping_test.go
@@ -0,0 +1,130 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package icmp_test
+
+import (
+ "errors"
+ "net"
+ "os"
+ "runtime"
+ "testing"
+
+ "golang.org/x/net/internal/iana"
+ "golang.org/x/net/internal/icmp"
+ "golang.org/x/net/ipv4"
+ "golang.org/x/net/ipv6"
+)
+
+func googleAddr(c *icmp.PacketConn, protocol int) (net.Addr, error) {
+ const host = "www.google.com"
+ ips, err := net.LookupIP(host)
+ if err != nil {
+ return nil, err
+ }
+ netaddr := func(ip net.IP) (net.Addr, error) {
+ switch c.LocalAddr().(type) {
+ case *net.UDPAddr:
+ return &net.UDPAddr{IP: ip}, nil
+ case *net.IPAddr:
+ return &net.IPAddr{IP: ip}, nil
+ default:
+ return nil, errors.New("neither UDPAddr nor IPAddr")
+ }
+ }
+ for _, ip := range ips {
+ switch protocol {
+ case iana.ProtocolICMP:
+ if ip.To4() != nil {
+ return netaddr(ip)
+ }
+ case iana.ProtocolIPv6ICMP:
+ if ip.To16() != nil && ip.To4() == nil {
+ return netaddr(ip)
+ }
+ }
+ }
+ return nil, errors.New("no A or AAAA record")
+}
+
+var pingGoogleTests = []struct {
+ network, address string
+ protocol int
+ mtype icmp.Type
+}{
+ {"udp4", "0.0.0.0", iana.ProtocolICMP, ipv4.ICMPTypeEcho},
+ {"ip4:icmp", "0.0.0.0", iana.ProtocolICMP, ipv4.ICMPTypeEcho},
+
+ {"udp6", "::", iana.ProtocolIPv6ICMP, ipv6.ICMPTypeEchoRequest},
+ {"ip6:ipv6-icmp", "::", iana.ProtocolIPv6ICMP, ipv6.ICMPTypeEchoRequest},
+}
+
+func TestPingGoogle(t *testing.T) {
+ if testing.Short() {
+ t.Skip("to avoid external network")
+ }
+ switch runtime.GOOS {
+ case "darwin":
+ case "linux":
+ t.Log("you may need to adjust the net.ipv4.ping_group_range kernel state")
+ default:
+ t.Skipf("not supported on %q", runtime.GOOS)
+ }
+
+ for i, tt := range pingGoogleTests {
+ if tt.network[:2] == "ip" && os.Getuid() != 0 {
+ continue
+ }
+ c, err := icmp.ListenPacket(tt.network, tt.address)
+ if err != nil {
+ t.Error(err)
+ continue
+ }
+ defer c.Close()
+
+ dst, err := googleAddr(c, tt.protocol)
+ if err != nil {
+ t.Error(err)
+ continue
+ }
+
+ wm := icmp.Message{
+ Type: tt.mtype, Code: 0,
+ Body: &icmp.Echo{
+ ID: os.Getpid() & 0xffff, Seq: 1 << uint(i),
+ Data: []byte("HELLO-R-U-THERE"),
+ },
+ }
+ wb, err := wm.Marshal(nil)
+ if err != nil {
+ t.Error(err)
+ continue
+ }
+ if n, err := c.WriteTo(wb, dst); err != nil {
+ t.Error(err, dst)
+ continue
+ } else if n != len(wb) {
+ t.Errorf("got %v; want %v", n, len(wb))
+ continue
+ }
+
+ rb := make([]byte, 1500)
+ n, peer, err := c.ReadFrom(rb)
+ if err != nil {
+ t.Error(err)
+ continue
+ }
+ rm, err := icmp.ParseMessage(tt.protocol, rb[:n])
+ if err != nil {
+ t.Error(err)
+ continue
+ }
+ switch rm.Type {
+ case ipv4.ICMPTypeEchoReply, ipv6.ICMPTypeEchoReply:
+ t.Logf("got reflection from %v", peer)
+ default:
+ t.Errorf("got %+v; want echo reply", rm)
+ }
+ }
+}