summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJames Tucker <james@tailscale.com>2023-11-02 18:39:55 -0700
committerJames Tucker <james@tailscale.com>2023-11-02 18:39:55 -0700
commit2e9a3e6b1fb37c94cd753e6a636c5c61aef18170 (patch)
treee53bf14488f3cc53fa5751d5a9d22ce5affb95ed
parent228a82f178ec97f470a12ea9998e0c93f59b2458 (diff)
downloadtailscale-raggi/icmplistener.tar.xz
tailscale-raggi/icmplistener.zip
net/icmplistener: add a way to create ICMP DGRAM socketsraggi/icmplistener
This can later be used in netns as the default underlying listener type and the net/ping package updated to not short-circuit when not running as root to perform ICMP pings without privileges. The approach probably also works on other platforms, but they should be tested independently. Signed-off-by: James Tucker <james@tailscale.com>
-rw-r--r--net/icmplistener/icmplistener.go121
-rw-r--r--net/icmplistener/icmplistener_test.go121
-rw-r--r--net/icmplistener/icmplistener_unsupported.go13
3 files changed, 255 insertions, 0 deletions
diff --git a/net/icmplistener/icmplistener.go b/net/icmplistener/icmplistener.go
new file mode 100644
index 000000000..f5da75d0d
--- /dev/null
+++ b/net/icmplistener/icmplistener.go
@@ -0,0 +1,121 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build linux || darwin
+
+// Package icmplistener implements a net.ListenConfig interface that overrides
+// the handling of "ip:icmp" and "ip6:icmp" networks to use datagram sockets
+// instead of raw sockets.
+//
+// In the 2000s the prevalence of ICMP based internet attacks led to broad
+// consensus that raw sockets must be highly priveleged, causing all ICMP to
+// become unavailable to unprivileged processes. In more recent years, standing
+// concerns about extending privelege to keep `ping` working have lead to a new
+// emerging consensus that ICMP Echo specifically should be allowed, and the
+// mechanism for doing so is to send ICMP Echo packets via a SOCK_DGRAM socket
+// type.
+//
+// This behavior is implemented by macOS and Linux (in Linux this is contingent
+// on `net.ipv4.ping_group_range` covering the users range, which it typically
+// does).
+//
+// The Go net abstraction does not directly lend itself to this kind of
+// reimplementation, as such some edge case behaviors may differ in deliberately
+// undocumented ways. Those behaviors may later change to fit intended use cases
+// (initially sending ICMP Echo from userspace).
+package icmplistener
+
+import (
+ "context"
+ "net"
+ "net/netip"
+ "os"
+
+ "golang.org/x/sys/unix"
+)
+
+type ListenConfig struct {
+ net.ListenConfig
+}
+
+func (lc *ListenConfig) ListenPacket(ctx context.Context, network, address string) (net.PacketConn, error) {
+ switch network {
+ case "ip:icmp", "ip6:icmp", "ip4:icmp", "ip6:icmp-ipv6":
+ return lc.listenICMP(ctx, network, address)
+ default:
+ return lc.ListenConfig.ListenPacket(ctx, network, address)
+ }
+}
+
+func (lc *ListenConfig) listenICMP(ctx context.Context, network, address string) (net.PacketConn, error) {
+ // If running as root, just fall back to the default behavior as SOCK_RAW
+ // should be available.
+ if os.Geteuid() == 0 {
+ return lc.ListenConfig.ListenPacket(ctx, network, address)
+ }
+
+ af := unix.AF_INET6
+ pr := unix.IPPROTO_ICMPV6
+ switch network {
+ case "ip:icmp", "ip4:icmp":
+ af = unix.AF_INET
+ pr = unix.IPPROTO_ICMP
+ case "ip6:icmp", "ip6:icmp-ipv6":
+ default:
+ // TODO: perhaps one day reimplement the full secret "favorite family"
+ // behavior from the stdlib.
+
+ // TODO: resolve, too
+ addr, err := netip.ParseAddr(address)
+ if err != nil {
+ // TODO: appropriate error type
+ return nil, err
+ }
+ if addr.Is4() {
+ af = unix.AF_INET
+ pr = unix.IPPROTO_ICMP
+ }
+ }
+
+ // technically the dup'd fd will get upgraded to nonblock and cloexec, but
+ // the behaviors and side effects are not entirely documented (and cloexec
+ // correctness in concurrent runtimes is very very complicated, especially
+ // if we're in a cgo program).
+ fd, err := unix.Socket(af, unix.SOCK_DGRAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, pr)
+ if err != nil {
+ // TODO: convert to net error
+ return nil, os.NewSyscallError("socket", err)
+ }
+ // close after the filepacketconn performs the dupfd
+ defer unix.Close(fd)
+
+ // TODO: handle configuration correctly:
+ if af == unix.AF_INET6 {
+ if err := unix.SetsockoptInt(fd, unix.IPPROTO_IPV6, unix.IPV6_V6ONLY, 0); err != nil {
+ // TODO: convert to net error
+ return nil, err
+ }
+ }
+
+ f := os.NewFile(uintptr(fd), address)
+ if lc.Control != nil {
+ rc, err := f.SyscallConn()
+ // TODO: convert to net error
+ if err != nil {
+ return nil, err
+ }
+ lc.Control(network, address, rc)
+ }
+
+ if af == unix.AF_INET6 {
+ err = unix.Bind(fd, &unix.SockaddrInet6{Port: 0})
+ } else {
+ err = unix.Bind(fd, &unix.SockaddrInet4{Port: 0})
+ }
+ if err != nil {
+ // TODO: convert to net error
+ return nil, err
+ }
+
+ return net.FilePacketConn(f)
+}
diff --git a/net/icmplistener/icmplistener_test.go b/net/icmplistener/icmplistener_test.go
new file mode 100644
index 000000000..dcc8d5212
--- /dev/null
+++ b/net/icmplistener/icmplistener_test.go
@@ -0,0 +1,121 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package icmplistener
+
+import (
+ "context"
+ "net"
+ "os"
+ "syscall"
+ "testing"
+
+ "golang.org/x/net/icmp"
+ "golang.org/x/net/ipv4"
+ "golang.org/x/sys/unix"
+)
+
+func TestListenPacket(t *testing.T) {
+ ctx := context.Background()
+ var lc ListenConfig
+ pc, err := lc.ListenPacket(ctx, "ip:icmp", "0.0.0.0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer pc.Close()
+
+ rc, err := pc.(syscall.Conn).SyscallConn()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertSockOpt := func(name string, fd uintptr, opt, want int) {
+ got, err := syscall.GetsockoptInt(int(fd), syscall.SOL_SOCKET, opt)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got != want {
+ t.Fatalf("unexpected sockopt %s: got %v, want %v", name, got, want)
+ }
+ }
+
+ assertFcntl := func(name string, fd uintptr, cmd, arg, want int) {
+ got, err := unix.FcntlInt(fd, cmd, arg)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if cmd == syscall.F_GETFL {
+ if arg&got != 0 {
+ got = 1
+ } else {
+ got = 0
+ }
+ }
+ if got != want {
+ t.Fatalf("unexpected fcntl %s: got %v, want %v", name, got, want)
+ }
+ }
+
+ rc.Control(func(fd uintptr) {
+ wantTyp := syscall.SOCK_DGRAM
+ if os.Geteuid() == 0 {
+ wantTyp = syscall.SOCK_RAW
+ }
+
+ assertSockOpt("TYPE", fd, syscall.SO_TYPE, wantTyp)
+ assertSockOpt("PROTOCOL", fd, syscall.SO_PROTOCOL, syscall.IPPROTO_ICMPV6)
+ // TODO: check IPV6_V6ONLY.
+
+ // Most of these options are set by the stdlib wrapper on the way to a
+ // pollable, but they're worth checking as failure to set them is a
+ // significant change on various axes, such as performance.
+ assertSockOpt("REUSEADDR", fd, syscall.SO_REUSEADDR, 1)
+ assertFcntl("NONBLOCK", fd, syscall.F_GETFL, syscall.O_NONBLOCK, 1)
+ assertFcntl("CLOEXEC", fd, syscall.F_GETFD, syscall.O_CLOEXEC, 1)
+ })
+}
+
+func TestPing(t *testing.T) {
+ ctx := context.Background()
+ var lc ListenConfig
+ pc, err := lc.ListenPacket(ctx, "ip:icmp", "0.0.0.0")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ localhost := "127.0.0.1:1"
+ dst, err := net.ResolveUDPAddr("udp", localhost)
+ if err != nil {
+ t.Fatal(err)
+ }
+ b, err := (&icmp.Message{
+ Type: ipv4.ICMPTypeEcho,
+ Code: 0,
+ Body: &icmp.Echo{
+ ID: 0,
+ Seq: 0,
+ Data: []byte("hello"),
+ },
+ }).Marshal(nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if _, err := pc.WriteTo(b, dst); err != nil {
+ t.Fatal(err)
+ }
+ b = make([]byte, 1500)
+ n, _, err := pc.ReadFrom(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ m, err := icmp.ParseMessage(1, b[:n])
+ if err != nil {
+ t.Fatal(err)
+ }
+ if m.Type != ipv4.ICMPTypeEchoReply {
+ t.Fatalf("got ICMP type %v, want %v", m.Type, ipv4.ICMPTypeEchoReply)
+ }
+ if string(m.Body.(*icmp.Echo).Data) != "hello" {
+ t.Fatalf("got ICMP body %q, want %q", m.Body, "hello")
+ }
+}
diff --git a/net/icmplistener/icmplistener_unsupported.go b/net/icmplistener/icmplistener_unsupported.go
new file mode 100644
index 000000000..59a974d31
--- /dev/null
+++ b/net/icmplistener/icmplistener_unsupported.go
@@ -0,0 +1,13 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+//go:build !(linux || darwin)
+
+package icmplistener
+
+import "net"
+
+// ListenConfig on this platform is simply a wrapper around net.ListenConfig.
+type ListenConfig struct {
+ net.ListenConfig
+}