mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-27 19:43:01 +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