Browse Source

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
Mikio Hara 11 years ago
parent
commit
fc168c3c5c

+ 108 - 0
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)
+}

+ 54 - 0
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)
+	}
+}

+ 87 - 0
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
+}

+ 33 - 0
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
+}

+ 98 - 0
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
+	}
+}

+ 130 - 0
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)
+		}
+	}
+}