mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-07 08:44:38 +00:00
Merge 2e9a3e6b1fb37c94cd753e6a636c5c61aef18170 into b3455fa99a5e8d07133d5140017ec7c49f032a07
This commit is contained in:
commit
47052857c2
121
net/icmplistener/icmplistener.go
Normal file
121
net/icmplistener/icmplistener.go
Normal file
@ -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)
|
||||||
|
}
|
121
net/icmplistener/icmplistener_test.go
Normal file
121
net/icmplistener/icmplistener_test.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
13
net/icmplistener/icmplistener_unsupported.go
Normal file
13
net/icmplistener/icmplistener_unsupported.go
Normal file
@ -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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user