Commit fc168c3c authored by Mikio Hara's avatar Mikio Hara

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
parent 2a8857c3
// 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)
}
// 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)
}
}
// 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
}
// 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
}
// 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
}
}
// 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)
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment