tailscale/derp/xdp/xdp_linux.go

320 lines
9.3 KiB
Go
Raw Permalink Normal View History

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package xdp
import (
"errors"
"fmt"
"log"
"math"
"net"
"sync"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/prometheus/client_golang/prometheus"
"tailscale.com/util/multierr"
)
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type config -type counters_key -type counter_key_af -type counter_key_packets_bytes_action -type counter_key_prog_end bpf xdp.c -- -I headers
// STUNServer manages loading and unloading of an eBPF XDP program that serves
// the STUN protocol. It exports statistics for the XDP program via its
// implementation of the prometheus.Collector interface.
type STUNServer struct {
mu sync.Mutex
objs *bpfObjects
metrics *stunServerMetrics
dstPort int
dropSTUN bool
link link.Link
}
//lint:ignore U1000 used in xdp_linux_test.go, which has a build tag
type noAttachOption struct{}
//lint:ignore u1000 Used in xdp_linux_test.go, which has a build tag
func (n noAttachOption) apply(opts *stunServerOptions) {
opts.noAttach = true
}
func (s *STUNServerConfig) validate() error {
if len(s.DeviceName) < 1 {
return errors.New("DeviceName is unspecified")
}
if s.DstPort < 0 || s.DstPort > math.MaxUint16 {
return errors.New("DstPort is outside of uint16 bounds")
}
return nil
}
// NewSTUNServer returns an instance of a STUNServer that has attached the STUN
// XDP program to the netdev and destination port specified by config.
func NewSTUNServer(config *STUNServerConfig, opts ...STUNServerOption) (*STUNServer, error) {
o := &stunServerOptions{}
for _, opt := range opts {
opt.apply(o)
}
err := config.validate()
if err != nil {
return nil, fmt.Errorf("invalid config: %v", err)
}
objs := new(bpfObjects)
err = loadBpfObjects(objs, nil)
if err != nil {
var ve *ebpf.VerifierError
if config.FullVerifierErr && errors.As(err, &ve) {
err = fmt.Errorf("verifier error: %+v", ve)
}
return nil, fmt.Errorf("error loading XDP program: %w", err)
}
server := &STUNServer{
objs: objs,
metrics: newSTUNServerMetrics(),
dstPort: config.DstPort,
}
var key uint32
xdpConfig := &bpfConfig{
DstPort: uint16(config.DstPort),
}
err = objs.ConfigMap.Put(key, xdpConfig)
if err != nil {
return nil, fmt.Errorf("error loading config in eBPF map: %w", err)
}
if o.noAttach {
return server, nil
}
iface, err := net.InterfaceByName(config.DeviceName)
if err != nil {
return nil, fmt.Errorf("error finding device: %w", err)
}
link, err := link.AttachXDP(link.XDPOptions{
Program: objs.XdpProgFunc,
Interface: iface.Index,
Flags: link.XDPAttachFlags(config.AttachFlags),
})
if err != nil {
return nil, fmt.Errorf("error attaching XDP program to dev: %w", err)
}
server.link = link
return server, nil
}
// Close unloads the XDP program and associated maps.
func (s *STUNServer) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
var errs []error
if s.link != nil {
errs = append(errs, s.link.Close())
}
errs = append(errs, s.objs.Close())
return multierr.New(errs...)
}
type stunServerMetrics struct {
last map[bpfCountersKey]uint64
registry *prometheus.Registry
packets *prometheus.CounterVec
bytes *prometheus.CounterVec
}
func newSTUNServerMetrics() *stunServerMetrics {
last := make(map[bpfCountersKey]uint64)
registry := prometheus.NewRegistry()
packets := prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "xdp",
Subsystem: "stun_server",
Name: "packets_total",
}, []string{addressFamilyKey, xdpOutcomeKey, progEndKey})
bytes := prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "xdp",
Subsystem: "stun_server",
Name: "bytes_total",
}, []string{addressFamilyKey, xdpOutcomeKey, progEndKey})
registry.MustRegister(packets, bytes)
return &stunServerMetrics{
last: last,
registry: registry,
packets: packets,
bytes: bytes,
}
}
const (
xdpOutcomeKey = "xdp_outcome"
progEndKey = "prog_end"
)
const (
xdpOutcomePass = "pass"
xdpOutcomeAborted = "aborted"
xdpOutcomeDrop = "drop"
xdpOutcomeTX = "tx"
)
func sum(vals []uint64) uint64 {
var s uint64
for _, v := range vals {
s += v
}
return s
}
const (
addressFamilyKey = "address_family"
)
const (
addressFamilyUnknown = "unknown"
addressFamilyIPv4 = "ipv4"
addressFamilyIPv6 = "ipv6"
)
var (
// TODO(jwhited): go generate these maps or equivalent switch logic behind bpf2go
pbaToOutcomeLV = map[bpfCounterKeyPacketsBytesAction]string{
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_PACKETS_PASS_TOTAL: xdpOutcomePass,
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_BYTES_PASS_TOTAL: xdpOutcomePass,
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_PACKETS_ABORTED_TOTAL: xdpOutcomeAborted,
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_BYTES_ABORTED_TOTAL: xdpOutcomeAborted,
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_PACKETS_TX_TOTAL: xdpOutcomeTX,
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_BYTES_TX_TOTAL: xdpOutcomeTX,
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_PACKETS_DROP_TOTAL: xdpOutcomeDrop,
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_BYTES_DROP_TOTAL: xdpOutcomeDrop,
}
progEndLV = map[bpfCounterKeyProgEnd]string{
bpfCounterKeyProgEndCOUNTER_KEY_END_UNSPECIFIED: "unspecified",
bpfCounterKeyProgEndCOUNTER_KEY_END_UNEXPECTED_FIRST_STUN_ATTR: "unexpected_first_stun_attr",
bpfCounterKeyProgEndCOUNTER_KEY_END_INVALID_UDP_CSUM: "invalid_udp_csum",
bpfCounterKeyProgEndCOUNTER_KEY_END_INVALID_IP_CSUM: "invalid_ip_csum",
bpfCounterKeyProgEndCOUNTER_KEY_END_NOT_STUN_PORT: "not_stun_port",
bpfCounterKeyProgEndCOUNTER_KEY_END_INVALID_SW_ATTR_VAL: "invalid_sw_attr_val",
bpfCounterKeyProgEndCOUNTER_KEY_END_DROP_STUN: "drop_stun",
}
packetCounterKeys = map[bpfCounterKeyPacketsBytesAction]bool{
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_PACKETS_PASS_TOTAL: true,
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_PACKETS_ABORTED_TOTAL: true,
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_PACKETS_TX_TOTAL: true,
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_PACKETS_DROP_TOTAL: true,
}
//lint:ignore U1000 used in xdp_linux_test.go, which has a build tag
bytesCounterKeys = map[bpfCounterKeyPacketsBytesAction]bool{
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_BYTES_PASS_TOTAL: true,
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_BYTES_ABORTED_TOTAL: true,
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_BYTES_TX_TOTAL: true,
bpfCounterKeyPacketsBytesActionCOUNTER_KEY_BYTES_DROP_TOTAL: true,
}
)
// increase returns the difference between "from" and "to" assuming they
// originated from the same counter gathered at different times, i.e. "from"
// was incremented by a non-negative value into "to". In the case of wraps
// increase returns the difference between "to" and zero.
func increase(from, to uint64) uint64 {
if to >= from {
return to - from
}
return to
}
func (s *stunServerMetrics) updateFromMapKV(key bpfCountersKey, vals []uint64) error {
if key.Unused != 0 ||
key.Af >= uint8(bpfCounterKeyAfCOUNTER_KEY_AF_LEN) ||
key.Pba >= uint8(bpfCounterKeyPacketsBytesActionCOUNTER_KEY_PACKETS_BYTES_ACTION_LEN) ||
key.ProgEnd >= uint8(bpfCounterKeyProgEndCOUNTER_KEY_END_LEN) {
return fmt.Errorf("unexpected counter key: %+v", key)
}
previousAllCPUs := s.last[key]
allCPUs := sum(vals)
s.last[key] = allCPUs
inc := increase(previousAllCPUs, allCPUs)
if inc == 0 {
return nil
}
var af string
switch key.Af {
case uint8(bpfCounterKeyAfCOUNTER_KEY_AF_UNKNOWN):
af = addressFamilyUnknown
case uint8(bpfCounterKeyAfCOUNTER_KEY_AF_IPV4):
af = addressFamilyIPv4
case uint8(bpfCounterKeyAfCOUNTER_KEY_AF_IPV6):
af = addressFamilyIPv6
}
labels := prometheus.Labels{
addressFamilyKey: af,
xdpOutcomeKey: pbaToOutcomeLV[bpfCounterKeyPacketsBytesAction(key.Pba)],
progEndKey: progEndLV[bpfCounterKeyProgEnd(key.ProgEnd)],
}
var metric *prometheus.CounterVec
if packetCounterKeys[bpfCounterKeyPacketsBytesAction(key.Pba)] {
metric = s.packets
} else {
metric = s.bytes
}
metric.With(labels).Add(float64(inc))
return nil
}
// Describe is part of the implementation of prometheus.Collector.
func (s *STUNServer) Describe(descCh chan<- *prometheus.Desc) {
s.metrics.registry.Describe(descCh)
}
// Collect is part of the implementation of prometheus.Collector.
func (s *STUNServer) Collect(metricCh chan<- prometheus.Metric) {
err := s.updateMetrics()
if err != nil {
log.Printf("xdp: error collecting metrics: %v", err)
}
s.metrics.registry.Collect(metricCh)
}
func (s *STUNServer) SetDropSTUN(v bool) error {
s.mu.Lock()
defer s.mu.Unlock()
dropSTUN := 0
if v {
dropSTUN = 1
}
xdpConfig := &bpfConfig{
DstPort: uint16(s.dstPort),
DropStun: uint16(dropSTUN),
}
var key uint32
err := s.objs.ConfigMap.Put(key, xdpConfig)
if err == nil {
s.dropSTUN = v
}
return err
}
func (s *STUNServer) GetDropSTUN() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.dropSTUN
}
func (s *STUNServer) updateMetrics() error {
s.mu.Lock()
defer s.mu.Unlock()
iter := s.objs.CountersMap.Iterate()
var key bpfCountersKey
numCPU, err := ebpf.PossibleCPU()
if err != nil {
return err
}
vals := make([]uint64, numCPU)
for iter.Next(&key, &vals) {
err := s.metrics.updateFromMapKV(key, vals)
if err != nil {
return err
}
}
return iter.Err()
}