mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	This updates all source files to use a new standard header for copyright and license declaration. Notably, copyright no longer includes a date, and we now use the standard SPDX-License-Identifier header. This commit was done almost entirely mechanically with perl, and then some minimal manual fixes. Updates #6865 Signed-off-by: Will Norris <will@tailscale.com>
		
			
				
	
	
		
			260 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			260 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| package main
 | |
| 
 | |
| import (
 | |
| 	"encoding/binary"
 | |
| 	"fmt"
 | |
| 	"log"
 | |
| 	"net/netip"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"tailscale.com/net/packet"
 | |
| 	"tailscale.com/types/ipproto"
 | |
| )
 | |
| 
 | |
| type Snapshot struct {
 | |
| 	WhenNsec int64 // current time
 | |
| 	timeAcc  int64 // accumulated time (+NSecPerTx per transmit)
 | |
| 
 | |
| 	LastSeqTx    int64 // last sequence number sent
 | |
| 	LastSeqRx    int64 // last sequence number received
 | |
| 	TotalLost    int64 // packets out-of-order or lost so far
 | |
| 	TotalOOO     int64 // packets out-of-order so far
 | |
| 	TotalBytesRx int64 // total bytes received so far
 | |
| }
 | |
| 
 | |
| type Delta struct {
 | |
| 	DurationNsec int64
 | |
| 	TxPackets    int64
 | |
| 	RxPackets    int64
 | |
| 	LostPackets  int64
 | |
| 	OOOPackets   int64
 | |
| 	Bytes        int64
 | |
| }
 | |
| 
 | |
| func (b Snapshot) Sub(a Snapshot) Delta {
 | |
| 	return Delta{
 | |
| 		DurationNsec: b.WhenNsec - a.WhenNsec,
 | |
| 		TxPackets:    b.LastSeqTx - a.LastSeqTx,
 | |
| 		RxPackets: (b.LastSeqRx - a.LastSeqRx) -
 | |
| 			(b.TotalLost - a.TotalLost) +
 | |
| 			(b.TotalOOO - a.TotalOOO),
 | |
| 		LostPackets: b.TotalLost - a.TotalLost,
 | |
| 		OOOPackets:  b.TotalOOO - a.TotalOOO,
 | |
| 		Bytes:       b.TotalBytesRx - a.TotalBytesRx,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (d Delta) String() string {
 | |
| 	return fmt.Sprintf("tx=%-6d rx=%-4d (%6d = %.1f%% loss) (%d OOO) (%4.1f Mbit/s)",
 | |
| 		d.TxPackets, d.RxPackets, d.LostPackets,
 | |
| 		float64(d.LostPackets)*100/float64(d.TxPackets),
 | |
| 		d.OOOPackets,
 | |
| 		float64(d.Bytes)*8*1e9/float64(d.DurationNsec)/1e6)
 | |
| }
 | |
| 
 | |
| type TrafficGen struct {
 | |
| 	mu        sync.Mutex
 | |
| 	cur, prev Snapshot // snapshots used for rate control
 | |
| 	buf       []byte   // pre-generated packet buffer
 | |
| 	done      bool     // true if the test has completed
 | |
| 
 | |
| 	onFirstPacket func() // function to call on first received packet
 | |
| 
 | |
| 	// maxPackets is the max packets to receive (not send) before
 | |
| 	// ending the test. If it's zero, the test runs forever.
 | |
| 	maxPackets int64
 | |
| 
 | |
| 	// nsPerPacket is the target average nanoseconds between packets.
 | |
| 	// It's initially zero, which means transmit as fast as the
 | |
| 	// caller wants to go.
 | |
| 	nsPerPacket int64
 | |
| 
 | |
| 	// ppsHistory is the observed packets-per-second from recent
 | |
| 	// samples.
 | |
| 	ppsHistory [5]int64
 | |
| }
 | |
| 
 | |
| // NewTrafficGen creates a new, initially locked, TrafficGen.
 | |
| // Until Start() is called, Generate() will block forever.
 | |
| func NewTrafficGen(onFirstPacket func()) *TrafficGen {
 | |
| 	t := TrafficGen{
 | |
| 		onFirstPacket: onFirstPacket,
 | |
| 	}
 | |
| 
 | |
| 	// initially locked, until first Start()
 | |
| 	t.mu.Lock()
 | |
| 
 | |
| 	return &t
 | |
| }
 | |
| 
 | |
| // Start starts the traffic generator. It assumes mu is already locked,
 | |
| // and unlocks it.
 | |
| func (t *TrafficGen) Start(src, dst netip.Addr, bytesPerPacket int, maxPackets int64) {
 | |
| 	h12 := packet.ICMP4Header{
 | |
| 		IP4Header: packet.IP4Header{
 | |
| 			IPProto: ipproto.ICMPv4,
 | |
| 			IPID:    0,
 | |
| 			Src:     src,
 | |
| 			Dst:     dst,
 | |
| 		},
 | |
| 		Type: packet.ICMP4EchoRequest,
 | |
| 		Code: packet.ICMP4NoCode,
 | |
| 	}
 | |
| 
 | |
| 	// ensure there's room for ICMP header plus sequence number
 | |
| 	if bytesPerPacket < ICMPMinSize+8 {
 | |
| 		log.Fatalf("bytesPerPacket must be > 24+8")
 | |
| 	}
 | |
| 
 | |
| 	t.maxPackets = maxPackets
 | |
| 
 | |
| 	payload := make([]byte, bytesPerPacket-ICMPMinSize)
 | |
| 	t.buf = packet.Generate(h12, payload)
 | |
| 
 | |
| 	t.mu.Unlock()
 | |
| }
 | |
| 
 | |
| func (t *TrafficGen) Snap() Snapshot {
 | |
| 	t.mu.Lock()
 | |
| 	defer t.mu.Unlock()
 | |
| 
 | |
| 	t.cur.WhenNsec = time.Now().UnixNano()
 | |
| 	return t.cur
 | |
| }
 | |
| 
 | |
| func (t *TrafficGen) Running() bool {
 | |
| 	t.mu.Lock()
 | |
| 	defer t.mu.Unlock()
 | |
| 
 | |
| 	return !t.done
 | |
| }
 | |
| 
 | |
| // Generate produces the next packet in the sequence. It sleeps if
 | |
| // it's too soon for the next packet to be sent.
 | |
| //
 | |
| // The generated packet is placed into buf at offset ofs, for compatibility
 | |
| // with the wireguard-go conventions.
 | |
| //
 | |
| // The return value is the number of bytes generated in the packet, or 0
 | |
| // if the test has finished running.
 | |
| func (t *TrafficGen) Generate(b []byte, ofs int) int {
 | |
| 	t.mu.Lock()
 | |
| 
 | |
| 	now := time.Now().UnixNano()
 | |
| 	if t.nsPerPacket == 0 || t.cur.timeAcc == 0 {
 | |
| 		t.cur.timeAcc = now - 1
 | |
| 	}
 | |
| 	if t.cur.timeAcc >= now {
 | |
| 		// too soon
 | |
| 		t.mu.Unlock()
 | |
| 		time.Sleep(time.Duration(t.cur.timeAcc-now) * time.Nanosecond)
 | |
| 		t.mu.Lock()
 | |
| 
 | |
| 		now = t.cur.timeAcc
 | |
| 	}
 | |
| 	if t.done {
 | |
| 		t.mu.Unlock()
 | |
| 		return 0
 | |
| 	}
 | |
| 
 | |
| 	t.cur.timeAcc += t.nsPerPacket
 | |
| 	t.cur.LastSeqTx += 1
 | |
| 	t.cur.WhenNsec = now
 | |
| 	seq := t.cur.LastSeqTx
 | |
| 
 | |
| 	t.mu.Unlock()
 | |
| 
 | |
| 	copy(b[ofs:], t.buf)
 | |
| 	binary.BigEndian.PutUint64(
 | |
| 		b[ofs+ICMPMinSize:ofs+ICMPMinSize+8],
 | |
| 		uint64(seq))
 | |
| 
 | |
| 	return len(t.buf)
 | |
| }
 | |
| 
 | |
| // GotPacket processes a packet that came back on the receive side.
 | |
| func (t *TrafficGen) GotPacket(b []byte, ofs int) {
 | |
| 	t.mu.Lock()
 | |
| 	defer t.mu.Unlock()
 | |
| 
 | |
| 	s := &t.cur
 | |
| 	seq := int64(binary.BigEndian.Uint64(
 | |
| 		b[ofs+ICMPMinSize : ofs+ICMPMinSize+8]))
 | |
| 	if seq > s.LastSeqRx {
 | |
| 		if s.LastSeqRx > 0 {
 | |
| 			// only count lost packets after the very first
 | |
| 			// successful one.
 | |
| 			s.TotalLost += seq - s.LastSeqRx - 1
 | |
| 		}
 | |
| 		s.LastSeqRx = seq
 | |
| 	} else {
 | |
| 		s.TotalOOO += 1
 | |
| 	}
 | |
| 
 | |
| 	// +1 packet since we only start counting after the first one
 | |
| 	if t.maxPackets > 0 && s.LastSeqRx >= t.maxPackets+1 {
 | |
| 		t.done = true
 | |
| 	}
 | |
| 	s.TotalBytesRx += int64(len(b) - ofs)
 | |
| 
 | |
| 	f := t.onFirstPacket
 | |
| 	t.onFirstPacket = nil
 | |
| 	if f != nil {
 | |
| 		f()
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Adjust tunes the transmit rate based on the received packets.
 | |
| // The goal is to converge on the fastest transmit rate that still has
 | |
| // minimal packet loss. Returns the new target rate in packets/sec.
 | |
| //
 | |
| // We need to play this guessing game in order to balance out tx and rx
 | |
| // rates when there's a lossy network between them. Otherwise we can end
 | |
| // up using 99% of the CPU to blast out transmitted packets and leaving only
 | |
| // 1% to receive them, leading to a misleading throughput calculation.
 | |
| //
 | |
| // Call this function multiple times per second.
 | |
| func (t *TrafficGen) Adjust() (pps int64) {
 | |
| 	t.mu.Lock()
 | |
| 	defer t.mu.Unlock()
 | |
| 
 | |
| 	d := t.cur.Sub(t.prev)
 | |
| 
 | |
| 	// don't adjust rate until the first full period *after* receiving
 | |
| 	// the first packet. This skips any handshake time in the underlying
 | |
| 	// transport.
 | |
| 	if t.prev.LastSeqRx == 0 || d.DurationNsec == 0 {
 | |
| 		t.prev = t.cur
 | |
| 		return 0 // no estimate yet, continue at max speed
 | |
| 	}
 | |
| 
 | |
| 	pps = int64(d.RxPackets) * 1e9 / int64(d.DurationNsec)
 | |
| 
 | |
| 	// We use a rate selection algorithm based loosely on TCP BBR.
 | |
| 	// Basically, we set the transmit rate to be a bit higher than
 | |
| 	// the best observed transmit rate in the last several time
 | |
| 	// periods. This guarantees some packet loss, but should converge
 | |
| 	// quickly on a rate near the sustainable maximum.
 | |
| 	bestPPS := pps
 | |
| 	for _, p := range t.ppsHistory {
 | |
| 		if p > bestPPS {
 | |
| 			bestPPS = p
 | |
| 		}
 | |
| 	}
 | |
| 	if pps > 0 && t.prev.WhenNsec > 0 {
 | |
| 		copy(t.ppsHistory[1:], t.ppsHistory[0:len(t.ppsHistory)-1])
 | |
| 		t.ppsHistory[0] = pps
 | |
| 	}
 | |
| 	if bestPPS > 0 {
 | |
| 		pps = bestPPS * 103 / 100
 | |
| 		t.nsPerPacket = int64(1e9 / pps)
 | |
| 	}
 | |
| 	t.prev = t.cur
 | |
| 
 | |
| 	return pps
 | |
| }
 |