Update dependencies
This commit is contained in:
244
vendor/tailscale.com/feature/capture/capture.go
generated
vendored
Normal file
244
vendor/tailscale.com/feature/capture/capture.go
generated
vendored
Normal file
@@ -0,0 +1,244 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package capture formats packet logging into a debug pcap stream.
|
||||
package capture
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/ipn/localapi"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
func init() {
|
||||
feature.Register("capture")
|
||||
localapi.Register("debug-capture", serveLocalAPIDebugCapture)
|
||||
}
|
||||
|
||||
func serveLocalAPIDebugCapture(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.(http.Flusher).Flush()
|
||||
|
||||
b := h.LocalBackend()
|
||||
s := b.GetOrSetCaptureSink(newSink)
|
||||
|
||||
unregister := s.RegisterOutput(w)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-s.WaitCh():
|
||||
}
|
||||
unregister()
|
||||
|
||||
b.ClearCaptureSink()
|
||||
}
|
||||
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() any {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
const flushPeriod = 100 * time.Millisecond
|
||||
|
||||
func writePcapHeader(w io.Writer) {
|
||||
binary.Write(w, binary.LittleEndian, uint32(0xA1B2C3D4)) // pcap magic number
|
||||
binary.Write(w, binary.LittleEndian, uint16(2)) // version major
|
||||
binary.Write(w, binary.LittleEndian, uint16(4)) // version minor
|
||||
binary.Write(w, binary.LittleEndian, uint32(0)) // this zone
|
||||
binary.Write(w, binary.LittleEndian, uint32(0)) // zone significant figures
|
||||
binary.Write(w, binary.LittleEndian, uint32(65535)) // max packet len
|
||||
binary.Write(w, binary.LittleEndian, uint32(147)) // link-layer ID - USER0
|
||||
}
|
||||
|
||||
func writePktHeader(w *bytes.Buffer, when time.Time, length int) {
|
||||
s := when.Unix()
|
||||
us := when.UnixMicro() - (s * 1000000)
|
||||
|
||||
binary.Write(w, binary.LittleEndian, uint32(s)) // timestamp in seconds
|
||||
binary.Write(w, binary.LittleEndian, uint32(us)) // timestamp microseconds
|
||||
binary.Write(w, binary.LittleEndian, uint32(length)) // length present
|
||||
binary.Write(w, binary.LittleEndian, uint32(length)) // total length
|
||||
}
|
||||
|
||||
// newSink creates a new capture sink.
|
||||
func newSink() packet.CaptureSink {
|
||||
ctx, c := context.WithCancel(context.Background())
|
||||
return &Sink{
|
||||
ctx: ctx,
|
||||
ctxCancel: c,
|
||||
}
|
||||
}
|
||||
|
||||
// Type Sink handles callbacks with packets to be logged,
|
||||
// formatting them into a pcap stream which is mirrored to
|
||||
// all registered outputs.
|
||||
type Sink struct {
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
|
||||
mu sync.Mutex
|
||||
outputs set.HandleSet[io.Writer]
|
||||
flushTimer *time.Timer // or nil if none running
|
||||
}
|
||||
|
||||
// RegisterOutput connects an output to this sink, which
|
||||
// will be written to with a pcap stream as packets are logged.
|
||||
// A function is returned which unregisters the output when
|
||||
// called.
|
||||
//
|
||||
// If w implements io.Closer, it will be closed upon error
|
||||
// or when the sink is closed. If w implements http.Flusher,
|
||||
// it will be flushed periodically.
|
||||
func (s *Sink) RegisterOutput(w io.Writer) (unregister func()) {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return func() {}
|
||||
default:
|
||||
}
|
||||
|
||||
writePcapHeader(w)
|
||||
s.mu.Lock()
|
||||
hnd := s.outputs.Add(w)
|
||||
s.mu.Unlock()
|
||||
|
||||
return func() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
delete(s.outputs, hnd)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Sink) CaptureCallback() packet.CaptureCallback {
|
||||
return s.LogPacket
|
||||
}
|
||||
|
||||
// NumOutputs returns the number of outputs registered with the sink.
|
||||
func (s *Sink) NumOutputs() int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return len(s.outputs)
|
||||
}
|
||||
|
||||
// Close shuts down the sink. Future calls to LogPacket
|
||||
// are ignored, and any registered output that implements
|
||||
// io.Closer is closed.
|
||||
func (s *Sink) Close() error {
|
||||
s.ctxCancel()
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.flushTimer != nil {
|
||||
s.flushTimer.Stop()
|
||||
s.flushTimer = nil
|
||||
}
|
||||
|
||||
for _, o := range s.outputs {
|
||||
if o, ok := o.(io.Closer); ok {
|
||||
o.Close()
|
||||
}
|
||||
}
|
||||
s.outputs = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// WaitCh returns a channel which blocks until
|
||||
// the sink is closed.
|
||||
func (s *Sink) WaitCh() <-chan struct{} {
|
||||
return s.ctx.Done()
|
||||
}
|
||||
|
||||
func customDataLen(meta packet.CaptureMeta) int {
|
||||
length := 4
|
||||
if meta.DidSNAT {
|
||||
length += meta.OriginalSrc.Addr().BitLen() / 8
|
||||
}
|
||||
if meta.DidDNAT {
|
||||
length += meta.OriginalDst.Addr().BitLen() / 8
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
// LogPacket is called to insert a packet into the capture.
|
||||
//
|
||||
// This function does not take ownership of the provided data slice.
|
||||
func (s *Sink) LogPacket(path packet.CapturePath, when time.Time, data []byte, meta packet.CaptureMeta) {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
extraLen := customDataLen(meta)
|
||||
b := bufferPool.Get().(*bytes.Buffer)
|
||||
b.Reset()
|
||||
b.Grow(16 + extraLen + len(data)) // 16b pcap header + len(metadata) + len(payload)
|
||||
defer bufferPool.Put(b)
|
||||
|
||||
writePktHeader(b, when, len(data)+extraLen)
|
||||
|
||||
// Custom tailscale debugging data
|
||||
binary.Write(b, binary.LittleEndian, uint16(path))
|
||||
if meta.DidSNAT {
|
||||
binary.Write(b, binary.LittleEndian, uint8(meta.OriginalSrc.Addr().BitLen()/8))
|
||||
b.Write(meta.OriginalSrc.Addr().AsSlice())
|
||||
} else {
|
||||
binary.Write(b, binary.LittleEndian, uint8(0)) // SNAT addr len == 0
|
||||
}
|
||||
if meta.DidDNAT {
|
||||
binary.Write(b, binary.LittleEndian, uint8(meta.OriginalDst.Addr().BitLen()/8))
|
||||
b.Write(meta.OriginalDst.Addr().AsSlice())
|
||||
} else {
|
||||
binary.Write(b, binary.LittleEndian, uint8(0)) // DNAT addr len == 0
|
||||
}
|
||||
|
||||
b.Write(data)
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
var hadError []set.Handle
|
||||
for hnd, o := range s.outputs {
|
||||
if _, err := o.Write(b.Bytes()); err != nil {
|
||||
hadError = append(hadError, hnd)
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, hnd := range hadError {
|
||||
if o, ok := s.outputs[hnd].(io.Closer); ok {
|
||||
o.Close()
|
||||
}
|
||||
delete(s.outputs, hnd)
|
||||
}
|
||||
|
||||
if s.flushTimer == nil {
|
||||
s.flushTimer = time.AfterFunc(flushPeriod, func() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for _, o := range s.outputs {
|
||||
if f, ok := o.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
s.flushTimer = nil
|
||||
})
|
||||
}
|
||||
}
|
||||
7
vendor/tailscale.com/feature/condregister/condregister.go
generated
vendored
Normal file
7
vendor/tailscale.com/feature/condregister/condregister.go
generated
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The condregister package registers all conditional features guarded
|
||||
// by build tags. It is one central package that callers can empty import
|
||||
// to ensure all conditional features are registered.
|
||||
package condregister
|
||||
8
vendor/tailscale.com/feature/condregister/maybe_capture.go
generated
vendored
Normal file
8
vendor/tailscale.com/feature/condregister/maybe_capture.go
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios && !ts_omit_capture
|
||||
|
||||
package condregister
|
||||
|
||||
import _ "tailscale.com/feature/capture"
|
||||
8
vendor/tailscale.com/feature/condregister/maybe_tap.go
generated
vendored
Normal file
8
vendor/tailscale.com/feature/condregister/maybe_tap.go
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && !ts_omit_tap
|
||||
|
||||
package condregister
|
||||
|
||||
import _ "tailscale.com/feature/tap"
|
||||
8
vendor/tailscale.com/feature/condregister/maybe_wakeonlan.go
generated
vendored
Normal file
8
vendor/tailscale.com/feature/condregister/maybe_wakeonlan.go
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_wakeonlan
|
||||
|
||||
package condregister
|
||||
|
||||
import _ "tailscale.com/feature/wakeonlan"
|
||||
54
vendor/tailscale.com/feature/feature.go
generated
vendored
Normal file
54
vendor/tailscale.com/feature/feature.go
generated
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package feature tracks which features are linked into the binary.
|
||||
package feature
|
||||
|
||||
import "reflect"
|
||||
|
||||
var in = map[string]bool{}
|
||||
|
||||
// Register notes that the named feature is linked into the binary.
|
||||
func Register(name string) {
|
||||
if _, ok := in[name]; ok {
|
||||
panic("duplicate feature registration for " + name)
|
||||
}
|
||||
in[name] = true
|
||||
}
|
||||
|
||||
// Hook is a func that can only be set once.
|
||||
//
|
||||
// It is not safe for concurrent use.
|
||||
type Hook[Func any] struct {
|
||||
f Func
|
||||
ok bool
|
||||
}
|
||||
|
||||
// IsSet reports whether the hook has been set.
|
||||
func (h *Hook[Func]) IsSet() bool {
|
||||
return h.ok
|
||||
}
|
||||
|
||||
// Set sets the hook function, panicking if it's already been set
|
||||
// or f is the zero value.
|
||||
//
|
||||
// It's meant to be called in init.
|
||||
func (h *Hook[Func]) Set(f Func) {
|
||||
if h.ok {
|
||||
panic("Set on already-set feature hook")
|
||||
}
|
||||
if reflect.ValueOf(f).IsZero() {
|
||||
panic("Set with zero value")
|
||||
}
|
||||
h.f = f
|
||||
h.ok = true
|
||||
}
|
||||
|
||||
// Get returns the hook function, or panics if it hasn't been set.
|
||||
// Use IsSet to check if it's been set.
|
||||
func (h *Hook[Func]) Get() Func {
|
||||
if !h.ok {
|
||||
panic("Get on unset feature hook, without IsSet")
|
||||
}
|
||||
return h.f
|
||||
}
|
||||
514
vendor/tailscale.com/feature/tap/tap_linux.go
generated
vendored
Normal file
514
vendor/tailscale.com/feature/tap/tap_linux.go
generated
vendored
Normal file
@@ -0,0 +1,514 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package tap registers Tailscale's experimental (demo) Linux TAP (Layer 2) support.
|
||||
package tap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"golang.org/x/sys/unix"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/checksum"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
// TODO: this was randomly generated once. Maybe do it per process start? But
|
||||
// then an upgraded tailscaled would be visible to devices behind it. So
|
||||
// maybe instead make it a function of the tailscaled's wireguard public key?
|
||||
// For now just hard code it.
|
||||
var ourMAC = net.HardwareAddr{0x30, 0x2D, 0x66, 0xEC, 0x7A, 0x93}
|
||||
|
||||
const tapDebug = tstun.TAPDebug
|
||||
|
||||
func init() {
|
||||
tstun.CreateTAP.Set(createTAPLinux)
|
||||
}
|
||||
|
||||
func createTAPLinux(logf logger.Logf, tapName, bridgeName string) (tun.Device, error) {
|
||||
fd, err := unix.Open("/dev/net/tun", unix.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dev, err := openDevice(logf, fd, tapName, bridgeName)
|
||||
if err != nil {
|
||||
unix.Close(fd)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dev, nil
|
||||
}
|
||||
|
||||
func openDevice(logf logger.Logf, fd int, tapName, bridgeName string) (tun.Device, error) {
|
||||
ifr, err := unix.NewIfreq(tapName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Flags are stored as a uint16 in the ifreq union.
|
||||
ifr.SetUint16(unix.IFF_TAP | unix.IFF_NO_PI)
|
||||
if err := unix.IoctlIfreq(fd, unix.TUNSETIFF, ifr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := run("ip", "link", "set", "dev", tapName, "up"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bridgeName != "" {
|
||||
if err := run("brctl", "addif", bridgeName, tapName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return newTAPDevice(logf, fd, tapName)
|
||||
}
|
||||
|
||||
type etherType [2]byte
|
||||
|
||||
var (
|
||||
etherTypeARP = etherType{0x08, 0x06}
|
||||
etherTypeIPv4 = etherType{0x08, 0x00}
|
||||
etherTypeIPv6 = etherType{0x86, 0xDD}
|
||||
)
|
||||
|
||||
const (
|
||||
ipv4HeaderLen = 20
|
||||
ethernetFrameSize = 14 // 2 six byte MACs, 2 bytes ethertype
|
||||
)
|
||||
|
||||
const (
|
||||
consumePacket = true
|
||||
passOnPacket = false
|
||||
)
|
||||
|
||||
// handleTAPFrame handles receiving a raw TAP ethernet frame and reports whether
|
||||
// it's been handled (that is, whether it should NOT be passed to wireguard).
|
||||
func (t *tapDevice) handleTAPFrame(ethBuf []byte) bool {
|
||||
|
||||
if len(ethBuf) < ethernetFrameSize {
|
||||
// Corrupt. Ignore.
|
||||
if tapDebug {
|
||||
t.logf("tap: short TAP frame")
|
||||
}
|
||||
return consumePacket
|
||||
}
|
||||
ethDstMAC, ethSrcMAC := ethBuf[:6], ethBuf[6:12]
|
||||
_ = ethDstMAC
|
||||
et := etherType{ethBuf[12], ethBuf[13]}
|
||||
switch et {
|
||||
default:
|
||||
if tapDebug {
|
||||
t.logf("tap: ignoring etherType %v", et)
|
||||
}
|
||||
return consumePacket // filter out packet we should ignore
|
||||
case etherTypeIPv6:
|
||||
// TODO: support DHCPv6/ND/etc later. For now pass all to WireGuard.
|
||||
if tapDebug {
|
||||
t.logf("tap: ignoring IPv6 %v", et)
|
||||
}
|
||||
return passOnPacket
|
||||
case etherTypeIPv4:
|
||||
if len(ethBuf) < ethernetFrameSize+ipv4HeaderLen {
|
||||
// Bogus IPv4. Eat.
|
||||
if tapDebug {
|
||||
t.logf("tap: short ipv4")
|
||||
}
|
||||
return consumePacket
|
||||
}
|
||||
return t.handleDHCPRequest(ethBuf)
|
||||
case etherTypeARP:
|
||||
arpPacket := header.ARP(ethBuf[ethernetFrameSize:])
|
||||
if !arpPacket.IsValid() {
|
||||
// Bogus ARP. Eat.
|
||||
return consumePacket
|
||||
}
|
||||
switch arpPacket.Op() {
|
||||
case header.ARPRequest:
|
||||
req := arpPacket // better name at this point
|
||||
buf := make([]byte, header.EthernetMinimumSize+header.ARPSize)
|
||||
|
||||
// Our ARP "Table" of one:
|
||||
var srcMAC [6]byte
|
||||
copy(srcMAC[:], ethSrcMAC)
|
||||
if old := t.destMAC(); old != srcMAC {
|
||||
t.destMACAtomic.Store(srcMAC)
|
||||
}
|
||||
|
||||
eth := header.Ethernet(buf)
|
||||
eth.Encode(&header.EthernetFields{
|
||||
SrcAddr: tcpip.LinkAddress(ourMAC[:]),
|
||||
DstAddr: tcpip.LinkAddress(ethSrcMAC),
|
||||
Type: 0x0806, // arp
|
||||
})
|
||||
res := header.ARP(buf[header.EthernetMinimumSize:])
|
||||
res.SetIPv4OverEthernet()
|
||||
res.SetOp(header.ARPReply)
|
||||
|
||||
// If the client's asking about their own IP, tell them it's
|
||||
// their own MAC. TODO(bradfitz): remove String allocs.
|
||||
if net.IP(req.ProtocolAddressTarget()).String() == t.clientIPv4.Load() {
|
||||
copy(res.HardwareAddressSender(), ethSrcMAC)
|
||||
} else {
|
||||
copy(res.HardwareAddressSender(), ourMAC[:])
|
||||
}
|
||||
|
||||
copy(res.ProtocolAddressSender(), req.ProtocolAddressTarget())
|
||||
copy(res.HardwareAddressTarget(), req.HardwareAddressSender())
|
||||
copy(res.ProtocolAddressTarget(), req.ProtocolAddressSender())
|
||||
|
||||
n, err := t.WriteEthernet(buf)
|
||||
if tapDebug {
|
||||
t.logf("tap: wrote ARP reply %v, %v", n, err)
|
||||
}
|
||||
}
|
||||
|
||||
return consumePacket
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// routerIP is the IP address of the DHCP server.
|
||||
routerIP = net.ParseIP(tsaddr.TailscaleServiceIPString)
|
||||
// cgnatNetMask is the netmask of the 100.64.0.0/10 CGNAT range.
|
||||
cgnatNetMask = net.IPMask(net.ParseIP("255.192.0.0").To4())
|
||||
)
|
||||
|
||||
// parsedPacketPool holds a pool of Parsed structs for use in filtering.
|
||||
// This is needed because escape analysis cannot see that parsed packets
|
||||
// do not escape through {Pre,Post}Filter{In,Out}.
|
||||
var parsedPacketPool = sync.Pool{New: func() any { return new(packet.Parsed) }}
|
||||
|
||||
// handleDHCPRequest handles receiving a raw TAP ethernet frame and reports whether
|
||||
// it's been handled as a DHCP request. That is, it reports whether the frame should
|
||||
// be ignored by the caller and not passed on.
|
||||
func (t *tapDevice) handleDHCPRequest(ethBuf []byte) bool {
|
||||
const udpHeader = 8
|
||||
if len(ethBuf) < ethernetFrameSize+ipv4HeaderLen+udpHeader {
|
||||
if tapDebug {
|
||||
t.logf("tap: DHCP short")
|
||||
}
|
||||
return passOnPacket
|
||||
}
|
||||
ethDstMAC, ethSrcMAC := ethBuf[:6], ethBuf[6:12]
|
||||
|
||||
if string(ethDstMAC) != "\xff\xff\xff\xff\xff\xff" {
|
||||
// Not a broadcast
|
||||
if tapDebug {
|
||||
t.logf("tap: dhcp no broadcast")
|
||||
}
|
||||
return passOnPacket
|
||||
}
|
||||
|
||||
p := parsedPacketPool.Get().(*packet.Parsed)
|
||||
defer parsedPacketPool.Put(p)
|
||||
p.Decode(ethBuf[ethernetFrameSize:])
|
||||
|
||||
if p.IPProto != ipproto.UDP || p.Src.Port() != 68 || p.Dst.Port() != 67 {
|
||||
// Not a DHCP request.
|
||||
if tapDebug {
|
||||
t.logf("tap: DHCP wrong meta: %+v", p)
|
||||
}
|
||||
return passOnPacket
|
||||
}
|
||||
|
||||
dp, err := dhcpv4.FromBytes(ethBuf[ethernetFrameSize+ipv4HeaderLen+udpHeader:])
|
||||
if err != nil {
|
||||
// Bogus. Trash it.
|
||||
if tapDebug {
|
||||
t.logf("tap: DHCP FromBytes bad")
|
||||
}
|
||||
return consumePacket
|
||||
}
|
||||
if tapDebug {
|
||||
t.logf("tap: DHCP request: %+v", dp)
|
||||
}
|
||||
switch dp.MessageType() {
|
||||
case dhcpv4.MessageTypeDiscover:
|
||||
ips := t.clientIPv4.Load()
|
||||
if ips == "" {
|
||||
t.logf("tap: DHCP no client IP")
|
||||
return consumePacket
|
||||
}
|
||||
offer, err := dhcpv4.New(
|
||||
dhcpv4.WithReply(dp),
|
||||
dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer),
|
||||
dhcpv4.WithRouter(routerIP), // the default route
|
||||
dhcpv4.WithDNS(routerIP),
|
||||
dhcpv4.WithServerIP(routerIP), // TODO: what is this?
|
||||
dhcpv4.WithOption(dhcpv4.OptServerIdentifier(routerIP)),
|
||||
dhcpv4.WithYourIP(net.ParseIP(ips)),
|
||||
dhcpv4.WithLeaseTime(3600), // hour works
|
||||
//dhcpv4.WithHwAddr(ethSrcMAC),
|
||||
dhcpv4.WithNetmask(cgnatNetMask),
|
||||
//dhcpv4.WithTransactionID(dp.TransactionID),
|
||||
)
|
||||
if err != nil {
|
||||
t.logf("error building DHCP offer: %v", err)
|
||||
return consumePacket
|
||||
}
|
||||
// Make a layer 2 packet to write out:
|
||||
pkt := packLayer2UDP(
|
||||
offer.ToBytes(),
|
||||
ourMAC, ethSrcMAC,
|
||||
netip.AddrPortFrom(netaddr.IPv4(100, 100, 100, 100), 67), // src
|
||||
netip.AddrPortFrom(netaddr.IPv4(255, 255, 255, 255), 68), // dst
|
||||
)
|
||||
|
||||
n, err := t.WriteEthernet(pkt)
|
||||
if tapDebug {
|
||||
t.logf("tap: wrote DHCP OFFER %v, %v", n, err)
|
||||
}
|
||||
case dhcpv4.MessageTypeRequest:
|
||||
ips := t.clientIPv4.Load()
|
||||
if ips == "" {
|
||||
t.logf("tap: DHCP no client IP")
|
||||
return consumePacket
|
||||
}
|
||||
ack, err := dhcpv4.New(
|
||||
dhcpv4.WithReply(dp),
|
||||
dhcpv4.WithMessageType(dhcpv4.MessageTypeAck),
|
||||
dhcpv4.WithDNS(routerIP),
|
||||
dhcpv4.WithRouter(routerIP), // the default route
|
||||
dhcpv4.WithServerIP(routerIP), // TODO: what is this?
|
||||
dhcpv4.WithOption(dhcpv4.OptServerIdentifier(routerIP)),
|
||||
dhcpv4.WithYourIP(net.ParseIP(ips)), // Hello world
|
||||
dhcpv4.WithLeaseTime(3600), // hour works
|
||||
dhcpv4.WithNetmask(cgnatNetMask),
|
||||
)
|
||||
if err != nil {
|
||||
t.logf("error building DHCP ack: %v", err)
|
||||
return consumePacket
|
||||
}
|
||||
// Make a layer 2 packet to write out:
|
||||
pkt := packLayer2UDP(
|
||||
ack.ToBytes(),
|
||||
ourMAC, ethSrcMAC,
|
||||
netip.AddrPortFrom(netaddr.IPv4(100, 100, 100, 100), 67), // src
|
||||
netip.AddrPortFrom(netaddr.IPv4(255, 255, 255, 255), 68), // dst
|
||||
)
|
||||
n, err := t.WriteEthernet(pkt)
|
||||
if tapDebug {
|
||||
t.logf("tap: wrote DHCP ACK %v, %v", n, err)
|
||||
}
|
||||
default:
|
||||
if tapDebug {
|
||||
t.logf("tap: unknown DHCP type")
|
||||
}
|
||||
}
|
||||
return consumePacket
|
||||
}
|
||||
|
||||
func writeEthernetFrame(buf []byte, srcMAC, dstMAC net.HardwareAddr, proto tcpip.NetworkProtocolNumber) {
|
||||
// Ethernet header
|
||||
eth := header.Ethernet(buf)
|
||||
eth.Encode(&header.EthernetFields{
|
||||
SrcAddr: tcpip.LinkAddress(srcMAC),
|
||||
DstAddr: tcpip.LinkAddress(dstMAC),
|
||||
Type: proto,
|
||||
})
|
||||
}
|
||||
|
||||
func packLayer2UDP(payload []byte, srcMAC, dstMAC net.HardwareAddr, src, dst netip.AddrPort) []byte {
|
||||
buf := make([]byte, header.EthernetMinimumSize+header.UDPMinimumSize+header.IPv4MinimumSize+len(payload))
|
||||
payloadStart := len(buf) - len(payload)
|
||||
copy(buf[payloadStart:], payload)
|
||||
srcB := src.Addr().As4()
|
||||
srcIP := tcpip.AddrFromSlice(srcB[:])
|
||||
dstB := dst.Addr().As4()
|
||||
dstIP := tcpip.AddrFromSlice(dstB[:])
|
||||
// Ethernet header
|
||||
writeEthernetFrame(buf, srcMAC, dstMAC, ipv4.ProtocolNumber)
|
||||
// IP header
|
||||
ipbuf := buf[header.EthernetMinimumSize:]
|
||||
ip := header.IPv4(ipbuf)
|
||||
ip.Encode(&header.IPv4Fields{
|
||||
TotalLength: uint16(len(ipbuf)),
|
||||
TTL: 65,
|
||||
Protocol: uint8(udp.ProtocolNumber),
|
||||
SrcAddr: srcIP,
|
||||
DstAddr: dstIP,
|
||||
})
|
||||
ip.SetChecksum(^ip.CalculateChecksum())
|
||||
// UDP header
|
||||
u := header.UDP(buf[header.EthernetMinimumSize+header.IPv4MinimumSize:])
|
||||
u.Encode(&header.UDPFields{
|
||||
SrcPort: src.Port(),
|
||||
DstPort: dst.Port(),
|
||||
Length: uint16(header.UDPMinimumSize + len(payload)),
|
||||
})
|
||||
// Calculate the UDP pseudo-header checksum.
|
||||
xsum := header.PseudoHeaderChecksum(udp.ProtocolNumber, srcIP, dstIP, uint16(len(u)))
|
||||
// Calculate the UDP checksum and set it.
|
||||
xsum = checksum.Checksum(payload, xsum)
|
||||
u.SetChecksum(^u.CalculateChecksum(xsum))
|
||||
return []byte(buf)
|
||||
}
|
||||
|
||||
func run(prog string, args ...string) error {
|
||||
cmd := exec.Command(prog, args...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("error running %v: %v", cmd, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tapDevice) destMAC() [6]byte {
|
||||
return t.destMACAtomic.Load()
|
||||
}
|
||||
|
||||
func newTAPDevice(logf logger.Logf, fd int, tapName string) (tun.Device, error) {
|
||||
err := unix.SetNonblock(fd, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file := os.NewFile(uintptr(fd), "/dev/tap")
|
||||
d := &tapDevice{
|
||||
logf: logf,
|
||||
file: file,
|
||||
events: make(chan tun.Event),
|
||||
name: tapName,
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
type tapDevice struct {
|
||||
file *os.File
|
||||
logf func(format string, args ...any)
|
||||
events chan tun.Event
|
||||
name string
|
||||
closeOnce sync.Once
|
||||
clientIPv4 syncs.AtomicValue[string]
|
||||
|
||||
destMACAtomic syncs.AtomicValue[[6]byte]
|
||||
}
|
||||
|
||||
var _ tstun.SetIPer = (*tapDevice)(nil)
|
||||
|
||||
func (t *tapDevice) SetIP(ipV4, ipV6TODO netip.Addr) error {
|
||||
t.clientIPv4.Store(ipV4.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tapDevice) File() *os.File {
|
||||
return t.file
|
||||
}
|
||||
|
||||
func (t *tapDevice) Name() (string, error) {
|
||||
return t.name, nil
|
||||
}
|
||||
|
||||
// Read reads an IP packet from the TAP device. It strips the ethernet frame header.
|
||||
func (t *tapDevice) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
|
||||
n, err := t.ReadEthernet(buffs, sizes, offset)
|
||||
if err != nil || n == 0 {
|
||||
return n, err
|
||||
}
|
||||
// Strip the ethernet frame header.
|
||||
copy(buffs[0][offset:], buffs[0][offset+ethernetFrameSize:offset+sizes[0]])
|
||||
sizes[0] -= ethernetFrameSize
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// ReadEthernet reads a raw ethernet frame from the TAP device.
|
||||
func (t *tapDevice) ReadEthernet(buffs [][]byte, sizes []int, offset int) (int, error) {
|
||||
n, err := t.file.Read(buffs[0][offset:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if t.handleTAPFrame(buffs[0][offset : offset+n]) {
|
||||
return 0, nil
|
||||
}
|
||||
sizes[0] = n
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
// WriteEthernet writes a raw ethernet frame to the TAP device.
|
||||
func (t *tapDevice) WriteEthernet(buf []byte) (int, error) {
|
||||
return t.file.Write(buf)
|
||||
}
|
||||
|
||||
// ethBufPool holds a pool of bytes.Buffers for use in [tapDevice.Write].
|
||||
var ethBufPool = syncs.Pool[*bytes.Buffer]{New: func() *bytes.Buffer { return new(bytes.Buffer) }}
|
||||
|
||||
// Write writes a raw IP packet to the TAP device. It adds the ethernet frame header.
|
||||
func (t *tapDevice) Write(buffs [][]byte, offset int) (int, error) {
|
||||
errs := make([]error, 0)
|
||||
wrote := 0
|
||||
m := t.destMAC()
|
||||
dstMac := net.HardwareAddr(m[:])
|
||||
buf := ethBufPool.Get()
|
||||
defer ethBufPool.Put(buf)
|
||||
for _, buff := range buffs {
|
||||
buf.Reset()
|
||||
buf.Grow(header.EthernetMinimumSize + len(buff) - offset)
|
||||
|
||||
var ebuf [14]byte
|
||||
switch buff[offset] >> 4 {
|
||||
case 4:
|
||||
writeEthernetFrame(ebuf[:], ourMAC, dstMac, ipv4.ProtocolNumber)
|
||||
case 6:
|
||||
writeEthernetFrame(ebuf[:], ourMAC, dstMac, ipv6.ProtocolNumber)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
buf.Write(ebuf[:])
|
||||
buf.Write(buff[offset:])
|
||||
_, err := t.WriteEthernet(buf.Bytes())
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
wrote++
|
||||
}
|
||||
}
|
||||
return wrote, multierr.New(errs...)
|
||||
}
|
||||
|
||||
func (t *tapDevice) MTU() (int, error) {
|
||||
ifr, err := unix.NewIfreq(t.name)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := unix.IoctlIfreq(int(t.file.Fd()), unix.SIOCGIFMTU, ifr); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(ifr.Uint32()), nil
|
||||
}
|
||||
|
||||
func (t *tapDevice) Events() <-chan tun.Event {
|
||||
return t.events
|
||||
}
|
||||
|
||||
func (t *tapDevice) Close() error {
|
||||
var err error
|
||||
t.closeOnce.Do(func() {
|
||||
close(t.events)
|
||||
err = t.file.Close()
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *tapDevice) BatchSize() int {
|
||||
return 1
|
||||
}
|
||||
243
vendor/tailscale.com/feature/wakeonlan/wakeonlan.go
generated
vendored
Normal file
243
vendor/tailscale.com/feature/wakeonlan/wakeonlan.go
generated
vendored
Normal file
@@ -0,0 +1,243 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package wakeonlan registers the Wake-on-LAN feature.
|
||||
package wakeonlan
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/kortschak/wol"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
)
|
||||
|
||||
func init() {
|
||||
feature.Register("wakeonlan")
|
||||
ipnlocal.RegisterC2N("POST /wol", handleC2NWoL)
|
||||
ipnlocal.RegisterPeerAPIHandler("/v0/wol", handlePeerAPIWakeOnLAN)
|
||||
hostinfo.RegisterHostinfoNewHook(func(h *tailcfg.Hostinfo) {
|
||||
h.WoLMACs = getWoLMACs()
|
||||
})
|
||||
}
|
||||
|
||||
func handleC2NWoL(b *ipnlocal.LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
var macs []net.HardwareAddr
|
||||
for _, macStr := range r.Form["mac"] {
|
||||
mac, err := net.ParseMAC(macStr)
|
||||
if err != nil {
|
||||
http.Error(w, "bad 'mac' param", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
macs = append(macs, mac)
|
||||
}
|
||||
var res struct {
|
||||
SentTo []string
|
||||
Errors []string
|
||||
}
|
||||
st := b.NetMon().InterfaceState()
|
||||
if st == nil {
|
||||
res.Errors = append(res.Errors, "no interface state")
|
||||
writeJSON(w, &res)
|
||||
return
|
||||
}
|
||||
var password []byte // TODO(bradfitz): support? does anything use WoL passwords?
|
||||
for _, mac := range macs {
|
||||
for ifName, ips := range st.InterfaceIPs {
|
||||
for _, ip := range ips {
|
||||
if ip.Addr().IsLoopback() || ip.Addr().Is6() {
|
||||
continue
|
||||
}
|
||||
local := &net.UDPAddr{
|
||||
IP: ip.Addr().AsSlice(),
|
||||
Port: 0,
|
||||
}
|
||||
remote := &net.UDPAddr{
|
||||
IP: net.IPv4bcast,
|
||||
Port: 0,
|
||||
}
|
||||
if err := wol.Wake(mac, password, local, remote); err != nil {
|
||||
res.Errors = append(res.Errors, err.Error())
|
||||
} else {
|
||||
res.SentTo = append(res.SentTo, ifName)
|
||||
}
|
||||
break // one per interface is enough
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(res.SentTo)
|
||||
writeJSON(w, &res)
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func canWakeOnLAN(h ipnlocal.PeerAPIHandler) bool {
|
||||
if h.Peer().UnsignedPeerAPIOnly() {
|
||||
return false
|
||||
}
|
||||
return h.IsSelfUntagged() || h.PeerCaps().HasCapability(tailcfg.PeerCapabilityWakeOnLAN)
|
||||
}
|
||||
|
||||
var metricWakeOnLANCalls = clientmetric.NewCounter("peerapi_wol")
|
||||
|
||||
func handlePeerAPIWakeOnLAN(h ipnlocal.PeerAPIHandler, w http.ResponseWriter, r *http.Request) {
|
||||
metricWakeOnLANCalls.Add(1)
|
||||
if !canWakeOnLAN(h) {
|
||||
http.Error(w, "no WoL access", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "bad method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
macStr := r.FormValue("mac")
|
||||
if macStr == "" {
|
||||
http.Error(w, "missing 'mac' param", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
mac, err := net.ParseMAC(macStr)
|
||||
if err != nil {
|
||||
http.Error(w, "bad 'mac' param", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var password []byte // TODO(bradfitz): support? does anything use WoL passwords?
|
||||
st := h.LocalBackend().NetMon().InterfaceState()
|
||||
if st == nil {
|
||||
http.Error(w, "failed to get interfaces state", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var res struct {
|
||||
SentTo []string
|
||||
Errors []string
|
||||
}
|
||||
for ifName, ips := range st.InterfaceIPs {
|
||||
for _, ip := range ips {
|
||||
if ip.Addr().IsLoopback() || ip.Addr().Is6() {
|
||||
continue
|
||||
}
|
||||
local := &net.UDPAddr{
|
||||
IP: ip.Addr().AsSlice(),
|
||||
Port: 0,
|
||||
}
|
||||
remote := &net.UDPAddr{
|
||||
IP: net.IPv4bcast,
|
||||
Port: 0,
|
||||
}
|
||||
if err := wol.Wake(mac, password, local, remote); err != nil {
|
||||
res.Errors = append(res.Errors, err.Error())
|
||||
} else {
|
||||
res.SentTo = append(res.SentTo, ifName)
|
||||
}
|
||||
break // one per interface is enough
|
||||
}
|
||||
}
|
||||
sort.Strings(res.SentTo)
|
||||
writeJSON(w, res)
|
||||
}
|
||||
|
||||
// TODO(bradfitz): this is all too simplistic and static. It needs to run
|
||||
// continuously in response to netmon events (USB ethernet adapters might get
|
||||
// plugged in) and look for the media type/status/etc. Right now on macOS it
|
||||
// still detects a half dozen "up" en0, en1, en2, en3 etc interfaces that don't
|
||||
// have any media. We should only report the one that's actually connected.
|
||||
// But it works for now (2023-10-05) for fleshing out the rest.
|
||||
|
||||
var wakeMAC = envknob.RegisterString("TS_WAKE_MAC") // mac address, "false" or "auto". for https://github.com/tailscale/tailscale/issues/306
|
||||
|
||||
// getWoLMACs returns up to 10 MAC address of the local machine to send
|
||||
// wake-on-LAN packets to in order to wake it up. The returned MACs are in
|
||||
// lowercase hex colon-separated form ("xx:xx:xx:xx:xx:xx").
|
||||
//
|
||||
// If TS_WAKE_MAC=auto, it tries to automatically find the MACs based on the OS
|
||||
// type and interface properties. (TODO(bradfitz): incomplete) If TS_WAKE_MAC is
|
||||
// set to a MAC address, that sole MAC address is returned.
|
||||
func getWoLMACs() (macs []string) {
|
||||
switch runtime.GOOS {
|
||||
case "ios", "android":
|
||||
return nil
|
||||
}
|
||||
if s := wakeMAC(); s != "" {
|
||||
switch s {
|
||||
case "auto":
|
||||
ifs, _ := net.Interfaces()
|
||||
for _, iface := range ifs {
|
||||
if iface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
if iface.Flags&net.FlagBroadcast == 0 ||
|
||||
iface.Flags&net.FlagRunning == 0 ||
|
||||
iface.Flags&net.FlagUp == 0 {
|
||||
continue
|
||||
}
|
||||
if keepMAC(iface.Name, iface.HardwareAddr) {
|
||||
macs = append(macs, iface.HardwareAddr.String())
|
||||
}
|
||||
if len(macs) == 10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return macs
|
||||
case "false", "off": // fast path before ParseMAC error
|
||||
return nil
|
||||
}
|
||||
mac, err := net.ParseMAC(s)
|
||||
if err != nil {
|
||||
log.Printf("invalid MAC %q", s)
|
||||
return nil
|
||||
}
|
||||
return []string{mac.String()}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var ignoreWakeOUI = map[[3]byte]bool{
|
||||
{0x00, 0x15, 0x5d}: true, // Hyper-V
|
||||
{0x00, 0x50, 0x56}: true, // VMware
|
||||
{0x00, 0x1c, 0x14}: true, // VMware
|
||||
{0x00, 0x05, 0x69}: true, // VMware
|
||||
{0x00, 0x0c, 0x29}: true, // VMware
|
||||
{0x00, 0x1c, 0x42}: true, // Parallels
|
||||
{0x08, 0x00, 0x27}: true, // VirtualBox
|
||||
{0x00, 0x21, 0xf6}: true, // VirtualBox
|
||||
{0x00, 0x14, 0x4f}: true, // VirtualBox
|
||||
{0x00, 0x0f, 0x4b}: true, // VirtualBox
|
||||
{0x52, 0x54, 0x00}: true, // VirtualBox/Vagrant
|
||||
}
|
||||
|
||||
func keepMAC(ifName string, mac []byte) bool {
|
||||
if len(mac) != 6 {
|
||||
return false
|
||||
}
|
||||
base := strings.TrimRightFunc(ifName, unicode.IsNumber)
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
switch base {
|
||||
case "llw", "awdl", "utun", "bridge", "lo", "gif", "stf", "anpi", "ap":
|
||||
return false
|
||||
}
|
||||
}
|
||||
if mac[0] == 0x02 && mac[1] == 0x42 {
|
||||
// Docker container.
|
||||
return false
|
||||
}
|
||||
oui := [3]byte{mac[0], mac[1], mac[2]}
|
||||
if ignoreWakeOUI[oui] {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user