irtt/ 0000755 0001751 0001751 00000000000 13240047124 010513 5 ustar pete pete irtt/event.go 0000644 0001751 0001751 00000002417 13240047124 012167 0 ustar pete pete package irtt
import (
"fmt"
"net"
)
// Code uniquely identifies events and errors to improve context.
type Code int
//go:generate stringer -type=Code
// Server event codes.
const (
MultipleAddresses Code = iota + 1*1024
ServerStart
ServerStop
ListenerStart
ListenerStop
ListenerError
Drop
NewConn
OpenClose
CloseConn
NoDSCPSupport
ExceededDuration
NoReceiveDstAddrSupport
RemoveNoConn
InvalidServerFill
)
// Client event codes.
const (
Connecting Code = iota + 2*1024
Connected
WaitForPackets
ServerRestriction
NoTest
ConnectedClosed
)
// Event is an event sent to a Handler.
type Event struct {
Code Code
LocalAddr *net.UDPAddr
RemoteAddr *net.UDPAddr
format string
Detail []interface{}
}
// Eventf returns a new event.
func Eventf(code Code, laddr *net.UDPAddr, raddr *net.UDPAddr, format string,
detail ...interface{}) *Event {
return &Event{code, laddr, raddr, format, detail}
}
func (e *Event) String() string {
msg := fmt.Sprintf(e.format, e.Detail...)
if e.RemoteAddr != nil {
return fmt.Sprintf("[%s] [%s] %s", e.RemoteAddr, e.Code.String(), msg)
}
return fmt.Sprintf("[%s] %s", e.Code.String(), msg)
}
// Handler is called with events.
type Handler interface {
// OnEvent is called when an event occurs.
OnEvent(e *Event)
}
irtt/params.go 0000644 0001751 0001751 00000010657 13240047124 012336 0 ustar pete pete package irtt
import (
"encoding/binary"
"time"
)
type paramType int
const paramsMaxLen = 128
const (
pProtocolVersion = iota + 1
pDuration
pInterval
pLength
pReceivedStats
pStampAt
pClock
pDSCP
pServerFill
)
// Params are the test parameters sent to and received from the server.
type Params struct {
ProtocolVersion int `json:"proto_version"`
Duration time.Duration `json:"duration"`
Interval time.Duration `json:"interval"`
Length int `json:"length"`
ReceivedStats ReceivedStats `json:"received_stats"`
StampAt StampAt `json:"stamp_at"`
Clock Clock `json:"clock"`
DSCP int `json:"dscp"`
ServerFill string `json:"server_fill"`
}
func parseParams(b []byte) (*Params, error) {
p := &Params{}
for pos := 0; pos < len(b); {
n, err := p.readParam(b[pos:])
if err != nil {
return nil, err
}
pos += n
}
return p, nil
}
func (p *Params) bytes() []byte {
b := make([]byte, paramsMaxLen)
pos := 0
if p.ProtocolVersion != 0 {
pos += binary.PutUvarint(b[pos:], pProtocolVersion)
pos += binary.PutVarint(b[pos:], int64(p.ProtocolVersion))
}
if p.Duration != 0 {
pos += binary.PutUvarint(b[pos:], pDuration)
pos += binary.PutVarint(b[pos:], int64(p.Duration))
}
if p.Interval != 0 {
pos += binary.PutUvarint(b[pos:], pInterval)
pos += binary.PutVarint(b[pos:], int64(p.Interval))
}
if p.Length != 0 {
pos += binary.PutUvarint(b[pos:], pLength)
pos += binary.PutVarint(b[pos:], int64(p.Length))
}
if p.ReceivedStats != 0 {
pos += binary.PutUvarint(b[pos:], pReceivedStats)
pos += binary.PutVarint(b[pos:], int64(p.ReceivedStats))
}
if p.StampAt != 0 {
pos += binary.PutUvarint(b[pos:], pStampAt)
pos += binary.PutVarint(b[pos:], int64(p.StampAt))
}
if p.Clock != 0 {
pos += binary.PutUvarint(b[pos:], pClock)
pos += binary.PutVarint(b[pos:], int64(p.Clock))
}
if p.DSCP != 0 {
pos += binary.PutUvarint(b[pos:], pDSCP)
pos += binary.PutVarint(b[pos:], int64(p.DSCP))
}
if len(p.ServerFill) > 0 {
pos += binary.PutUvarint(b[pos:], pServerFill)
pos += putString(b[pos:], p.ServerFill, maxServerFillLen)
}
return b[:pos]
}
func (p *Params) readParam(b []byte) (pos int, err error) {
var t uint64
var n int
t, n, err = readUvarint(b[pos:])
if err != nil {
return
}
pos += n
if t == pServerFill {
p.ServerFill, n, err = readString(b[pos:], maxServerFillLen)
if err != nil {
return
}
} else {
var v int64
v, n, err = readVarint(b[pos:])
if err != nil {
return
}
switch t {
case pProtocolVersion:
p.ProtocolVersion = int(v)
case pDuration:
p.Duration = time.Duration(v)
if p.Duration <= 0 {
err = Errorf(InvalidParamValue, "duration %d is <= 0", p.Duration)
}
case pInterval:
p.Interval = time.Duration(v)
if p.Interval <= 0 {
err = Errorf(InvalidParamValue, "interval %d is <= 0", p.Interval)
}
case pLength:
p.Length = int(v)
case pReceivedStats:
p.ReceivedStats, err = ReceivedStatsFromInt(int(v))
case pStampAt:
p.StampAt, err = StampAtFromInt(int(v))
case pClock:
p.Clock, err = ClockFromInt(int(v))
case pDSCP:
p.DSCP = int(v)
default:
// note: unknown params are silently ignored
}
}
if err != nil {
return
}
pos += n
return
}
func readUvarint(b []byte) (v uint64, n int, err error) {
v, n = binary.Uvarint(b)
if n == 0 {
err = Errorf(ShortParamBuffer,
"param buffer too short for uvarint (%d)", len(b))
}
if n < 0 {
err = Errorf(ParamOverflow,
"param value overflow for uvarint (read %d)", n)
}
return
}
func readVarint(b []byte) (v int64, n int, err error) {
v, n = binary.Varint(b)
if n == 0 {
err = Errorf(ShortParamBuffer,
"param buffer too short for varint (%d)", len(b))
}
if n < 0 {
err = Errorf(ParamOverflow,
"param value overflow for varint (read %d)", n)
}
return
}
func readString(b []byte, maxLen int) (v string, n int, err error) {
l, n, err := readUvarint(b[n:])
if err != nil {
return
}
if l > uint64(maxLen) {
err = Errorf(ParamOverflow, "string param too large (%d>%d)", l, maxLen)
return
}
if len(b[n:]) < int(l) {
err = Errorf(ShortParamBuffer,
"param buffer (%d) too short for string (%d)", len(b[n:]), l)
return
}
v = string(b[n : n+int(l)])
n += int(l)
return
}
func putString(b []byte, s string, maxLen int) (n int) {
l := len(s)
if l > maxLen {
l = maxLen
}
n += binary.PutUvarint(b[n:], uint64(l))
n += copy(b[n:], s[:l])
return
}
irtt/irtt_server.go 0000644 0001751 0001751 00000015652 13240047124 013423 0 ustar pete pete package irtt
import (
"os"
"os/signal"
"strings"
"syscall"
flag "github.com/ogier/pflag"
)
func serverUsage() {
setBufio()
printf("Options:")
printf("--------")
printf("")
printf("-b addresses bind addresses (default \"%s\"), comma separated list of:", strings.Join(DefaultBindAddrs, ","))
printf(" :port (unspecified address with port, use with care)")
printf(" host (host with default port %s, see Host formats below)", DefaultPort)
printf(" host:port (host with specified port, see Host formats below)")
printf(" %%iface (all addresses on interface iface with default port %s)", DefaultPort)
printf(" %%iface:port (all addresses on interface iface with port)")
printf(" note: iface strings may contain * to match multiple interfaces")
printf("-d duration max test duration, or 0 for no maximum")
printf(" (default %s, see Duration units below)", DefaultMaxDuration)
printf("-i interval min send interval, or 0 for no minimum")
printf(" (default %s, see Duration units below)", DefaultMinInterval)
printf("-l length max packet length (default %d), or 0 for no maximum", DefaultMaxLength)
printf(" numbers too small will cause test packets to be dropped")
printf("--hmac=key add HMAC with key (0x for hex) to all packets, provides:")
printf(" dropping of all packets without a correct HMAC")
printf(" protection for server against unauthorized discovery and use")
printf("--timeout=dur timeout for closing connections if no requests received")
printf(" 0 means no timeout (not recommended on public servers)")
printf(" max client interval will be restricted to timeout/%d", maxIntervalTimeoutFactor)
printf(" (default %s, see Duration units below)", DefaultServerTimeout)
printf("--pburst=# packet burst allowed before enforcing minimum interval")
printf(" (default %d)", DefaultPacketBurst)
printf("--fill=fill payload fill if not requested (default %s)", DefaultServerFiller.String())
printf(" none: echo client payload (insecure on public servers)")
for _, ffac := range FillerFactories {
printf(" %s", ffac.Usage)
}
printf("--allow-fills= comma separated patterns of fill requests to allow (default %s)", strings.Join(DefaultAllowFills, ","))
printf(" fills see options for --fill")
printf(" allowing non-random fills insecure on public servers")
printf(" use --allow-fills=\"\" to disallow all fill requests")
printf(" note: patterns may contain * for matching")
printf("--tstamp=modes timestamp modes to allow (default %s)", DefaultAllowStamp)
printf(" none: don't allow timestamps")
printf(" single: allow a single timestamp (send, receive or midpoint)")
printf(" dual: allow dual timestamps")
printf("--no-dscp don't allow setting dscp (default %t)", !DefaultAllowDSCP)
printf("--set-src-ip set source IP address on all outgoing packets from listeners")
printf(" on unspecified IP addresses (use for more reliable reply")
printf(" routing, but increases per-packet heap allocations)")
printf("--gc=mode sets garbage collection mode (default %s)", DefaultGCMode)
printf(" on: garbage collector always on")
printf(" off: garbage collector always off")
printf(" idle: garbage collector enabled only when idle")
printf("--thread lock request handling goroutines to OS threads")
printf("-h show help")
printf("-v show version")
printf("")
hostUsage()
printf("")
durationUsage()
}
// runServerCLI runs the server command line interface.
func runServerCLI(args []string) {
// server flags
fs := flag.NewFlagSet("server", 0)
fs.Usage = func() {
usageAndExit(serverUsage, exitCodeBadCommandLine)
}
var baddrsStr = fs.StringP("b", "b", strings.Join(DefaultBindAddrs, ","), "bind addresses")
var maxDuration = fs.DurationP("d", "d", DefaultMaxDuration, "max duration")
var minInterval = fs.DurationP("i", "i", DefaultMinInterval, "min interval")
var maxLength = fs.IntP("l", "l", DefaultMaxLength, "max length")
var allowTimestampStr = fs.String("tstamp", DefaultAllowStamp.String(), "allow timestamp")
var hmacStr = fs.String("hmac", defaultHMACKey, "HMAC key")
var timeout = fs.Duration("timeout", DefaultServerTimeout, "timeout")
var packetBurst = fs.Int("pburst", DefaultPacketBurst, "packet burst")
var fillStr = fs.String("fill", DefaultServerFiller.String(), "fill")
var allowFillsStr = fs.String("allow-fills", strings.Join(DefaultAllowFills, ","), "sfill")
var ipv4 = fs.BoolP("4", "4", false, "IPv4 only")
var ipv6 = fs.BoolP("6", "6", false, "IPv6 only")
var ttl = fs.Int("ttl", DefaultTTL, "IP time to live")
var noDSCP = fs.Bool("no-dscp", !DefaultAllowDSCP, "no DSCP")
var setSrcIP = fs.Bool("set-src-ip", DefaultSetSrcIP, "set source IP")
var gcModeStr = fs.String("gc", DefaultGCMode.String(), "gc mode")
var lockOSThread = fs.Bool("thread", DefaultThreadLock, "thread")
var version = fs.BoolP("version", "v", false, "version")
fs.Parse(args)
// start profiling, if enabled in build
if profileEnabled {
defer startProfile("./server.pprof").Stop()
}
// version
if *version {
runVersion(args)
os.Exit(0)
}
// determine IP version
ipVer := IPVersionFromBooleans(*ipv4, *ipv6, DualStack)
// parse allow stamp string
allowStamp, err := ParseAllowStamp(*allowTimestampStr)
exitOnError(err, exitCodeBadCommandLine)
// parse fill
filler, err := NewFiller(*fillStr)
exitOnError(err, exitCodeBadCommandLine)
// parse HMAC key
var hmacKey []byte
if *hmacStr != "" {
hmacKey, err = decodeHexOrNot(*hmacStr)
exitOnError(err, exitCodeBadCommandLine)
}
// parse GC mode
gcMode, err := ParseGCMode(*gcModeStr)
exitOnError(err, exitCodeBadCommandLine)
// create server config
cfg := NewServerConfig()
cfg.Addrs = strings.Split(*baddrsStr, ",")
cfg.MaxDuration = *maxDuration
cfg.MinInterval = *minInterval
cfg.AllowStamp = allowStamp
cfg.HMACKey = hmacKey
cfg.Timeout = *timeout
cfg.PacketBurst = *packetBurst
cfg.MaxLength = *maxLength
cfg.Filler = filler
cfg.AllowFills = strings.Split(*allowFillsStr, ",")
cfg.AllowDSCP = !*noDSCP
cfg.TTL = *ttl
cfg.Handler = &serverHandler{}
cfg.IPVersion = ipVer
cfg.SetSrcIP = *setSrcIP
cfg.GCMode = gcMode
cfg.ThreadLock = *lockOSThread
// create server
s := NewServer(cfg)
// install signal handler to stop server
sigs := make(chan os.Signal, 1)
signal.Notify(sigs,
syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
sig := <-sigs
printf("%s", sig)
s.Shutdown()
sig = <-sigs
os.Exit(exitCodeDoubleSignal)
}()
err = s.ListenAndServe()
exitOnError(err, exitCodeRuntimeError)
}
type serverHandler struct {
}
func (s *serverHandler) OnEvent(ev *Event) {
println(ev.String())
}
irtt/version.go 0000644 0001751 0001751 00000001250 13240047124 012525 0 ustar pete pete package irtt
// Version is the IRTT version number (replaced during build).
var Version = "0.9.0"
// ProtocolVersion is the protocol version number, which must match between client
// and server.
var ProtocolVersion = 1
// JSONFormatVersion is the JSON format number.
var JSONFormatVersion = 1
// VersionInfo stores the version information.
type VersionInfo struct {
IRTT string `json:"irtt"`
Protocol int `json:"protocol"`
JSONFormat int `json:"json_format"`
}
// NewVersionInfo returns a new VersionInfo.
func NewVersionInfo() *VersionInfo {
return &VersionInfo{
IRTT: Version,
Protocol: ProtocolVersion,
JSONFormat: JSONFormatVersion,
}
}
irtt/df.go 0000644 0001751 0001751 00000001062 13240047124 011432 0 ustar pete pete package irtt
import (
"fmt"
)
// DF is the value for the do not fragment bit.
type DF int
// DF constants.
const (
DFDefault DF = iota
DFFalse
DFTrue
)
var dfs = [...]string{"default", "false", "true"}
func (d DF) String() string {
if int(d) < 0 || int(d) > len(dfs) {
return fmt.Sprintf("DF:%d", d)
}
return dfs[int(d)]
}
// ParseDF returns a DF value from its string.
func ParseDF(s string) (DF, error) {
for i, x := range dfs {
if x == s {
return DF(i), nil
}
}
return DFDefault, Errorf(InvalidDFString, "invalid DF string: %s", s)
}
irtt/sconfig.go 0000644 0001751 0001751 00000002124 13240047124 012471 0 ustar pete pete package irtt
import "time"
// ServerConfig defines the Server configuration.
type ServerConfig struct {
Addrs []string
HMACKey []byte
MaxDuration time.Duration
MinInterval time.Duration
MaxLength int
Timeout time.Duration
PacketBurst int
Filler Filler
AllowFills []string
AllowStamp AllowStamp
AllowDSCP bool
TTL int
IPVersion IPVersion
Handler Handler
SetSrcIP bool
GCMode GCMode
ThreadLock bool
}
// NewServerConfig returns a new ServerConfig with the default settings.
func NewServerConfig() *ServerConfig {
return &ServerConfig{
Addrs: DefaultBindAddrs,
MaxDuration: DefaultMaxDuration,
MinInterval: DefaultMinInterval,
MaxLength: DefaultMaxLength,
Timeout: DefaultServerTimeout,
PacketBurst: DefaultPacketBurst,
Filler: DefaultServerFiller,
AllowFills: DefaultAllowFills,
AllowStamp: DefaultAllowStamp,
AllowDSCP: DefaultAllowDSCP,
TTL: DefaultTTL,
IPVersion: DefaultIPVersion,
SetSrcIP: DefaultSetSrcIP,
GCMode: DefaultGCMode,
ThreadLock: DefaultThreadLock,
}
}
irtt/cconfig.go 0000644 0001751 0001751 00000006003 13240047124 012451 0 ustar pete pete package irtt
import (
"encoding/json"
"net"
)
// ClientConfig defines the Client configuration.
type ClientConfig struct {
LocalAddress string
RemoteAddress string
LocalAddr net.Addr
RemoteAddr net.Addr
OpenTimeouts Durations
NoTest bool
Params
Loose bool
IPVersion IPVersion
DF DF
TTL int
Timer Timer
Waiter Waiter
Filler Filler
FillOne bool
HMACKey []byte
Handler ClientHandler
ThreadLock bool
Supplied *ClientConfig
}
// NewClientConfig returns a new ClientConfig with the default settings.
func NewClientConfig() *ClientConfig {
return &ClientConfig{
LocalAddress: DefaultLocalAddress,
OpenTimeouts: DefaultOpenTimeouts,
Params: Params{
ProtocolVersion: ProtocolVersion,
Duration: DefaultDuration,
Interval: DefaultInterval,
Length: DefaultLength,
StampAt: DefaultStampAt,
Clock: DefaultClock,
DSCP: DefaultDSCP,
},
Loose: DefaultLoose,
IPVersion: DefaultIPVersion,
DF: DefaultDF,
TTL: DefaultTTL,
Timer: DefaultTimer,
Waiter: DefaultWait,
ThreadLock: DefaultThreadLock,
}
}
// validate validates the configuration
func (c *ClientConfig) validate() error {
if c.Interval <= 0 {
return Errorf(IntervalNonPositive, "interval (%s) must be > 0", c.Interval)
}
if c.Duration <= 0 {
return Errorf(DurationNonPositive, "duration (%s) must be > 0", c.Duration)
}
if len(c.ServerFill) > maxServerFillLen {
return Errorf(ServerFillTooLong,
"server fill string (%s) must be less than %d characters",
c.ServerFill, maxServerFillLen)
}
return validateInterval(c.Interval)
}
// MarshalJSON implements the json.Marshaler interface.
func (c *ClientConfig) MarshalJSON() ([]byte, error) {
fstr := "none"
if c.Filler != nil {
fstr = c.Filler.String()
}
j := &struct {
LocalAddress string `json:"local_address"`
RemoteAddress string `json:"remote_address"`
OpenTimeouts string `json:"open_timeouts"`
Params `json:"params"`
Loose bool `json:"loose"`
IPVersion IPVersion `json:"ip_version"`
DF DF `json:"df"`
TTL int `json:"ttl"`
Timer string `json:"timer"`
Waiter string `json:"waiter"`
Filler string `json:"filler"`
FillOne bool `json:"fill_one"`
ServerFill string `json:"server_fill"`
ThreadLock bool `json:"thread_lock"`
Supplied *ClientConfig `json:"supplied,omitempty"`
}{
LocalAddress: c.LocalAddress,
RemoteAddress: c.RemoteAddress,
OpenTimeouts: c.OpenTimeouts.String(),
Params: c.Params,
Loose: c.Loose,
IPVersion: c.IPVersion,
DF: c.DF,
TTL: c.TTL,
Timer: c.Timer.String(),
Waiter: c.Waiter.String(),
Filler: fstr,
FillOne: c.FillOne,
ServerFill: c.ServerFill,
ThreadLock: c.ThreadLock,
Supplied: c.Supplied,
}
return json.Marshal(j)
}
irtt/bitrate.go 0000644 0001751 0001751 00000002522 13240047124 012475 0 ustar pete pete package irtt
import (
"encoding/json"
"fmt"
"time"
)
// Bitrate is a bit rate in bits per second.
type Bitrate uint64
func calculateBitrate(n uint64, d time.Duration) Bitrate {
if n == 0 || d == 0 {
return Bitrate(0)
}
return Bitrate(8 * float64(n) / d.Seconds())
}
// String returns a Bitrate string in appropriate units.
func (r Bitrate) String() string {
// Yes, it's exhaustive, just for fun. A 64-int unsigned int can't hold
// Yottabits per second as 1e21 overflows it. If this problem affects
// you, thanks for solving climate change!
if r < 1000 {
return fmt.Sprintf("%d bps", r)
} else if r < 1e6 {
return fmt.Sprintf("%.1f Kbps", float64(r)/float64(1000))
} else if r < 1e9 {
return fmt.Sprintf("%.2f Mbps", float64(r)/float64(1e6))
} else if r < 1e12 {
return fmt.Sprintf("%.3f Gbps", float64(r)/float64(1e9))
} else if r < 1e15 {
return fmt.Sprintf("%.3f Pbps", float64(r)/float64(1e12))
} else if r < 1e18 {
return fmt.Sprintf("%.3f Ebps", float64(r)/float64(1e15))
}
return fmt.Sprintf("%.3f Zbps", float64(r)/float64(1e18))
}
// MarshalJSON implements the json.Marshaler interface.
func (r Bitrate) MarshalJSON() ([]byte, error) {
type Alias DurationStats
j := &struct {
BPS uint64 `json:"bps"`
String string `json:"string"`
}{
BPS: uint64(r),
String: r.String(),
}
return json.Marshal(j)
}
irtt/build.sh 0000755 0001751 0001751 00000003202 13240047124 012146 0 ustar pete pete #!/bin/sh
# This script may be used during development for making builds and generating doc.
# Requirements:
# - stringer (go get -u -a golang.org/x/tools/cmd/stringer)
# - pandoc (apt-get install pandoc OR brew install pandoc)
action="build"
pkg="github.com/peteheist/irtt/cmd/irtt"
ldflags=""
linkshared=""
tags=""
race=""
env=""
# html filter
html_filter() {
sed 's/
/
/g'
}
# interpret keywords
for a in $*; do
case "$a" in
"install") action="install"
ldflags="$ldflags -s -w"
;;
"nobuild") nobuild="1"
;;
"nodoc") nodoc="1"
;;
"min") ldflags="$ldflags -s -w"
;;
"linkshared") linkshared="-linkshared"
;;
"race") race="-race"
;;
"profile") tags="$tags profile"
;;
"prod") tags="$tags prod"
;;
"linux-386"|"linux") env="GOOS=linux GOARCH=386"
;;
"linux-amd64"|"linux64") env="GOOS=linux GOARCH=amd64"
;;
"linux-arm"|"rpi") env="GOOS=linux GOARCH=arm"
;;
"linux-mips64"|"erl") env="GOOS=linux GOARCH=mips"
;;
"linux-mipsle"|"erx"|"om2p") env="GOOS=linux GOARCH=mipsle"
;;
"darwin-amd64"|"osx") env="GOOS=darwin GOARCH=amd64"
;;
"win"|"windows") env="GOOS=windows GOARCH=386"
;;
"win64"|"windows64") env="GOOS=windows GOARCH=amd64"
;;
*) echo "Unknown parameter: $a"
exit 1
;;
esac
done
# build source
if [ -z "$nobuild" ]; then
go generate
eval $env go $action -tags \'$tags\' $race -ldflags=\'$ldflags\' $linkshared $pkg
fi
# generate docs
if [ -z "$nodoc" ]; then
for f in irtt irtt-client irtt-server; do
pandoc -s -t man doc/$f.md -o doc/$f.1
pandoc -t html -H doc/head.html doc/$f.md | html_filter > doc/$f.html
done
fi
irtt/irtt_bench.go 0000644 0001751 0001751 00000002731 13240047124 013166 0 ustar pete pete package irtt
import (
"crypto/hmac"
"crypto/md5"
"crypto/rand"
"io"
mrand "math/rand"
"time"
)
func runBenchBufTest(fn func([]byte)) {
lengths := []int{16, 32, 64, 172, 1472, 8972}
printf("")
for _, l := range lengths {
buf := make([]byte, l)
end := time.Now().Add(1 * time.Second)
i := 0
elapsed := time.Duration(0)
for time.Now().Before(end) {
if _, err := io.ReadFull(rand.Reader, buf); err != nil {
panic(err)
}
start := time.Now()
fn(buf)
elapsed += time.Since(start)
i++
}
printf("len %d, %d iterations, %.0f ns/op, %.0f Mbps", l,
i, float64(elapsed)/float64(i),
8000.0*float64(l)*float64(i)/float64(elapsed))
}
}
func testHMAC() {
printf("Testing HMAC...")
key := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, key[:]); err != nil {
panic(err)
}
md5Hash := hmac.New(md5.New, key)
runBenchBufTest(func(b []byte) {
md5Hash.Reset()
md5Hash.Write(b)
md5Hash.Sum(nil)
})
}
func testPatternFill() {
printf("Testing pattern fill...")
patlen := 4
pattern := make([]byte, patlen)
if _, err := io.ReadFull(rand.Reader, pattern[:]); err != nil {
panic(err)
}
bp := NewPatternFiller(pattern)
runBenchBufTest(func(b []byte) {
bp.Read(b)
})
}
func testRandFill() {
printf("Testing random fill...")
r := mrand.New(mrand.NewSource(time.Now().UnixNano()))
runBenchBufTest(func(b []byte) {
r.Read(b)
})
}
func runBench(args []string) {
testHMAC()
printf("")
testPatternFill()
printf("")
testRandFill()
}
irtt/LICENSE 0000644 0001751 0001751 00000104505 13240047124 011525 0 ustar pete pete GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{one line to give the program's name and a brief idea of what it does.}
Copyright (C) {year} {name of author}
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
{project} Copyright (C) {year} {fullname}
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
irtt/defaults.go 0000644 0001751 0001751 00000007373 13240047124 012663 0 ustar pete pete package irtt
import (
"time"
)
// Common defaults.
const (
DefaultIPVersion = DualStack
DefaultPort = "2112"
DefaultPortInt = 2112
DefaultTTL = 0
DefaultThreadLock = false
)
// Client defaults.
const (
DefaultDuration = 1 * time.Minute
DefaultInterval = 1 * time.Second
DefaultLength = 0
DefaultReceivedStats = ReceivedStatsBoth
DefaultStampAt = AtBoth
DefaultClock = BothClocks
DefaultDSCP = 0
DefaultLoose = false
DefaultLocalAddress = ":0"
DefaultLocalPort = "0"
DefaultDF = DFDefault
DefaultCompTimerMinErrorFactor = 0.0
DefaultCompTimerMaxErrorFactor = 2.0
DefaultHybridTimerSleepFactor = 0.95
DefaultAverageWindow = 5
DefaultExponentialAverageAlpha = 0.1
)
// DefaultOpenTimeouts are the default timeouts used when the client opens a
// connection to the server.
var DefaultOpenTimeouts = Durations([]time.Duration{
1 * time.Second,
2 * time.Second,
4 * time.Second,
8 * time.Second,
})
// DefaultCompTimerAverage is the default timer error averaging algorithm for
// the CompTimer.
var DefaultCompTimerAverage = NewDefaultExponentialAverager()
// DefaultWait is the default client wait time for the final responses after all
// packets have been sent.
var DefaultWait = &WaitMaxRTT{time.Duration(4) * time.Second, 3}
// DefaultTimer is the default timer implementation, CompTimer.
var DefaultTimer = NewCompTimer(DefaultCompTimerAverage)
// DefaultFillPattern is the default fill pattern.
var DefaultFillPattern = []byte("irtt")
// DefaultServerFiller it the default filler for the server, PatternFiller.
var DefaultServerFiller = NewDefaultPatternFiller()
// Server defaults.
const (
DefaultMaxDuration = time.Duration(0)
DefaultMinInterval = 10 * time.Millisecond
DefaultMaxLength = 0
DefaultServerTimeout = 1 * time.Minute
DefaultPacketBurst = 5
DefaultAllowStamp = DualStamps
DefaultAllowDSCP = true
DefaultSetSrcIP = false
DefaultGCMode = GCOn
)
// DefaultBindAddrs are the default bind addresses.
var DefaultBindAddrs = []string{":2112"}
// DefaultAllowFills are the default allowed fill prefixes.
var DefaultAllowFills = []string{"rand"}
// server duplicates and drops for testing (0.0-1.0)
const serverDupsPercent = 0
const serverDropsPercent = 0
// grace period for connection closure due to timeout
const timeoutGrace = 5 * time.Second
// factor of timeout used for maximum interval
const maxIntervalTimeoutFactor = 4
// max test duration grace period
const maxDurationGrace = 2 * time.Second
// ignore server restrictions (for testing hard limits)
const ignoreServerRestrictions = false
// settings for testing
const clientDropsPercent = 0
// minOpenTimeout sets the minimum time open() will wait before sending the
// next packet. This prevents clients from requesting a timeout that sends
// packets to the server too quickly.
const minOpenTimeout = 200 * time.Millisecond
// maximum initial length of pattern filler buffer
const patternMaxInitLen = 4 * 1024
// maxMTU is the MTU used if it could not be determined by autodetection.
const maxMTU = 64 * 1024
// minimum valid MTU per RFC 791
const minValidMTU = 68
// number of sconns to check to remove on each add (2 seems to be the least
// aggresive number where the map size still levels off over time, but I use 5
// to clean up unused sconns more quickly)
const checkExpiredCount = 5
// initial capacity for sconns map
const sconnsInitSize = 32
// maximum length of server fill string
const maxServerFillLen = 32
// minRestrictedInterval is the minimum restricted interval that the client will
// accept from the server.
const minRestrictedInterval = 1 * time.Second
irtt/prof_on.go 0000644 0001751 0001751 00000000426 13240047124 012506 0 ustar pete pete // +build profile
package irtt
import (
"github.com/pkg/profile"
)
const profileEnabled = true
func startProfile(path string) interface {
Stop()
} {
//debug.SetGCPercent(-1)
return profile.Start(profile.CPUProfile, profile.ProfilePath(path),
profile.NoShutdownHook)
}
irtt/bytes.go 0000644 0001751 0001751 00000001253 13240047124 012171 0 ustar pete pete package irtt
import (
"encoding/hex"
"strings"
)
// bytes helpers
// make zeroes array so we can use copy builtin for fast zero-ing
var zeroes = make([]byte, 64*1024)
func decodeHexOrNot(s string) (b []byte, err error) {
if strings.HasPrefix(s, "0x") {
b, err = hex.DecodeString(s[2:])
return
}
b = []byte(s)
return
}
func bytesEqual(a, b []byte) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func zero(b []byte) {
if len(b) > len(zeroes) {
zeroes = make([]byte, len(b)*2)
}
copy(b, zeroes)
}
irtt/timer.go 0000644 0001751 0001751 00000016544 13240047124 012174 0 ustar pete pete package irtt
import (
"context"
"fmt"
"strconv"
"strings"
"time"
)
// Timer is implemented to wait for the next send.
type Timer interface {
// Sleep waits for at least duration d and returns the current time. The
// current time is passed as t as a convenience for timers performing error
// compensation. Timers should obey the Context and use a select that
// includes ctx.Done() so that the sleep can be terminated early. In that
// case, ctx.Err() should be returned.
Sleep(ctx context.Context, t time.Time, d time.Duration) (time.Time, error)
String() string
}
// SimpleTimer uses Go's default time functions. It must be created using
// NewSimpleTimer.
type SimpleTimer struct {
timer *time.Timer
}
// NewSimpleTimer returns a new SimpleTimer.
func NewSimpleTimer() *SimpleTimer {
t := time.NewTimer(0)
<-t.C
return &SimpleTimer{t}
}
// Sleep selects on both a time.Timer channel and the done channel.
func (st *SimpleTimer) Sleep(ctx context.Context, t time.Time, d time.Duration) (time.Time, error) {
st.timer.Reset(d)
select {
case t := <-st.timer.C:
return t, nil
case <-ctx.Done():
// stop and drain timer for cleanliness
if !st.timer.Stop() {
<-st.timer.C
}
return time.Now(), ctx.Err()
}
}
func (st *SimpleTimer) String() string {
return "simple"
}
// CompTimer uses Go's default time functions and performs compensation by
// continually measuring the timer error and applying a correction factor to try
// to improve precision. It must be created using NewCompTimer. MinErrorFactor
// and MaxErrorFactor may be adjusted to reject correction factor outliers,
// which may be seen before enough data is collected. They default to 0 and 2,
// respectively.
type CompTimer struct {
MinErrorFactor float64 `json:"min_error_factor"`
MaxErrorFactor float64 `json:"max_error_factor"`
avg Averager
stimer *SimpleTimer
}
// NewCompTimer returns a new CompTimer with the specified Average.
// MinErrorFactor and MaxErrorFactor may be changed before use.
func NewCompTimer(a Averager) *CompTimer {
return &CompTimer{DefaultCompTimerMinErrorFactor,
DefaultCompTimerMaxErrorFactor, a, NewSimpleTimer()}
}
// NewDefaultCompTimer returns a new CompTimer with the default Average.
// MinErrorFactor and MaxErrorFactor may be changed before use.
func NewDefaultCompTimer() *CompTimer {
return NewCompTimer(DefaultCompTimerAverage)
}
// Sleep selects on both a time.Timer channel and the done channel.
func (ct *CompTimer) Sleep(ctx context.Context, t time.Time, d time.Duration) (time.Time, error) {
comp := ct.avg.Average()
// do compensation
if comp != 0 {
d = time.Duration(float64(d) / comp)
}
// sleep and calculate error
t2, err := ct.stimer.Sleep(ctx, t, d)
erf := float64(t2.Sub(t)) / float64(d)
// reject outliers
if erf >= ct.MinErrorFactor && erf <= ct.MaxErrorFactor {
ct.avg.Push(erf)
}
return t2, err
}
func (ct *CompTimer) String() string {
return "comp"
}
// BusyTimer uses a busy wait loop to wait for the next send. It wastes CPU
// and should only be used for extremely tight timing requirements.
type BusyTimer struct {
}
// Sleep waits with a busy loop and checks the done channel every iteration.
func (bt *BusyTimer) Sleep(ctx context.Context, t time.Time, d time.Duration) (time.Time, error) {
e := t.Add(d)
for t.Before(e) {
select {
case <-ctx.Done():
return t, ctx.Err()
default:
t = time.Now()
}
}
return t, nil
}
func (bt *BusyTimer) String() string {
return "busy"
}
// HybridTimer uses Go's default time functions and performs compensation to try
// to improve precision. To further improve precision, it sleeps to within some
// factor of the target value, then uses a busy wait loop for the remainder.
// The CPU will be in a busy wait for 1 - sleep factor for each sleep performed,
// so ideally the sleep factor should be increased to some threshold before
// precision starts to be lost, or some balance between the desired precision
// and CPU load is struck. The sleep factor typically can be increased for
// longer intervals and must be decreased for shorter intervals to keep high
// precision. In one example, a sleep factor of 0.95 could be used for 15ns
// precision at an interval of 200ms, but a sleep factor of 0.80 was required
// for 100ns precision at an interval of 1ms. These requirements will likely
// vary for different hardware and OS combinations.
type HybridTimer struct {
ctimer *CompTimer
slfct float64
}
// NewHybridTimer returns a new HybridTimer using the given Average algorithm
// and sleep factor (0 - 1.0) before the busy wait.
func NewHybridTimer(a Averager, sleepFactor float64) *HybridTimer {
return &HybridTimer{NewCompTimer(a), sleepFactor}
}
// NewDefaultHybridTimer returns a new HybridTimer using the default Average
// and sleep factor.
func NewDefaultHybridTimer() *HybridTimer {
return NewHybridTimer(DefaultCompTimerAverage, DefaultHybridTimerSleepFactor)
}
// SleepFactor returns the sleep factor.
func (ht *HybridTimer) SleepFactor() float64 {
return ht.slfct
}
// Sleep selects on both a time.Timer channel and the done channel.
func (ht *HybridTimer) Sleep(ctx context.Context, t time.Time, d time.Duration) (time.Time, error) {
e := t.Add(d)
d = time.Duration(float64(d) * ht.slfct)
t2, err := ht.ctimer.Sleep(ctx, t, d)
if err != nil {
return t2, err
}
for t2.Before(e) {
select {
case <-ctx.Done():
return t2, ctx.Err()
default:
t2 = time.Now()
}
}
return t2, nil
}
func (ht *HybridTimer) String() string {
return fmt.Sprintf("hybrid:%f", ht.slfct)
}
// TimerFactories are the registered Timer factories.
var TimerFactories = make([]TimerFactory, 0)
// TimerFactory can create a Timer from a string.
type TimerFactory struct {
FactoryFunc func(string, Averager) (Timer, error)
Usage string
}
// RegisterTimer registers a new Timer.
func RegisterTimer(fn func(string, Averager) (Timer, error), usage string) {
TimerFactories = append(TimerFactories, TimerFactory{fn, usage})
}
// NewTimer returns a Timer from a string.
func NewTimer(s string, a Averager) (Timer, error) {
for _, fac := range TimerFactories {
t, err := fac.FactoryFunc(s, a)
if err != nil {
return nil, err
}
if t != nil {
return t, nil
}
}
return nil, Errorf(NoSuchTimer, "no such Timer %s", s)
}
func init() {
RegisterTimer(
func(s string, a Averager) (t Timer, err error) {
if s == "simple" {
t = NewSimpleTimer()
}
return
},
"simple: Go's standard time.Timer",
)
RegisterTimer(
func(s string, a Averager) (t Timer, err error) {
if s == "comp" {
t = NewCompTimer(a)
}
return
},
"comp: simple timer with error compensation (see --tcomp)",
)
RegisterTimer(
func(s string, a Averager) (t Timer, err error) {
args := strings.Split(s, ":")
if args[0] != "hybrid" {
return nil, nil
}
if len(args) == 1 {
return NewHybridTimer(a, DefaultHybridTimerSleepFactor), nil
}
sfct, err := strconv.ParseFloat(args[1], 64)
if err != nil || sfct <= 0 || sfct >= 1 {
return nil, Errorf(InvalidSleepFactor,
"invalid sleep factor %s to hybrid timer", args[1])
}
return NewHybridTimer(a, sfct), nil
},
fmt.Sprintf("hybrid:#: hybrid comp/busy timer w/ sleep factor (dfl %.2f)",
DefaultHybridTimerSleepFactor),
)
RegisterTimer(
func(s string, a Averager) (t Timer, err error) {
if s == "busy" {
t = &BusyTimer{}
}
return
},
"busy: busy wait loop (high precision and CPU, blasphemy)",
)
}
irtt/doc/ 0000755 0001751 0001751 00000000000 13240047124 011260 5 ustar pete pete irtt/doc/irtt-server.html 0000644 0001751 0001751 00000024333 13240047124 014441 0 ustar pete pete
IRTT-SERVER(1) v0.9.0 | IRTT Manual
all addresses on interface iface with default port 2112
%iface:port
all addresses on interface iface with port
Note: iface strings may contain * to match multiple interfaces
-d duration
Max test duration, or 0 for no maximum (default 0s, see Duration units below)
-i interval
Min send interval, or 0 for no minimum (default 10ms, see Duration units below)
-l length
Max packet length (default 0), or 0 for no maximum. Numbers less than size of required headers will cause test packets to be dropped.
--hmac=key
Add HMAC with key (0x for hex) to all packets, provides:
Dropping of all packets without a correct HMAC
Protection for server against unauthorized discovery and use
--timeout=duration
Timeout for closing connections if no requests received on a connection (default 1m0s, see Duration units below). 0 means no timeout (not recommended, especially on public servers). Max client interval will be restricted to timeout/4.
--pburst=#
Packet burst allowed before enforcing minimum interval (default 5)
--fill=fill
Payload fill if not requested (default pattern:69727474). Possible values include:
Value
Fill
none
Echo client payload (insecure on public servers)
rand
Use random bytes from Go’s math.rand
pattern:XX
Use repeating pattern of hex (default 69727474)
--allow-fills=fills
Comma separated patterns of fill requests to allow (default rand). See options for –fill. Notes:
Patterns may contain * for matching
Allowing non-random fills insecure on public servers
Use --allow-fills=“” to disallow all fill requests
--tstamp=modes
Timestamp modes to allow (default dual). Possible values:
Value
Allowed Timestamps
none
Don’t allow any timestamps
single
Allow a single timestamp (send, receive or midpoint)
dual
Allow dual timestamps
--no-dscp
Don’t allow setting dscp (default false)
--set-src-ip
Set source IP address on all outgoing packets from listeners on unspecified IP addresses (use for more reliable reply routing, but increases per-packet heap allocations)
--gc=mode
Sets garbage collection mode (default on). Possible values:
Value
Meaning
on
Garbage collector always on
off
Garbage collector always off
idle
Garbage collector enabled only when idle
--thread
Lock request handling goroutines to OS threads
-h
Show help
-v
Show version
Host formats
Hosts may be either hostnames (for IPv4 or IPv6) or IP addresses. IPv6 addresses must be surrounded by brackets and may include a zone after the % character. Examples:
Type
Example
IPv4 IP
192.168.1.10
IPv6 IP
[2001:db8:8f::2/32]
IPv4/6 hostname
localhost
Note: IPv6 addresses must be quoted in most shells.
Duration units
Durations are a sequence of decimal numbers, each with optional fraction, and unit suffix, such as: “300ms”, “1m30s” or “2.5m”. Sanity not enforced.
Suffix
Unit
h
hours
m
minutes
s
seconds
ms
milliseconds
ns
nanoseconds
SECURITY
Running an IRTT server that’s open to the outside world requires some additional attention. For starters, irtt server’s command line flags should be used to, at a minimum:
Restrict the duration (-d), interval (-i) and length (-l) of tests, particularly for public servers
Set an HMAC key (--hmac) for private servers to prevent unauthorized discovery and use
In addition, there are various systemd(1) options available for securing services. The irtt.service file included with the distribution sets many commonly used options, but should not be considered exhaustive.
To secure a server for public use, additional steps may be taken that are outside of the scope of this documentation, including but not limited to:
Setting up an iptables firewall (only UDP port 2112 must be open)
Setting up a chroot jail
It should be noted that there are no known security vulnerabilities in the Go language at this time, and the steps above, in particular the chroot jail, may or may not serve to enhance security in any way. Go-based servers are generally regarded as safe because of Go’s high-level language constructs for memory management, and at this time IRTT makes no use of Go’s unsafe package.
EXIT STATUS
irtt server exits with one of the following status codes:
Code
Meaning
0
Success
1
Runtime error
2
Command line error
3
Two interrupt signals received
EXAMPLES
$ irtt server
Starts the server and listens on all addresses (unspecified address)
Starts the server and listens on all addresses, setting the maximum test duration to 30 seconds, minimum interval to 20 ms, and maximum packet length to 256 bytes. Disallows fill requests and forces all return packets to be filled with random data.
$ irtt server -b 192.168.100.11:64381 --hmac=secret
Starts the server and binds to IPv4 address 192.168.100.11, port 64381. Requires a valid HMAC on all packets with the key secret, otherwise packets are dropped.
Add HMAC with key (0x for hex) to all packets, provides:
Dropping of all packets without a correct HMAC
Protection for server against unauthorized discovery and use
-4
IPv4 only
-6
IPv6 only
--timeouts=durations
Timeouts used when connecting to server (default 1s,2s,4s,8s). Comma separated list of durations (see Duration units below). Total wait time will be up to the sum of these Durations. Max packets sent is up to the number of Durations. Minimum timeout duration is 200ms.
--ttl=ttl
Time to live (default 0, meaning use OS default)
--loose
Accept and use any server restricted test parameters instead of exiting with nonzero status.
--thread
Lock sending and receiving goroutines to OS threads
-h
Show help
-v
Show version
Host formats
Hosts may be either hostnames (for IPv4 or IPv6) or IP addresses. IPv6 addresses must be surrounded by brackets and may include a zone after the % character. Examples:
Type
Example
IPv4 IP
192.168.1.10
IPv6 IP
[2001:db8:8f::2/32]
IPv4/6 hostname
localhost
Note: IPv6 addresses must be quoted in most shells.
Duration units
Durations are a sequence of decimal numbers, each with optional fraction, and unit suffix, such as: “300ms”, “1m30s” or “2.5m”. Sanity not enforced.
Suffix
Unit
h
hours
m
minutes
s
seconds
ms
milliseconds
ns
nanoseconds
OUTPUT
IRTT’s JSON output format consists of five top-level objects:
cpus the number of CPUs reported by Go’s runtime.NumCPU(), which reflects the number of logical rather than physical CPUs. In the example below, the number 8 is reported for a Core i7 (quad core) with hyperthreading (2 threads per core).
go_version the version of Go the executable was built with
timer the timer used: simple, comp, hybrid or busy (irtt client --timer flag)
waiter the waiter used: fixed duration, multiple of RTT or multiple of max RTT (irtt client --wait flag)
filler the packet filler used: none, rand or pattern (irtt client --fill flag)
fill_one whether to fill only once and repeat for all packets (irtt client --fill-one flag)
thread_lock whether to lock packet handling goroutines to OS threads
supplied a nested config object with the configuration as originally supplied to the API or irtt command. The supplied configuration can differ from the final configuration in the following ways:
local_address and remote_address may have hostnames or named ports before being resolved to an IP and numbered port
ip_version may be IPv4+6 before it is determined after address resolution
params may be different before the server applies restrictions based on its configuration
Note: In the stats object, a duration stats class of object repeats and will not be repeated in the individual descriptions. It contains statistics about nanosecond duration values and has the following attributes:
total the total of the duration values
n the number of duration values
min the minimum duration value
max the maximum duration value
mean the mean duration value
stddev the standard deviation
variance the variance
The regular attributes in stats are as follows:
start_time the start time of the test, in TZ format
send_call a duration stats object for the call time when sending packets
timer_error a duration stats object for the observed sleep time error
rtt a duration stats object for the round-trip time
send_delay a duration stats object for the one-way send delay (only available if server timestamps are enabled)
receive_delay a duration stats object for the one-way receive delay (only available if server timestamps are enabled)
server_packets_received the number of packets received by the server, including duplicates (always present, but only valid if the ReceivedStats parameter includes ReceivedStatsCount, or the --stats flag to the irtt client is count or both)
bytes_sent the number of UDP payload bytes sent during the test
bytes_received the number of UDP payload bytes received during the test
duplicates the number of packets received with the same sequence number
late_packets the number of packets received with a sequence number lower than the previously received sequence number (one simple metric for out-of-order packets)
wait the actual time spent waiting for final packets, in nanoseconds
duration the actual duration of the test, in nanoseconds, from the time just before the first packet was sent to the time after the last packet was received and results are starting to be calculated
packets_sent the number of packets sent to the server
packets_received the number of packets received from the server
upstream_loss_percent 100 * (packets_sent - server_packets_received / packets_sent) (always present, but only valid if server_packets_received is valid)
downstream_loss_percent 100 * (server_packets_received - packets_received / server_packets_received) (always present, but only valid if server_packets_received is valid)
ipdv_send a duration stats object for the send IPDV(only available if server timestamps are enabled)
ipdv_receive a duration stats object for the receive IPDV(only available if server timestamps are enabled)
ipdv_round_trip a duration stats object for the round-trip IPDV(available regardless of whether server timestamps are enabled or not)
server_processing_time a duration stats object for the time the server took after it received the packet to when it sent the response (only available when both send and receive timestamps are enabled)
timer_err_percent the mean of the absolute values of the timer error, as a percentage of the interval
timer_misses the number of times the timer missed the interval (was at least 50% over the scheduled time)
timer_miss_percent 100 * timer_misses / expected packets sent
send_rate the send bitrate (bits-per-second and corresponding string), calculated using the number of UDP payload bytes sent between the time right before the first send call and the time right after the last send call
receive_rate the receive bitrate (bits-per-second and corresponding string), calculated using the number of UDP payload bytes received between the time right after the first receive call and the time right after the last receive call
round_trips
each round-trip is a single request to / reply from the server
Note:wall values are from Go’s time.Time.UnixNano(), the number of nanoseconds elapsed since January 1, 1970 UTC
Note:monotonic values are the number of nanoseconds since the start of the test for the client, and since start of the process for the server
seqno the sequence number
lost the lost status of the packet, which can be one of false, true, true_down or true_up. The true_down and true_up values are only possible if the ReceivedStats parameter includes ReceivedStatsWindow (irtt client --stats flag). Even then, if it could not be determined whether the packet was lost upstream or downstream, the value true is used.
timestamps the client and server timestamps
client the client send and receive wall and monotonic timestamps (receive values only present if lost is false)
server the server send and receive wall and monotonic timestamps (both send and receive values not present if lost is true), and additionally:
send values are not present if the StampAt (irtt client --tstamp flag) does not include send timestamps
receive values are not present if the StampAt (irtt client --tstamp flag) does not include receive timestamps
wall values are not present if the Clock (irtt client --clock flag) does not include wall values or server timestamps are not enabled
monotonic values are not present if the Clock (irtt client --clock flag) does not include monotonic values or server timestamps are not enabled
delay an object containing the delay values
receive the one-way receive delay, in nanoseconds (present only if server timestamps are enabled and at least one wall clock value is available)
rtt the round-trip time, in nanoseconds, always present
send the one-way send delay, in nanoseconds (present only if server timestamps are enabled and at least one wall clock value is available)
ipdv an object containing the IPDV values (attributes present only for seqno > 0, and if lost is false for both the current and previous round_trip)
receive the difference in receive delay relative to the previous packet (present only if at least one server timestamp is available)
rtt the difference in round-trip time relative to the previous packet (always present for seqno > 0)
send the difference in send delay relative to the previous packet (present only if at least one server timestamp is available)
EXIT STATUS
irtt client exits with one of the following status codes:
Code
Meaning
0
Success
1
Runtime error
2
Command line error
3
Two interrupt signals received
WARNINGS
It is possible with the irtt client to dramatically harm network performance by using intervals that are too low, particularly in combination with large packet lengths. Careful consideration should be given before using sub-millisecond intervals, not only because of the impact on the network, but also because:
Timer accuracy at sub-millisecond intervals may begin to suffer without the use of a custom kernel or the busy timer (which pins the CPU)
Memory consumption for results storage and system CPU time both rise rapidly
The granularity of the results reported may very well not be required
EXAMPLES
$ irtt client localhost
Sends requests once per second for one minute to localhost.
$ irtt client -i 200ms -d 10s -o - localhost
Sends requests every 0.2 sec for 10 seconds to localhost. Writes JSON output to stdout.
Sends requests every 20ms for one minute to 192.168.100.10. Fills both the client and server payload with random data. This simulates a G.711 VoIP conversation, one of the most commonly used codecs for VoIP as of this writing.
Sends requests with IPv6 every 100ms for 5 seconds to irtt.example.org. Sets the DSCP value (ToS field) of requests and responses to 46 (Expedited Forwarding).
irtt/doc/irtt.html 0000644 0001751 0001751 00000027672 13240047124 013146 0 ustar pete pete
IRTT(1) v0.9.0 | IRTT Manual
IRTT(1) v0.9.0 | IRTT Manual
February 11, 2018
NAME
irtt - Isochronous Round-Trip Time
SYNOPSIS
irtt command [args]
irtt help command
DESCRIPTION
IRTT measures round-trip time and other latency related metrics using UDP packets sent on a fixed period, and produces both text and JSON output.
COMMANDS
client
runs the client
server
runs the server
bench
runs HMAC and fill benchmarks
clock
runs wall vs monotonic clock test
sleep
runs sleep accuracy test
version
shows the version
EXAMPLES
After installing IRTT, start a server:
$ irtt server
IRTT server starting...
[ListenerStart] starting IPv6 listener on [::]:2112
[ListenerStart] starting IPv4 listener on 0.0.0.0:2112
While that’s running, run a client. If no options are supplied, it will send a request once per second, like ping. Here we simulate a one minute G.711 VoIP conversation by using an interval of 20ms and randomly filled payloads of 172 bytes:
In the results above, the client and server are located at two different sites, around 50km from one another, each of which connects to the Internet via point-to-point WiFi. The client is 3km NLOS through trees located near its transmitter, which is likely the reason for the higher upstream packet loss, mean send delay and IPDV.
BUGS
Windows is unable to set DSCP values for IPv6.
Windows is unable to set the source IP address, so --set-src-ip may not be used on the server.
The server doesn’t run well on 32-bit Windows platforms. When connecting with a client, you may see Terminated due to receive error. To work around this, disable dual timestamps from the client by including --tstamp=midpoint.
LIMITATIONS
“It is the limitations of software that give it life.”
-Me, justifying my limitations
Isochronous (fixed period) send schedule
Currently, IRTT only sends packets on a fixed period, foregoing the ability to simulate arbitrary traffic. Accepting this limitation offers some benefits:
It’s easy to implement
It’s easy to calculate how many packets and how much data will be sent in a given time
It simplifies timer error compensation
Also, isochronous packets are commonly seen in VoIP, games and some streaming media, so it already simulates an array of common types of traffic.
Fixed packet lengths for a given test
Packet lengths are fixed for the duration of the test. While this may not be an accurate simulation of some types of traffic, it means that IPDV measurements are accurate, where they wouldn’t be in any other case.
Stateful protocol
There are numerous benefits to stateless protocols, particularly for developers and data centers, including simplified server design, horizontal scalabity, and easily implemented zero-downtime restarts. However, in this case, a stateful protocol provides important benefits to the user, including:
Smaller packet sizes (a design goal) as context does not need to be included in every request
More accurate measurement of upstream vs downstream packet loss (this gets worse in a stateless protocol as RTT approaches the test duration, complicating interplanetary tests!)
More accurate rate and test duration limiting on the server
In-memory results storage
Results for each round-trip are stored in memory as the test is being run. Each result takes 72 bytes in memory (8 64-bit timestamps and a 64-bit server received packet window), so this limits the effective duration of the test, especially at very small send intervals. However, the advantages are:
It’s easier to perform statistical analysis (like calculation of the median) on fixed arrays than on running data values
We don’t need to either send client timestamps to the server, or maintain a local running window of sent packet info, because they’re all in memory, no matter when server replies come back
Not accessing the disk during the test to write test output prevents inadvertently affecting the results
It simplifies the API
As a consequence of storing results in memory, packet sequence numbers are fixed at 32-bits. If all 2^32 sequence numbers were used, the results would require over 300 Gb of virtual memory to record while the test is running. That is why 64-bit sequence numbers are currently unnecessary.
64-bit received window
In order to determine per-packet differentiation between upstream and downstream loss, a 64-bit “received window” may be returned with each packet that contains the receipt status of the previous 64 packets. This can be enabled using --stats=window/both with the irtt client. Its limited width and simple bitmap format lead to some caveats:
Per-packet differentiation is not available (for any intervening packets) if greater than 64 packets are lost in succession. These packets will be marked with the generic Lost.
While any packet marked LostDown is guaranteed to be marked properly, there is no confirmation of receipt of the receive window from the client to the server, so packets may sometimes be erroneously marked LostUp, for example, if they arrive late to the server and slide out of the received window before they can be confirmed to the client, or if the received window is lost on its way to the client and not amended by a later packet’s received window.
There are many ways that this simple approach could be improved, such as by:
Allowing a wider window
Encoding receipt seqnos in a more intelligent way to allow a wider seqno range
Sending confirmation of window receipt from the client to the server and re-sending unreceived windows
However, the current strategy means that a good approximation of per-packet loss results can be obtained with only 8 additional bytes in each packet. It also requires very little computational time on the server, and almost all computation on the client occurs during results generation, after the test is complete. It isn’t as accurate with late (out-of-order) upstream packets or with long sequences of lost packets, but high loss or high numbers of late packets typically indicate more severe network conditions that should be corrected first anyway, perhaps before per-packet results matter. Note that in case of very high packet loss, the total number of packets received by the server but not returned to the client (which can be obtained using --stats=count) will still be correct, which will still provide an accurate average loss percentage in each direction over the course of the test.
Many thanks to both Toke Høiland-Jørgensen and Dave Täht from the Bufferbloat project for their valuable advice. Any problems in design or implementation are entirely my own.
HISTORY
IRTT was originally written to improve the latency and packet loss measurements for the excellent Flent tool. Flent was developed by and for the Bufferbloat project, which aims to reduce “chaotic and laggy network performance,” making this project valuable to anyone who values their time and sanity while using the Internet.
irtt/doc/irtt-server.1 0000644 0001751 0001751 00000015475 13240047124 013644 0 ustar pete pete .\"t
.\" Automatically generated by Pandoc 2.1.1
.\"
.TH "IRTT\-SERVER" "1" "February 11, 2018" "v0.9.0" "IRTT Manual"
.hy
.SH NAME
.PP
irtt\-server \- Isochronous Round\-Trip Time Server
.SH SYNOPSIS
.PP
irtt server [\f[I]args\f[]]
.SH DESCRIPTION
.PP
\f[I]irtt server\f[] is the server for irtt(1) (irtt.html).
.SH OPTIONS
.TP
.B \-b \f[I]addresses\f[]
Bind addresses (default \[lq]:2112\[rq]), comma separated list of:
.RS
.PP
.TS
tab(@);
l l.
T{
Format
T}@T{
Address Type
T}
_
T{
:port
T}@T{
unspecified address with port, use with care
T}
T{
host
T}@T{
host with default port 2112, see Host formats below
T}
T{
host:port
T}@T{
host with specified port, see Host formats below
T}
T{
%iface
T}@T{
all addresses on interface iface with default port 2112
T}
T{
%iface:port
T}@T{
all addresses on interface iface with port
T}
.TE
.PP
\f[B]Note:\f[] iface strings may contain * to match multiple interfaces
.RE
.TP
.B \-d \f[I]duration\f[]
Max test duration, or 0 for no maximum (default 0s, see Duration units
below)
.RS
.RE
.TP
.B \-i \f[I]interval\f[]
Min send interval, or 0 for no minimum (default 10ms, see Duration units
below)
.RS
.RE
.TP
.B \-l \f[I]length\f[]
Max packet length (default 0), or 0 for no maximum.
Numbers less than size of required headers will cause test packets to be
dropped.
.RS
.RE
.TP
.B \-\-hmac=\f[I]key\f[]
Add HMAC with \f[I]key\f[] (0x for hex) to all packets, provides:
.RS
.IP \[bu] 2
Dropping of all packets without a correct HMAC
.IP \[bu] 2
Protection for server against unauthorized discovery and use
.RE
.TP
.B \-\-timeout=\f[I]duration\f[]
Timeout for closing connections if no requests received on a connection
(default 1m0s, see Duration units below).
0 means no timeout (not recommended, especially on public servers).
Max client interval will be restricted to timeout/4.
.RS
.RE
.TP
.B \-\-pburst=\f[I]#\f[]
Packet burst allowed before enforcing minimum interval (default 5)
.RS
.RE
.TP
.B \-\-fill=\f[I]fill\f[]
Payload fill if not requested (default pattern:69727474).
Possible values include:
.RS
.PP
.TS
tab(@);
l l.
T{
Value
T}@T{
Fill
T}
_
T{
\f[I]none\f[]
T}@T{
Echo client payload (insecure on public servers)
T}
T{
\f[I]rand\f[]
T}@T{
Use random bytes from Go's math.rand
T}
T{
\f[I]pattern:\f[]XX
T}@T{
Use repeating pattern of hex (default 69727474)
T}
.TE
.RE
.TP
.B \-\-allow\-fills=\f[I]fills\f[]
Comma separated patterns of fill requests to allow (default rand).
See options for \f[I]\[en]fill\f[].
Notes:
.RS
.IP \[bu] 2
Patterns may contain * for matching
.IP \[bu] 2
Allowing non\-random fills insecure on public servers
.IP \[bu] 2
Use \f[I]\-\-allow\-fills=\[lq]\[rq]\f[] to disallow all fill requests
.RE
.TP
.B \-\-tstamp=\f[I]modes\f[]
Timestamp modes to allow (default dual).
Possible values:
.RS
.PP
.TS
tab(@);
l l.
T{
Value
T}@T{
Allowed Timestamps
T}
_
T{
\f[I]none\f[]
T}@T{
Don't allow any timestamps
T}
T{
\f[I]single\f[]
T}@T{
Allow a single timestamp (send, receive or midpoint)
T}
T{
\f[I]dual\f[]
T}@T{
Allow dual timestamps
T}
.TE
.RE
.TP
.B \-\-no\-dscp
Don't allow setting dscp (default false)
.RS
.RE
.TP
.B \-\-set\-src\-ip
Set source IP address on all outgoing packets from listeners on
unspecified IP addresses (use for more reliable reply routing, but
increases per\-packet heap allocations)
.RS
.RE
.TP
.B \-\-gc=\f[I]mode\f[]
Sets garbage collection mode (default on).
Possible values:
.RS
.PP
.TS
tab(@);
l l.
T{
Value
T}@T{
Meaning
T}
_
T{
\f[I]on\f[]
T}@T{
Garbage collector always on
T}
T{
\f[I]off\f[]
T}@T{
Garbage collector always off
T}
T{
\f[I]idle\f[]
T}@T{
Garbage collector enabled only when idle
T}
.TE
.RE
.TP
.B \-\-thread
Lock request handling goroutines to OS threads
.RS
.RE
.TP
.B \-h
Show help
.RS
.RE
.TP
.B \-v
Show version
.RS
.RE
.SS Host formats
.PP
Hosts may be either hostnames (for IPv4 or IPv6) or IP addresses.
IPv6 addresses must be surrounded by brackets and may include a zone
after the % character.
Examples:
.PP
.TS
tab(@);
l l.
T{
Type
T}@T{
Example
T}
_
T{
IPv4 IP
T}@T{
192.168.1.10
T}
T{
IPv6 IP
T}@T{
[2001:db8:8f::2/32]
T}
T{
IPv4/6 hostname
T}@T{
localhost
T}
.TE
.PP
\f[B]Note:\f[] IPv6 addresses must be quoted in most shells.
.SS Duration units
.PP
Durations are a sequence of decimal numbers, each with optional
fraction, and unit suffix, such as: \[lq]300ms\[rq], \[lq]1m30s\[rq] or
\[lq]2.5m\[rq].
Sanity not enforced.
.PP
.TS
tab(@);
l l.
T{
Suffix
T}@T{
Unit
T}
_
T{
h
T}@T{
hours
T}
T{
m
T}@T{
minutes
T}
T{
s
T}@T{
seconds
T}
T{
ms
T}@T{
milliseconds
T}
T{
ns
T}@T{
nanoseconds
T}
.TE
.SH SECURITY
.PP
Running an IRTT server that's open to the outside world requires some
additional attention.
For starters, irtt server's command line flags should be used to, at a
minimum:
.IP \[bu] 2
Restrict the duration (\f[I]\-d\f[]), interval (\f[I]\-i\f[]) and length
(\f[I]\-l\f[]) of tests, particularly for public servers
.IP \[bu] 2
Set an HMAC key (\f[I]\-\-hmac\f[]) for private servers to prevent
unauthorized discovery and use
.PP
In addition, there are various systemd(1) options available for securing
services.
The irtt.service file included with the distribution sets many commonly
used options, but should not be considered exhaustive.
.PP
To secure a server for public use, additional steps may be taken that
are outside of the scope of this documentation, including but not
limited to:
.IP \[bu] 2
Setting up an iptables firewall (only UDP port 2112 must be open)
.IP \[bu] 2
Setting up a chroot jail
.PP
It should be noted that there are no known security vulnerabilities in
the Go language at this time, and the steps above, in particular the
chroot jail, may or may not serve to enhance security in any way.
Go\-based servers are generally regarded as safe because of Go's
high\-level language constructs for memory management, and at this time
IRTT makes no use of Go's unsafe (https://golang.org/pkg/unsafe/)
package.
.SH EXIT STATUS
.PP
\f[I]irtt server\f[] exits with one of the following status codes:
.PP
.TS
tab(@);
l l.
T{
Code
T}@T{
Meaning
T}
_
T{
0
T}@T{
Success
T}
T{
1
T}@T{
Runtime error
T}
T{
2
T}@T{
Command line error
T}
T{
3
T}@T{
Two interrupt signals received
T}
.TE
.SH EXAMPLES
.TP
.B $ irtt server
Starts the server and listens on all addresses (unspecified address)
.RS
.RE
.TP
.B $ irtt server \-d 30s \-i 20ms \-l 256 \-\-fill=rand \-\-allow\-fills=\[lq]\[rq]
Starts the server and listens on all addresses, setting the maximum test
duration to 30 seconds, minimum interval to 20 ms, and maximum packet
length to 256 bytes.
Disallows fill requests and forces all return packets to be filled with
random data.
.RS
.RE
.TP
.B $ irtt server \-b 192.168.100.11:64381 \-\-hmac=secret
Starts the server and binds to IPv4 address 192.168.100.11, port 64381.
Requires a valid HMAC on all packets with the key \f[I]secret\f[],
otherwise packets are dropped.
.RS
.RE
.SH SEE ALSO
.PP
irtt(1) (irtt.html), irtt\-client(1) (irtt-client.html)
.PP
IRTT GitHub repository (https://github.com/peteheist/irtt/)
irtt/doc/irtt-client.md 0000644 0001751 0001751 00000060375 13240047124 014053 0 ustar pete pete % IRTT-CLIENT(1) v0.9.0 | IRTT Manual
%
% February 11, 2018
# NAME
irtt-client - Isochronous Round-Trip Time Client
# SYNOPSIS
irtt client [*args*]
# DESCRIPTION
*irtt client* is the client for [irtt(1)](irtt.html).
# OPTIONS
-d *duration*
: Total time to send (default 1m0s, see [Duration units](#duration-units)
below)
-i *interval*
: Send interval (default 1s, see [Duration units](#duration-units) below)
-l *length*
: Length of packet (default 0, increased as necessary for required headers),
common values:
- 1472 (max unfragmented size of IPv4 datagram for 1500 byte MTU)
- 1452 (max unfragmented size of IPv6 datagram for 1500 byte MTU)
-o *file*
: Write JSON output to file (use '-' for stdout). The extension used for
*file* controls the gzip behavior as follows (output to stdout is not
gzipped):
Extension | Behavior
--------- | --------
none | extension .json.gz is added, output is gzipped
.json.gz | output is gzipped
.gz | output is gzipped, extension changed to .json.gz
.json | output is not gzipped
-q
: Quiet, suppress per-packet output
-Q
: Really quiet, suppress all output except errors to stderr
-n
: No test, connect to the server and validate test parameters but don't run
the test
\--stats=*stats*
: Server stats on received packets (default *both*). Possible values:
Value | Meaning
-------- | -------
*none* | no server stats on received packets
*count* | total count of received packets
*window* | receipt status of last 64 packets with each reply
*both* | both count and window
\--tstamp=*mode*
: Server timestamp mode (default *both*). Possible values:
Value | Meaning
---------- | -------
*none* | request no timestamps
*send* | request timestamp at server send
*receive* | request timestamp at server receive
*both* | request both send and receive timestamps
*midpoint* | request midpoint timestamp (send/receive avg)
\--clock=*clock*
: Clock/s used for server timestamps (default *both*). Possible values:
Value | Meaning
----------- | -------
*wall* | wall clock only
*monotonic* | monotonic clock only
*both* | both clocks
\--dscp=*dscp*
: DSCP (ToS) value (default 0, 0x prefix for hex). Common values:
Value | Meaning
----- | -------
0 | Best effort
8 | CS1- Bulk
40 | CS5- Video
46 | EF- Expedited forwarding
[DSCP & ToS](https://www.tucny.com/Home/dscp-tos)
\--df=*DF*
: Setting for do not fragment (DF) bit in all packets. Possible values:
Value | Meaning
--------- | -------
*default* | OS default
*false* | DF bit not set
*true* | DF bit set
\--wait=*wait*
: Wait time at end of test for unreceived replies (default 3x4s).
Possible values:
Format | Meaning
------------ | -------
#*x*duration | # times max RTT, or duration if no response
#*r*duration | # times RTT, or duration if no response
duration | fixed duration (see [Duration units](#duration-units) below)
Examples:
Example | Meaning
------- | -------
3x4s | 3 times max RTT, or 4 seconds if no response
1500ms | fixed 1500 milliseconds
\--timer=*timer*
: Timer for waiting to send packets (default comp). Possible values:
Value | Meaning
---------- | -------
*simple* | Go's standard time.Timer
*comp* | Simple timer with error compensation (see -tcomp)
*hybrid:*# | Hybrid comp/busy timer with sleep factor (default 0.95)
*busy* | busy wait loop (high precision and CPU, blasphemy)
\--tcomp=*alg*
: Comp timer averaging algorithm (default exp:0.10). Possible values:
Value | Meaning
------- | -------
*avg* | Cumulative average error
*win:*# | Moving average error with window # (default 5)
*exp:*# | Exponential average with alpha # (default 0.10)
\--fill=*fill*
: Fill payload with given data (default none). Possible values:
Value | Meaning
------------ | -------
*none* | Leave payload as all zeroes
*rand* | Use random bytes from Go's math.rand
*pattern:*XX | Use repeating pattern of hex (default 69727474)
\--fill-one
: Fill only once and repeat for all packets
\--sfill=fill
: Request server fill (default not specified). See values for --fill.
Server must support and allow this fill with --allow-fills.
\--local=addr
: Local address (default from OS). Possible values:
Value | Meaning
----------- | -------
*:port* | Unspecified address (all IPv4/IPv6 addresses) with port
*host* | Host with dynamic port, see [Host formats](#host-formats) below
*host:port* | Host with specified port, see [Host formats](#host-formats) below
\--hmac=key
: Add HMAC with key (0x for hex) to all packets, provides:
- Dropping of all packets without a correct HMAC
- Protection for server against unauthorized discovery and use
-4
: IPv4 only
-6
: IPv6 only
\--timeouts=*durations*
: Timeouts used when connecting to server (default 1s,2s,4s,8s).
Comma separated list of durations (see [Duration units](#duration-units) below).
Total wait time will be up to the sum of these Durations.
Max packets sent is up to the number of Durations.
Minimum timeout duration is 200ms.
\--ttl=*ttl*
: Time to live (default 0, meaning use OS default)
\--loose
: Accept and use any server restricted test parameters instead of
exiting with nonzero status.
\--thread
: Lock sending and receiving goroutines to OS threads
-h
: Show help
-v
: Show version
## Host formats
Hosts may be either hostnames (for IPv4 or IPv6) or IP addresses. IPv6
addresses must be surrounded by brackets and may include a zone after the %
character. Examples:
Type | Example
--------------- | -------
IPv4 IP | 192.168.1.10
IPv6 IP | [2001:db8:8f::2/32]
IPv4/6 hostname | localhost
**Note:** IPv6 addresses must be quoted in most shells.
## Duration units
Durations are a sequence of decimal numbers, each with optional fraction, and
unit suffix, such as: "300ms", "1m30s" or "2.5m". Sanity not enforced.
Suffix | Unit
------ | ----
h | hours
m | minutes
s | seconds
ms | milliseconds
ns | nanoseconds
# OUTPUT
IRTT's JSON output format consists of five top-level objects:
1. [version](#version)
2. [system_info](#system_info)
3. [config](#config)
4. [stats](#stats)
5. [round_trips](#round_trips)
These are documented through the examples below. All attributes are present
unless otherwise **noted**.
## version
version information
```
"version": {
"irtt": "0.9.0",
"protocol": 1,
"json_format": 1
},
```
- *irtt* the IRTT version number
- *protocol* the protocol version number (increments mean incompatible changes)
- *json_format* the JSON format number (increments mean incompatible changes)
## system_info
a few basic pieces of system information
```
"system_info": {
"os": "darwin",
"cpus": 8,
"go_version": "go1.9.2",
"hostname": "tron.local"
},
```
- *os* the Operating System from Go's *runtime.GOOS*
- *cpus* the number of CPUs reported by Go's *runtime.NumCPU()*, which reflects
the number of logical rather than physical CPUs. In the example below, the
number 8 is reported for a Core i7 (quad core) with hyperthreading (2 threads
per core).
- *go_version* the version of Go the executable was built with
- *hostname* the local hostname
## config
the configuration used for the test
```
"config": {
"local_address": "127.0.0.1:51203",
"remote_address": "127.0.0.1:2112",
"open_timeouts": "1s,2s,4s,8s",
"params": {
"proto_version": 1,
"duration": 600000000,
"interval": 200000000,
"length": 48,
"received_stats": "both",
"stamp_at": "both",
"clock": "both",
"dscp": 0,
"server_fill": ""
},
"loose": false,
"ip_version": "IPv4",
"df": 0,
"ttl": 0,
"timer": "comp",
"waiter": "3x4s",
"filler": "none",
"fill_one": false,
"thread_lock": false,
"supplied": {
"local_address": ":0",
"remote_address": "localhost",
"open_timeouts": "1s,2s,4s,8s",
"params": {
"proto_version": 1,
"duration": 600000000,
"interval": 200000000,
"length": 0,
"received_stats": "both",
"stamp_at": "both",
"clock": "both",
"dscp": 0,
"server_fill": ""
},
"loose": false,
"ip_version": "IPv4+6",
"df": 0,
"ttl": 0,
"timer": "comp",
"waiter": "3x4s",
"filler": "none",
"fill_one": false,
"thread_lock": false
}
},
```
- *local_address* the local address (IP:port) for the client
- *remote_address* the remote address (IP:port) for the server
- *open_timeouts* a list of timeout durations used after an open packet is sent
- *params* are the parameters that were negotiated with the server, including:
- *proto_version* protocol version number
- *duration* duration of the test, in nanoseconds
- *interval* send interval, in nanoseconds
- *length* packet length
- *received_stats* statistics for packets received by server (none, count,
window or both, *\--stats* flag for irtt client)
- *stamp_at* timestamp selection parameter (none, send, receive, both or
midpoint, *\--tstamp* flag for irtt client)
- *clock* clock selection parameter (wall or monotonic, *\--clock* flag for irtt client)
- *dscp* the [DSCP](https://en.wikipedia.org/wiki/Differentiated_services)
value
- *server_fill* the requested server fill (*\--sfill* flag for irtt client)
- *loose* if true, client accepts and uses restricted server parameters, with a
warning
- *ip_version* the IP version used (IPv4 or IPv6)
- *df* the do-not-fragment setting (0 == OS default, 1 == false, 2 == true)
- *ttl* the IP [time-to-live](https://en.wikipedia.org/wiki/Time_to_live) value
- *timer* the timer used: simple, comp, hybrid or busy (irtt client \--timer flag)
- *waiter* the waiter used: fixed duration, multiple of RTT or multiple of max RTT
(irtt client *\--wait* flag)
- *filler* the packet filler used: none, rand or pattern (irtt client *\--fill*
flag)
- *fill_one* whether to fill only once and repeat for all packets
(irtt client *\--fill-one* flag)
- *thread_lock* whether to lock packet handling goroutines to OS threads
- *supplied* a nested *config* object with the configuration as
originally supplied to the API or *irtt* command. The supplied configuration can
differ from the final configuration in the following ways:
- *local_address* and *remote_address* may have hostnames or named ports before
being resolved to an IP and numbered port
- *ip_version* may be IPv4+6 before it is determined after address resolution
- *params* may be different before the server applies restrictions based on
its configuration
## stats
statistics for the results
```
"stats": {
"start_time": "2017-10-16T21:05:23.502719056+02:00",
"send_call": {
"total": 79547,
"n": 3,
"min": 17790,
"max": 33926,
"mean": 26515,
"stddev": 8148,
"variance": 66390200
},
"timer_error": {
"total": 227261,
"n": 2,
"min": 59003,
"max": 168258,
"mean": 113630,
"stddev": 77254,
"variance": 5968327512
},
"rtt": {
"total": 233915,
"n": 2,
"min": 99455,
"max": 134460,
"mean": 116957,
"median": 116957,
"stddev": 24752,
"variance": 612675012
},
"send_delay": {
"total": 143470,
"n": 2,
"min": 54187,
"max": 89283,
"mean": 71735,
"median": 71735,
"stddev": 24816,
"variance": 615864608
},
"receive_delay": {
"total": 90445,
"n": 2,
"min": 45177,
"max": 45268,
"mean": 45222,
"median": 45222,
"stddev": 64,
"variance": 4140
},
"server_packets_received": 2,
"bytes_sent": 144,
"bytes_received": 96,
"duplicates": 0,
"late_packets": 0,
"wait": 403380,
"duration": 400964028,
"packets_sent": 3,
"packets_received": 2,
"packet_loss_percent": 33.333333333333336,
"upstream_loss_percent": 33.333333333333336,
"downstream_loss_percent": 0,
"duplicate_percent": 0,
"late_packets_percent": 0,
"ipdv_send": {
"total": 35096,
"n": 1,
"min": 35096,
"max": 35096,
"mean": 35096,
"median": 35096,
"stddev": 0,
"variance": 0
},
"ipdv_receive": {
"total": 91,
"n": 1,
"min": 91,
"max": 91,
"mean": 91,
"median": 91,
"stddev": 0,
"variance": 0
},
"ipdv_round_trip": {
"total": 35005,
"n": 1,
"min": 35005,
"max": 35005,
"mean": 35005,
"median": 35005,
"stddev": 0,
"variance": 0
},
"server_processing_time": {
"total": 20931,
"n": 2,
"min": 9979,
"max": 10952,
"mean": 10465,
"stddev": 688,
"variance": 473364
},
"timer_err_percent": 0.056815,
"timer_misses": 0,
"timer_miss_percent": 0,
"send_rate": {
"bps": 2878,
"string": "2.9 Kbps"
},
"receive_rate": {
"bps": 3839,
"string": "3.8 Kbps"
}
},
```
**Note:** In the *stats* object, a _duration stats_ class of object repeats and
will not be repeated in the individual descriptions. It contains statistics about
nanosecond duration values and has the following attributes:
- *total* the total of the duration values
- *n* the number of duration values
- *min* the minimum duration value
- *max* the maximum duration value
- *mean* the mean duration value
- *stddev* the standard deviation
- *variance* the variance
The regular attributes in *stats* are as follows:
- *start_time* the start time of the test, in TZ format
- *send_call* a duration stats object for the call time when sending packets
- *timer_error* a duration stats object for the observed sleep time error
- *rtt* a duration stats object for the round-trip time
- *send_delay* a duration stats object for the one-way send delay
**(only available if server timestamps are enabled)**
- *receive_delay* a duration stats object for the one-way receive delay
**(only available if server timestamps are enabled)**
- *server_packets_received* the number of packets received by the server,
including duplicates (always present, but only valid if the *ReceivedStats*
parameter includes *ReceivedStatsCount*, or the *\--stats* flag to the irtt
client is *count* or *both*)
- *bytes_sent* the number of UDP payload bytes sent during the test
- *bytes_received* the number of UDP payload bytes received during the test
- *duplicates* the number of packets received with the same sequence number
- *late_packets* the number of packets received with a sequence number lower
than the previously received sequence number (one simple metric for
out-of-order packets)
- *wait* the actual time spent waiting for final packets, in nanoseconds
- *duration* the actual duration of the test, in nanoseconds, from the time just
before the first packet was sent to the time after the last packet was
received and results are starting to be calculated
- *packets_sent* the number of packets sent to the server
- *packets_received* the number of packets received from the server
- *packet_loss_percent* 100 * (*packets_sent* - *packets_received*) / *packets_sent*
- *upstream_loss_percent* 100 * (*packets_sent* - *server_packets_received* /
*packets_sent*) (always present, but only valid if *server_packets_received*
is valid)
- *downstream_loss_percent* 100 * (*server_packets_received* - *packets_received* /
*server_packets_received*) (always present, but only valid if
*server_packets_received* is valid)
- *duplicate_percent* 100 * *duplicates* / *packets_received*
- *late_packets_percent* 100 * *late_packets* / *packets_received*
- *ipdv_send* a duration stats object for the send
[IPDV](https://en.wikipedia.org/wiki/Packet_delay_variation)
**(only available if server timestamps are enabled)**
- *ipdv_receive* a duration stats object for the receive
[IPDV](https://en.wikipedia.org/wiki/Packet_delay_variation)
**(only available if server timestamps are enabled)**
- *ipdv_round_trip* a duration stats object for the round-trip
[IPDV](https://en.wikipedia.org/wiki/Packet_delay_variation)
**(available regardless of whether server timestamps are enabled or not)**
- *server_processing_time* a duration stats object for the time the server took
after it received the packet to when it sent the response **(only available
when both send and receive timestamps are enabled)**
- *timer_err_percent* the mean of the absolute values of the timer error, as a
percentage of the interval
- *timer_misses* the number of times the timer missed the interval (was at least
50% over the scheduled time)
- *timer_miss_percent* 100 * *timer_misses* / expected packets sent
- *send_rate* the send bitrate (bits-per-second and corresponding string),
calculated using the number of UDP payload bytes sent between the time right
before the first send call and the time right after the last send call
- *receive_rate* the receive bitrate (bits-per-second and corresponding string),
calculated using the number of UDP payload bytes received between the time right
after the first receive call and the time right after the last receive call
## round_trips
each round-trip is a single request to / reply from the server
```
"round_trips": [
{
"seqno": 0,
"lost": false,
"timestamps": {
"client": {
"receive": {
"wall": 1508180723502871779,
"monotonic": 2921143
},
"send": {
"wall": 1508180723502727340,
"monotonic": 2776704
}
},
"server": {
"receive": {
"wall": 1508180723502816623,
"monotonic": 32644353327
},
"send": {
"wall": 1508180723502826602,
"monotonic": 32644363306
}
}
},
"delay": {
"receive": 45177,
"rtt": 134460,
"send": 89283
},
"ipdv": {}
},
{
"seqno": 1,
"lost": false,
"timestamps": {
"client": {
"receive": {
"wall": 1508180723702917735,
"monotonic": 202967099
},
"send": {
"wall": 1508180723702807328,
"monotonic": 202856692
}
},
"server": {
"receive": {
"wall": 1508180723702861515,
"monotonic": 32844398219
},
"send": {
"wall": 1508180723702872467,
"monotonic": 32844409171
}
}
},
"delay": {
"receive": 45268,
"rtt": 99455,
"send": 54187
},
"ipdv": {
"receive": 91,
"rtt": -35005,
"send": -35096
}
},
{
"seqno": 2,
"lost": true,
"timestamps": {
"client": {
"receive": {},
"send": {
"wall": 1508180723902925971,
"monotonic": 402975335
}
},
"server": {
"receive": {},
"send": {}
}
},
"delay": {},
"ipdv": {}
}
]
```
**Note:** *wall* values are from Go's *time.Time.UnixNano()*, the number of nanoseconds
elapsed since January 1, 1970 UTC
**Note:** *monotonic* values are the number of nanoseconds since the start of the test for
the client, and since start of the process for the server
- *seqno* the sequence number
- *lost* the lost status of the packet, which can be one of *false*, *true*,
*true_down* or *true_up*. The *true_down* and *true_up* values are only
possible if the *ReceivedStats* parameter includes *ReceivedStatsWindow*
(irtt client *\--stats* flag). Even then, if it could not be determined whether
the packet was lost upstream or downstream, the value *true* is used.
- *timestamps* the client and server timestamps
- *client* the client send and receive wall and monotonic timestamps
**(*receive* values only present if *lost* is false)**
- *server* the server send and receive wall and monotonic timestamps **(both
*send* and *receive* values not present if *lost* is true)**, and
additionally:
- *send* values are not present if the StampAt (irtt client *\--tstamp* flag)
does not include send timestamps
- *receive* values are not present if the StampAt (irtt client *\--tstamp*
flag) does not include receive timestamps
- *wall* values are not present if the Clock (irtt client *\--clock* flag) does
not include wall values or server timestamps are not enabled
- *monotonic* values are not present if the Clock (irtt client *\--clock*
flag) does not include monotonic values or server timestamps are not enabled
- *delay* an object containing the delay values
- *receive* the one-way receive delay, in nanoseconds **(present only if
server timestamps are enabled and at least one wall clock value is
available)**
- *rtt* the round-trip time, in nanoseconds, always present
- *send* the one-way send delay, in nanoseconds **(present only if server
timestamps are enabled and at least one wall clock value is available)**
- *ipdv* an object containing the
[IPDV](https://en.wikipedia.org/wiki/Packet_delay_variation) values
**(attributes present only for *seqno* > 0, and if *lost* is *false* for both
the current and previous *round_trip*)**
- *receive* the difference in receive delay relative to the previous packet
**(present only if at least one server timestamp is available)**
- *rtt* the difference in round-trip time relative to the previous packet
(always present for *seqno* > 0)
- *send* the difference in send delay relative to the previous packet
**(present only if at least one server timestamp is available)**
# EXIT STATUS
*irtt client* exits with one of the following status codes:
Code | Meaning
---- | -------
0 | Success
1 | Runtime error
2 | Command line error
3 | Two interrupt signals received
# WARNINGS
It is possible with the irtt client to dramatically harm network performance
by using intervals that are too low, particularly in combination with large
packet lengths. Careful consideration should be given before using
sub-millisecond intervals, not only because of the impact on the network, but
also because:
- Timer accuracy at sub-millisecond intervals may begin to suffer without
the use of a custom kernel or the busy timer (which pins the CPU)
- Memory consumption for results storage and system CPU time both rise rapidly
- The granularity of the results reported may very well not be required
# EXAMPLES
$ irtt client localhost
: Sends requests once per second for one minute to localhost.
$ irtt client -i 200ms -d 10s -o - localhost
: Sends requests every 0.2 sec for 10 seconds to localhost. Writes JSON
output to stdout.
$ irtt client -i 20ms -d 1m -l 172 \--fill=rand \--sfill=rand 192.168.100.10
: Sends requests every 20ms for one minute to 192.168.100.10. Fills both the
client and server payload with random data. This simulates a G.711 VoIP
conversation, one of the most commonly used codecs for VoIP as of this
writing.
$ irtt client -i 0.1s -d 5s -6 \--dscp=46 irtt.example.org
: Sends requests with IPv6 every 100ms for 5 seconds to irtt.example.org.
Sets the DSCP value (ToS field) of requests and responses to 46
(Expedited Forwarding).
$ irtt client \--hmac=secret -d 10s "[2001:db8:8f::2/32]:64381"
: Sends requests to the specified IPv6 IP on port 64381 every second for
10 seconds. Adds an HMAC to each packet with the key *secret*.
# SEE ALSO
[irtt(1)](irtt.html), [irtt-server(1)](irtt-server.html)
[IRTT GitHub repository](https://github.com/peteheist/irtt/)
irtt/doc/irtt-server.md 0000644 0001751 0001751 00000014245 13240047124 014076 0 ustar pete pete % IRTT-SERVER(1) v0.9.0 | IRTT Manual
%
% February 11, 2018
# NAME
irtt-server - Isochronous Round-Trip Time Server
# SYNOPSIS
irtt server [*args*]
# DESCRIPTION
*irtt server* is the server for [irtt(1)](irtt.html).
# OPTIONS
-b *addresses*
: Bind addresses (default ":2112"), comma separated list of:
Format | Address Type
----------- | ------------
:port | unspecified address with port, use with care
host | host with default port 2112, see [Host formats](#host-formats) below
host:port | host with specified port, see [Host formats](#host-formats) below
%iface | all addresses on interface iface with default port 2112
%iface:port | all addresses on interface iface with port
**Note:** iface strings may contain * to match multiple interfaces
-d *duration*
: Max test duration, or 0 for no maximum (default 0s, see
[Duration units](#duration-units) below)
-i *interval*
: Min send interval, or 0 for no minimum (default 10ms, see
[Duration units](#duration-units) below)
-l *length*
: Max packet length (default 0), or 0 for no maximum. Numbers less than
size of required headers will cause test packets to be dropped.
\--hmac=*key*
: Add HMAC with *key* (0x for hex) to all packets, provides:
- Dropping of all packets without a correct HMAC
- Protection for server against unauthorized discovery and use
\--timeout=*duration*
: Timeout for closing connections if no requests received on a
connection (default 1m0s, see [Duration units](#duration-units) below).
0 means no timeout (not recommended, especially on public servers).
Max client interval will be restricted to timeout/4.
\--pburst=*#*
: Packet burst allowed before enforcing minimum interval (default 5)
\--fill=*fill*
: Payload fill if not requested (default pattern:69727474). Possible values
include:
Value | Fill
------------ | ------------
*none* | Echo client payload (insecure on public servers)
*rand* | Use random bytes from Go's math.rand
*pattern:*XX | Use repeating pattern of hex (default 69727474)
\--allow-fills=*fills*
: Comma separated patterns of fill requests to allow (default rand). See
options for *--fill*. Notes:
- Patterns may contain * for matching
- Allowing non-random fills insecure on public servers
- Use *\--allow-fills=""* to disallow all fill requests
\--tstamp=*modes*
: Timestamp modes to allow (default dual). Possible values:
Value | Allowed Timestamps
-------- | ------------------
*none* | Don't allow any timestamps
*single* | Allow a single timestamp (send, receive or midpoint)
*dual* | Allow dual timestamps
\--no-dscp
: Don't allow setting dscp (default false)
\--set-src-ip
: Set source IP address on all outgoing packets from listeners on
unspecified IP addresses (use for more reliable reply routing, but
increases per-packet heap allocations)
\--gc=*mode*
: Sets garbage collection mode (default on). Possible values:
Value | Meaning
------ | ------------------
*on* | Garbage collector always on
*off* | Garbage collector always off
*idle* | Garbage collector enabled only when idle
\--thread
: Lock request handling goroutines to OS threads
-h
: Show help
-v
: Show version
## Host formats
Hosts may be either hostnames (for IPv4 or IPv6) or IP addresses. IPv6
addresses must be surrounded by brackets and may include a zone after the %
character. Examples:
Type | Example
--------------- | -------
IPv4 IP | 192.168.1.10
IPv6 IP | [2001:db8:8f::2/32]
IPv4/6 hostname | localhost
**Note:** IPv6 addresses must be quoted in most shells.
## Duration units
Durations are a sequence of decimal numbers, each with optional fraction, and
unit suffix, such as: "300ms", "1m30s" or "2.5m". Sanity not enforced.
Suffix | Unit
------ | ----
h | hours
m | minutes
s | seconds
ms | milliseconds
ns | nanoseconds
# SECURITY
Running an IRTT server that's open to the outside world requires some additional
attention. For starters, irtt server's command line flags should be used to, at
a minimum:
- Restrict the duration (*-d*), interval (*-i*) and length (*-l*) of tests,
particularly for public servers
- Set an HMAC key (*\--hmac*) for private servers to prevent unauthorized
discovery and use
In addition, there are various systemd(1) options available for securing
services. The irtt.service file included with the distribution sets many
commonly used options, but should not be considered exhaustive.
To secure a server for public use, additional steps may be taken that are
outside of the scope of this documentation, including but not limited to:
- Setting up an iptables firewall (only UDP port 2112 must be open)
- Setting up a chroot jail
It should be noted that there are no known security vulnerabilities in the Go
language at this time, and the steps above, in particular the chroot jail, may
or may not serve to enhance security in any way. Go-based servers are generally
regarded as safe because of Go's high-level language constructs for memory
management, and at this time IRTT makes no use of Go's
[unsafe](https://golang.org/pkg/unsafe/) package.
# EXIT STATUS
*irtt server* exits with one of the following status codes:
Code | Meaning
---- | -------
0 | Success
1 | Runtime error
2 | Command line error
3 | Two interrupt signals received
# EXAMPLES
$ irtt server
: Starts the server and listens on all addresses (unspecified address)
$ irtt server -d 30s -i 20ms -l 256 \--fill=rand \--allow-fills=""
: Starts the server and listens on all addresses, setting the maximum
test duration to 30 seconds, minimum interval to 20 ms, and maximum
packet length to 256 bytes. Disallows fill requests and forces all
return packets to be filled with random data.
$ irtt server -b 192.168.100.11:64381 \--hmac=secret
: Starts the server and binds to IPv4 address 192.168.100.11, port 64381.
Requires a valid HMAC on all packets with the key *secret*, otherwise
packets are dropped.
# SEE ALSO
[irtt(1)](irtt.html), [irtt-client(1)](irtt-client.html)
[IRTT GitHub repository](https://github.com/peteheist/irtt/)
irtt/doc/irtt-client.1 0000644 0001751 0001751 00000073207 13240047124 013611 0 ustar pete pete .\"t
.\" Automatically generated by Pandoc 2.1.1
.\"
.TH "IRTT\-CLIENT" "1" "February 11, 2018" "v0.9.0" "IRTT Manual"
.hy
.SH NAME
.PP
irtt\-client \- Isochronous Round\-Trip Time Client
.SH SYNOPSIS
.PP
irtt client [\f[I]args\f[]]
.SH DESCRIPTION
.PP
\f[I]irtt client\f[] is the client for irtt(1) (irtt.html).
.SH OPTIONS
.TP
.B \-d \f[I]duration\f[]
Total time to send (default 1m0s, see Duration units below)
.RS
.RE
.TP
.B \-i \f[I]interval\f[]
Send interval (default 1s, see Duration units below)
.RS
.RE
.TP
.B \-l \f[I]length\f[]
Length of packet (default 0, increased as necessary for required
headers), common values:
.RS
.IP \[bu] 2
1472 (max unfragmented size of IPv4 datagram for 1500 byte MTU)
.IP \[bu] 2
1452 (max unfragmented size of IPv6 datagram for 1500 byte MTU)
.RE
.TP
.B \-o \f[I]file\f[]
Write JSON output to file (use `\-' for stdout).
The extension used for \f[I]file\f[] controls the gzip behavior as
follows (output to stdout is not gzipped):
.RS
.PP
.TS
tab(@);
l l.
T{
Extension
T}@T{
Behavior
T}
_
T{
none
T}@T{
extension .json.gz is added, output is gzipped
T}
T{
\&.json.gz
T}@T{
output is gzipped
T}
T{
\&.gz
T}@T{
output is gzipped, extension changed to .json.gz
T}
T{
\&.json
T}@T{
output is not gzipped
T}
.TE
.RE
.TP
.B \-q
Quiet, suppress per\-packet output
.RS
.RE
.TP
.B \-Q
Really quiet, suppress all output except errors to stderr
.RS
.RE
.TP
.B \-n
No test, connect to the server and validate test parameters but don't
run the test
.RS
.RE
.TP
.B \-\-stats=\f[I]stats\f[]
Server stats on received packets (default \f[I]both\f[]).
Possible values:
.RS
.PP
.TS
tab(@);
l l.
T{
Value
T}@T{
Meaning
T}
_
T{
\f[I]none\f[]
T}@T{
no server stats on received packets
T}
T{
\f[I]count\f[]
T}@T{
total count of received packets
T}
T{
\f[I]window\f[]
T}@T{
receipt status of last 64 packets with each reply
T}
T{
\f[I]both\f[]
T}@T{
both count and window
T}
.TE
.RE
.TP
.B \-\-tstamp=\f[I]mode\f[]
Server timestamp mode (default \f[I]both\f[]).
Possible values:
.RS
.PP
.TS
tab(@);
l l.
T{
Value
T}@T{
Meaning
T}
_
T{
\f[I]none\f[]
T}@T{
request no timestamps
T}
T{
\f[I]send\f[]
T}@T{
request timestamp at server send
T}
T{
\f[I]receive\f[]
T}@T{
request timestamp at server receive
T}
T{
\f[I]both\f[]
T}@T{
request both send and receive timestamps
T}
T{
\f[I]midpoint\f[]
T}@T{
request midpoint timestamp (send/receive avg)
T}
.TE
.RE
.TP
.B \-\-clock=\f[I]clock\f[]
Clock/s used for server timestamps (default \f[I]both\f[]).
Possible values:
.RS
.PP
.TS
tab(@);
l l.
T{
Value
T}@T{
Meaning
T}
_
T{
\f[I]wall\f[]
T}@T{
wall clock only
T}
T{
\f[I]monotonic\f[]
T}@T{
monotonic clock only
T}
T{
\f[I]both\f[]
T}@T{
both clocks
T}
.TE
.RE
.TP
.B \-\-dscp=\f[I]dscp\f[]
DSCP (ToS) value (default 0, 0x prefix for hex).
Common values:
.RS
.PP
.TS
tab(@);
l l.
T{
Value
T}@T{
Meaning
T}
_
T{
0
T}@T{
Best effort
T}
T{
8
T}@T{
CS1\- Bulk
T}
T{
40
T}@T{
CS5\- Video
T}
T{
46
T}@T{
EF\- Expedited forwarding
T}
.TE
.PP
DSCP & ToS (https://www.tucny.com/Home/dscp-tos)
.RE
.TP
.B \-\-df=\f[I]DF\f[]
Setting for do not fragment (DF) bit in all packets.
Possible values:
.RS
.PP
.TS
tab(@);
l l.
T{
Value
T}@T{
Meaning
T}
_
T{
\f[I]default\f[]
T}@T{
OS default
T}
T{
\f[I]false\f[]
T}@T{
DF bit not set
T}
T{
\f[I]true\f[]
T}@T{
DF bit set
T}
.TE
.RE
.TP
.B \-\-wait=\f[I]wait\f[]
Wait time at end of test for unreceived replies (default 3x4s).
Possible values:
.RS
.PP
.TS
tab(@);
l l.
T{
Format
T}@T{
Meaning
T}
_
T{
#\f[I]x\f[]duration
T}@T{
# times max RTT, or duration if no response
T}
T{
#\f[I]r\f[]duration
T}@T{
# times RTT, or duration if no response
T}
T{
duration
T}@T{
fixed duration (see Duration units below)
T}
.TE
.PP
Examples:
.PP
.TS
tab(@);
l l.
T{
Example
T}@T{
Meaning
T}
_
T{
3x4s
T}@T{
3 times max RTT, or 4 seconds if no response
T}
T{
1500ms
T}@T{
fixed 1500 milliseconds
T}
.TE
.RE
.TP
.B \-\-timer=\f[I]timer\f[]
Timer for waiting to send packets (default comp).
Possible values:
.RS
.PP
.TS
tab(@);
l l.
T{
Value
T}@T{
Meaning
T}
_
T{
\f[I]simple\f[]
T}@T{
Go's standard time.Timer
T}
T{
\f[I]comp\f[]
T}@T{
Simple timer with error compensation (see \-tcomp)
T}
T{
\f[I]hybrid:\f[]#
T}@T{
Hybrid comp/busy timer with sleep factor (default 0.95)
T}
T{
\f[I]busy\f[]
T}@T{
busy wait loop (high precision and CPU, blasphemy)
T}
.TE
.RE
.TP
.B \-\-tcomp=\f[I]alg\f[]
Comp timer averaging algorithm (default exp:0.10).
Possible values:
.RS
.PP
.TS
tab(@);
l l.
T{
Value
T}@T{
Meaning
T}
_
T{
\f[I]avg\f[]
T}@T{
Cumulative average error
T}
T{
\f[I]win:\f[]#
T}@T{
Moving average error with window # (default 5)
T}
T{
\f[I]exp:\f[]#
T}@T{
Exponential average with alpha # (default 0.10)
T}
.TE
.RE
.TP
.B \-\-fill=\f[I]fill\f[]
Fill payload with given data (default none).
Possible values:
.RS
.PP
.TS
tab(@);
l l.
T{
Value
T}@T{
Meaning
T}
_
T{
\f[I]none\f[]
T}@T{
Leave payload as all zeroes
T}
T{
\f[I]rand\f[]
T}@T{
Use random bytes from Go's math.rand
T}
T{
\f[I]pattern:\f[]XX
T}@T{
Use repeating pattern of hex (default 69727474)
T}
.TE
.RE
.TP
.B \-\-fill\-one
Fill only once and repeat for all packets
.RS
.RE
.TP
.B \-\-sfill=fill
Request server fill (default not specified).
See values for \[en]fill.
Server must support and allow this fill with \[en]allow\-fills.
.RS
.RE
.TP
.B \-\-local=addr
Local address (default from OS).
Possible values:
.RS
.PP
.TS
tab(@);
l l.
T{
Value
T}@T{
Meaning
T}
_
T{
\f[I]:port\f[]
T}@T{
Unspecified address (all IPv4/IPv6 addresses) with port
T}
T{
\f[I]host\f[]
T}@T{
Host with dynamic port, see Host formats below
T}
T{
\f[I]host:port\f[]
T}@T{
Host with specified port, see Host formats below
T}
.TE
.RE
.TP
.B \-\-hmac=key
Add HMAC with key (0x for hex) to all packets, provides:
.RS
.IP \[bu] 2
Dropping of all packets without a correct HMAC
.IP \[bu] 2
Protection for server against unauthorized discovery and use
.RE
.TP
.B \-4
IPv4 only
.RS
.RE
.TP
.B \-6
IPv6 only
.RS
.RE
.TP
.B \-\-timeouts=\f[I]durations\f[]
Timeouts used when connecting to server (default 1s,2s,4s,8s).
Comma separated list of durations (see Duration units below).
Total wait time will be up to the sum of these Durations.
Max packets sent is up to the number of Durations.
Minimum timeout duration is 200ms.
.RS
.RE
.TP
.B \-\-ttl=\f[I]ttl\f[]
Time to live (default 0, meaning use OS default)
.RS
.RE
.TP
.B \-\-loose
Accept and use any server restricted test parameters instead of exiting
with nonzero status.
.RS
.RE
.TP
.B \-\-thread
Lock sending and receiving goroutines to OS threads
.RS
.RE
.TP
.B \-h
Show help
.RS
.RE
.TP
.B \-v
Show version
.RS
.RE
.SS Host formats
.PP
Hosts may be either hostnames (for IPv4 or IPv6) or IP addresses.
IPv6 addresses must be surrounded by brackets and may include a zone
after the % character.
Examples:
.PP
.TS
tab(@);
l l.
T{
Type
T}@T{
Example
T}
_
T{
IPv4 IP
T}@T{
192.168.1.10
T}
T{
IPv6 IP
T}@T{
[2001:db8:8f::2/32]
T}
T{
IPv4/6 hostname
T}@T{
localhost
T}
.TE
.PP
\f[B]Note:\f[] IPv6 addresses must be quoted in most shells.
.SS Duration units
.PP
Durations are a sequence of decimal numbers, each with optional
fraction, and unit suffix, such as: \[lq]300ms\[rq], \[lq]1m30s\[rq] or
\[lq]2.5m\[rq].
Sanity not enforced.
.PP
.TS
tab(@);
l l.
T{
Suffix
T}@T{
Unit
T}
_
T{
h
T}@T{
hours
T}
T{
m
T}@T{
minutes
T}
T{
s
T}@T{
seconds
T}
T{
ms
T}@T{
milliseconds
T}
T{
ns
T}@T{
nanoseconds
T}
.TE
.SH OUTPUT
.PP
IRTT's JSON output format consists of five top\-level objects:
.IP "1." 3
version
.IP "2." 3
system_info
.IP "3." 3
config
.IP "4." 3
stats
.IP "5." 3
round_trips
.PP
These are documented through the examples below.
All attributes are present unless otherwise \f[B]noted\f[].
.SS version
.PP
version information
.IP
.nf
\f[C]
"version":\ {
\ \ \ \ "irtt":\ "0.9.0",
\ \ \ \ "protocol":\ 1,
\ \ \ \ "json_format":\ 1
},
\f[]
.fi
.IP \[bu] 2
\f[I]irtt\f[] the IRTT version number
.IP \[bu] 2
\f[I]protocol\f[] the protocol version number (increments mean
incompatible changes)
.IP \[bu] 2
\f[I]json_format\f[] the JSON format number (increments mean
incompatible changes)
.SS system_info
.PP
a few basic pieces of system information
.IP
.nf
\f[C]
"system_info":\ {
\ \ \ \ "os":\ "darwin",
\ \ \ \ "cpus":\ 8,
\ \ \ \ "go_version":\ "go1.9.2",
\ \ \ \ "hostname":\ "tron.local"
},
\f[]
.fi
.IP \[bu] 2
\f[I]os\f[] the Operating System from Go's \f[I]runtime.GOOS\f[]
.IP \[bu] 2
\f[I]cpus\f[] the number of CPUs reported by Go's
\f[I]runtime.NumCPU()\f[], which reflects the number of logical rather
than physical CPUs.
In the example below, the number 8 is reported for a Core i7 (quad core)
with hyperthreading (2 threads per core).
.IP \[bu] 2
\f[I]go_version\f[] the version of Go the executable was built with
.IP \[bu] 2
\f[I]hostname\f[] the local hostname
.SS config
.PP
the configuration used for the test
.IP
.nf
\f[C]
"config":\ {
\ \ \ \ "local_address":\ "127.0.0.1:51203",
\ \ \ \ "remote_address":\ "127.0.0.1:2112",
\ \ \ \ "open_timeouts":\ "1s,2s,4s,8s",
\ \ \ \ "params":\ {
\ \ \ \ \ \ \ \ "proto_version":\ 1,
\ \ \ \ \ \ \ \ "duration":\ 600000000,
\ \ \ \ \ \ \ \ "interval":\ 200000000,
\ \ \ \ \ \ \ \ "length":\ 48,
\ \ \ \ \ \ \ \ "received_stats":\ "both",
\ \ \ \ \ \ \ \ "stamp_at":\ "both",
\ \ \ \ \ \ \ \ "clock":\ "both",
\ \ \ \ \ \ \ \ "dscp":\ 0,
\ \ \ \ \ \ \ \ "server_fill":\ ""
\ \ \ \ },
\ \ \ \ "loose":\ false,
\ \ \ \ "ip_version":\ "IPv4",
\ \ \ \ "df":\ 0,
\ \ \ \ "ttl":\ 0,
\ \ \ \ "timer":\ "comp",
\ \ \ \ "waiter":\ "3x4s",
\ \ \ \ "filler":\ "none",
\ \ \ \ "fill_one":\ false,
\ \ \ \ "thread_lock":\ false,
\ \ \ \ "supplied":\ {
\ \ \ \ \ \ \ \ "local_address":\ ":0",
\ \ \ \ \ \ \ \ "remote_address":\ "localhost",
\ \ \ \ \ \ \ \ "open_timeouts":\ "1s,2s,4s,8s",
\ \ \ \ \ \ \ \ "params":\ {
\ \ \ \ \ \ \ \ \ \ \ \ "proto_version":\ 1,
\ \ \ \ \ \ \ \ \ \ \ \ "duration":\ 600000000,
\ \ \ \ \ \ \ \ \ \ \ \ "interval":\ 200000000,
\ \ \ \ \ \ \ \ \ \ \ \ "length":\ 0,
\ \ \ \ \ \ \ \ \ \ \ \ "received_stats":\ "both",
\ \ \ \ \ \ \ \ \ \ \ \ "stamp_at":\ "both",
\ \ \ \ \ \ \ \ \ \ \ \ "clock":\ "both",
\ \ \ \ \ \ \ \ \ \ \ \ "dscp":\ 0,
\ \ \ \ \ \ \ \ \ \ \ \ "server_fill":\ ""
\ \ \ \ \ \ \ \ },
\ \ \ \ \ \ \ \ "loose":\ false,
\ \ \ \ \ \ \ \ "ip_version":\ "IPv4+6",
\ \ \ \ \ \ \ \ "df":\ 0,
\ \ \ \ \ \ \ \ "ttl":\ 0,
\ \ \ \ \ \ \ \ "timer":\ "comp",
\ \ \ \ \ \ \ \ "waiter":\ "3x4s",
\ \ \ \ \ \ \ \ "filler":\ "none",
\ \ \ \ \ \ \ \ "fill_one":\ false,
\ \ \ \ \ \ \ \ "thread_lock":\ false
\ \ \ \ }
},
\f[]
.fi
.IP \[bu] 2
\f[I]local_address\f[] the local address (IP:port) for the client
.IP \[bu] 2
\f[I]remote_address\f[] the remote address (IP:port) for the server
.IP \[bu] 2
\f[I]open_timeouts\f[] a list of timeout durations used after an open
packet is sent
.IP \[bu] 2
\f[I]params\f[] are the parameters that were negotiated with the server,
including:
.RS 2
.IP \[bu] 2
\f[I]proto_version\f[] protocol version number
.IP \[bu] 2
\f[I]duration\f[] duration of the test, in nanoseconds
.IP \[bu] 2
\f[I]interval\f[] send interval, in nanoseconds
.IP \[bu] 2
\f[I]length\f[] packet length
.IP \[bu] 2
\f[I]received_stats\f[] statistics for packets received by server (none,
count, window or both, \f[I]\-\-stats\f[] flag for irtt client)
.IP \[bu] 2
\f[I]stamp_at\f[] timestamp selection parameter (none, send, receive,
both or midpoint, \f[I]\-\-tstamp\f[] flag for irtt client)
.IP \[bu] 2
\f[I]clock\f[] clock selection parameter (wall or monotonic,
\f[I]\-\-clock\f[] flag for irtt client)
.IP \[bu] 2
\f[I]dscp\f[] the
DSCP (https://en.wikipedia.org/wiki/Differentiated_services) value
.IP \[bu] 2
\f[I]server_fill\f[] the requested server fill (\f[I]\-\-sfill\f[] flag
for irtt client)
.RE
.IP \[bu] 2
\f[I]loose\f[] if true, client accepts and uses restricted server
parameters, with a warning
.IP \[bu] 2
\f[I]ip_version\f[] the IP version used (IPv4 or IPv6)
.IP \[bu] 2
\f[I]df\f[] the do\-not\-fragment setting (0 == OS default, 1 == false,
2 == true)
.IP \[bu] 2
\f[I]ttl\f[] the IP
time\-to\-live (https://en.wikipedia.org/wiki/Time_to_live) value
.IP \[bu] 2
\f[I]timer\f[] the timer used: simple, comp, hybrid or busy (irtt client
\-\-timer flag)
.IP \[bu] 2
\f[I]waiter\f[] the waiter used: fixed duration, multiple of RTT or
multiple of max RTT (irtt client \f[I]\-\-wait\f[] flag)
.IP \[bu] 2
\f[I]filler\f[] the packet filler used: none, rand or pattern (irtt
client \f[I]\-\-fill\f[] flag)
.IP \[bu] 2
\f[I]fill_one\f[] whether to fill only once and repeat for all packets
(irtt client \f[I]\-\-fill\-one\f[] flag)
.IP \[bu] 2
\f[I]thread_lock\f[] whether to lock packet handling goroutines to OS
threads
.IP \[bu] 2
\f[I]supplied\f[] a nested \f[I]config\f[] object with the configuration
as originally supplied to the API or \f[I]irtt\f[] command.
The supplied configuration can differ from the final configuration in
the following ways:
.RS 2
.IP \[bu] 2
\f[I]local_address\f[] and \f[I]remote_address\f[] may have hostnames or
named ports before being resolved to an IP and numbered port
.IP \[bu] 2
\f[I]ip_version\f[] may be IPv4+6 before it is determined after address
resolution
.IP \[bu] 2
\f[I]params\f[] may be different before the server applies restrictions
based on its configuration
.RE
.SS stats
.PP
statistics for the results
.IP
.nf
\f[C]
"stats":\ {
\ \ \ \ "start_time":\ "2017\-10\-16T21:05:23.502719056+02:00",
\ \ \ \ "send_call":\ {
\ \ \ \ \ \ \ \ "total":\ 79547,
\ \ \ \ \ \ \ \ "n":\ 3,
\ \ \ \ \ \ \ \ "min":\ 17790,
\ \ \ \ \ \ \ \ "max":\ 33926,
\ \ \ \ \ \ \ \ "mean":\ 26515,
\ \ \ \ \ \ \ \ "stddev":\ 8148,
\ \ \ \ \ \ \ \ "variance":\ 66390200
\ \ \ \ },
\ \ \ \ "timer_error":\ {
\ \ \ \ \ \ \ \ "total":\ 227261,
\ \ \ \ \ \ \ \ "n":\ 2,
\ \ \ \ \ \ \ \ "min":\ 59003,
\ \ \ \ \ \ \ \ "max":\ 168258,
\ \ \ \ \ \ \ \ "mean":\ 113630,
\ \ \ \ \ \ \ \ "stddev":\ 77254,
\ \ \ \ \ \ \ \ "variance":\ 5968327512
\ \ \ \ },
\ \ \ \ "rtt":\ {
\ \ \ \ \ \ \ \ "total":\ 233915,
\ \ \ \ \ \ \ \ "n":\ 2,
\ \ \ \ \ \ \ \ "min":\ 99455,
\ \ \ \ \ \ \ \ "max":\ 134460,
\ \ \ \ \ \ \ \ "mean":\ 116957,
\ \ \ \ \ \ \ \ "median":\ 116957,
\ \ \ \ \ \ \ \ "stddev":\ 24752,
\ \ \ \ \ \ \ \ "variance":\ 612675012
\ \ \ \ },
\ \ \ \ "send_delay":\ {
\ \ \ \ \ \ \ \ "total":\ 143470,
\ \ \ \ \ \ \ \ "n":\ 2,
\ \ \ \ \ \ \ \ "min":\ 54187,
\ \ \ \ \ \ \ \ "max":\ 89283,
\ \ \ \ \ \ \ \ "mean":\ 71735,
\ \ \ \ \ \ \ \ "median":\ 71735,
\ \ \ \ \ \ \ \ "stddev":\ 24816,
\ \ \ \ \ \ \ \ "variance":\ 615864608
\ \ \ \ },
\ \ \ \ "receive_delay":\ {
\ \ \ \ \ \ \ \ "total":\ 90445,
\ \ \ \ \ \ \ \ "n":\ 2,
\ \ \ \ \ \ \ \ "min":\ 45177,
\ \ \ \ \ \ \ \ "max":\ 45268,
\ \ \ \ \ \ \ \ "mean":\ 45222,
\ \ \ \ \ \ \ \ "median":\ 45222,
\ \ \ \ \ \ \ \ "stddev":\ 64,
\ \ \ \ \ \ \ \ "variance":\ 4140
\ \ \ \ },
\ \ \ \ "server_packets_received":\ 2,
\ \ \ \ "bytes_sent":\ 144,
\ \ \ \ "bytes_received":\ 96,
\ \ \ \ "duplicates":\ 0,
\ \ \ \ "late_packets":\ 0,
\ \ \ \ "wait":\ 403380,
\ \ \ \ "duration":\ 400964028,
\ \ \ \ "packets_sent":\ 3,
\ \ \ \ "packets_received":\ 2,
\ \ \ \ "packet_loss_percent":\ 33.333333333333336,
\ \ \ \ "upstream_loss_percent":\ 33.333333333333336,
\ \ \ \ "downstream_loss_percent":\ 0,
\ \ \ \ "duplicate_percent":\ 0,
\ \ \ \ "late_packets_percent":\ 0,
\ \ \ \ "ipdv_send":\ {
\ \ \ \ \ \ \ \ "total":\ 35096,
\ \ \ \ \ \ \ \ "n":\ 1,
\ \ \ \ \ \ \ \ "min":\ 35096,
\ \ \ \ \ \ \ \ "max":\ 35096,
\ \ \ \ \ \ \ \ "mean":\ 35096,
\ \ \ \ \ \ \ \ "median":\ 35096,
\ \ \ \ \ \ \ \ "stddev":\ 0,
\ \ \ \ \ \ \ \ "variance":\ 0
\ \ \ \ },
\ \ \ \ "ipdv_receive":\ {
\ \ \ \ \ \ \ \ "total":\ 91,
\ \ \ \ \ \ \ \ "n":\ 1,
\ \ \ \ \ \ \ \ "min":\ 91,
\ \ \ \ \ \ \ \ "max":\ 91,
\ \ \ \ \ \ \ \ "mean":\ 91,
\ \ \ \ \ \ \ \ "median":\ 91,
\ \ \ \ \ \ \ \ "stddev":\ 0,
\ \ \ \ \ \ \ \ "variance":\ 0
\ \ \ \ },
\ \ \ \ "ipdv_round_trip":\ {
\ \ \ \ \ \ \ \ "total":\ 35005,
\ \ \ \ \ \ \ \ "n":\ 1,
\ \ \ \ \ \ \ \ "min":\ 35005,
\ \ \ \ \ \ \ \ "max":\ 35005,
\ \ \ \ \ \ \ \ "mean":\ 35005,
\ \ \ \ \ \ \ \ "median":\ 35005,
\ \ \ \ \ \ \ \ "stddev":\ 0,
\ \ \ \ \ \ \ \ "variance":\ 0
\ \ \ \ },
\ \ \ \ "server_processing_time":\ {
\ \ \ \ \ \ \ \ "total":\ 20931,
\ \ \ \ \ \ \ \ "n":\ 2,
\ \ \ \ \ \ \ \ "min":\ 9979,
\ \ \ \ \ \ \ \ "max":\ 10952,
\ \ \ \ \ \ \ \ "mean":\ 10465,
\ \ \ \ \ \ \ \ "stddev":\ 688,
\ \ \ \ \ \ \ \ "variance":\ 473364
\ \ \ \ },
\ \ \ \ "timer_err_percent":\ 0.056815,
\ \ \ \ "timer_misses":\ 0,
\ \ \ \ "timer_miss_percent":\ 0,
\ \ \ \ "send_rate":\ {
\ \ \ \ \ \ \ \ "bps":\ 2878,
\ \ \ \ \ \ \ \ "string":\ "2.9\ Kbps"
\ \ \ \ },
\ \ \ \ "receive_rate":\ {
\ \ \ \ \ \ \ \ "bps":\ 3839,
\ \ \ \ \ \ \ \ "string":\ "3.8\ Kbps"
\ \ \ \ }
},
\f[]
.fi
.PP
\f[B]Note:\f[] In the \f[I]stats\f[] object, a \f[I]duration stats\f[]
class of object repeats and will not be repeated in the individual
descriptions.
It contains statistics about nanosecond duration values and has the
following attributes:
.IP \[bu] 2
\f[I]total\f[] the total of the duration values
.IP \[bu] 2
\f[I]n\f[] the number of duration values
.IP \[bu] 2
\f[I]min\f[] the minimum duration value
.IP \[bu] 2
\f[I]max\f[] the maximum duration value
.IP \[bu] 2
\f[I]mean\f[] the mean duration value
.IP \[bu] 2
\f[I]stddev\f[] the standard deviation
.IP \[bu] 2
\f[I]variance\f[] the variance
.PP
The regular attributes in \f[I]stats\f[] are as follows:
.IP \[bu] 2
\f[I]start_time\f[] the start time of the test, in TZ format
.IP \[bu] 2
\f[I]send_call\f[] a duration stats object for the call time when
sending packets
.IP \[bu] 2
\f[I]timer_error\f[] a duration stats object for the observed sleep time
error
.IP \[bu] 2
\f[I]rtt\f[] a duration stats object for the round\-trip time
.IP \[bu] 2
\f[I]send_delay\f[] a duration stats object for the one\-way send delay
\f[B](only available if server timestamps are enabled)\f[]
.IP \[bu] 2
\f[I]receive_delay\f[] a duration stats object for the one\-way receive
delay \f[B](only available if server timestamps are enabled)\f[]
.IP \[bu] 2
\f[I]server_packets_received\f[] the number of packets received by the
server, including duplicates (always present, but only valid if the
\f[I]ReceivedStats\f[] parameter includes \f[I]ReceivedStatsCount\f[],
or the \f[I]\-\-stats\f[] flag to the irtt client is \f[I]count\f[] or
\f[I]both\f[])
.IP \[bu] 2
\f[I]bytes_sent\f[] the number of UDP payload bytes sent during the test
.IP \[bu] 2
\f[I]bytes_received\f[] the number of UDP payload bytes received during
the test
.IP \[bu] 2
\f[I]duplicates\f[] the number of packets received with the same
sequence number
.IP \[bu] 2
\f[I]late_packets\f[] the number of packets received with a sequence
number lower than the previously received sequence number (one simple
metric for out\-of\-order packets)
.IP \[bu] 2
\f[I]wait\f[] the actual time spent waiting for final packets, in
nanoseconds
.IP \[bu] 2
\f[I]duration\f[] the actual duration of the test, in nanoseconds, from
the time just before the first packet was sent to the time after the
last packet was received and results are starting to be calculated
.IP \[bu] 2
\f[I]packets_sent\f[] the number of packets sent to the server
.IP \[bu] 2
\f[I]packets_received\f[] the number of packets received from the server
.IP \[bu] 2
\f[I]packet_loss_percent\f[] 100 * (\f[I]packets_sent\f[] \-
\f[I]packets_received\f[]) / \f[I]packets_sent\f[]
.IP \[bu] 2
\f[I]upstream_loss_percent\f[] 100 * (\f[I]packets_sent\f[] \-
\f[I]server_packets_received\f[] / \f[I]packets_sent\f[]) (always
present, but only valid if \f[I]server_packets_received\f[] is valid)
.IP \[bu] 2
\f[I]downstream_loss_percent\f[] 100 * (\f[I]server_packets_received\f[]
\- \f[I]packets_received\f[] / \f[I]server_packets_received\f[]) (always
present, but only valid if \f[I]server_packets_received\f[] is valid)
.IP \[bu] 2
\f[I]duplicate_percent\f[] 100 * \f[I]duplicates\f[] /
\f[I]packets_received\f[]
.IP \[bu] 2
\f[I]late_packets_percent\f[] 100 * \f[I]late_packets\f[] /
\f[I]packets_received\f[]
.IP \[bu] 2
\f[I]ipdv_send\f[] a duration stats object for the send
IPDV (https://en.wikipedia.org/wiki/Packet_delay_variation) \f[B](only
available if server timestamps are enabled)\f[]
.IP \[bu] 2
\f[I]ipdv_receive\f[] a duration stats object for the receive
IPDV (https://en.wikipedia.org/wiki/Packet_delay_variation) \f[B](only
available if server timestamps are enabled)\f[]
.IP \[bu] 2
\f[I]ipdv_round_trip\f[] a duration stats object for the round\-trip
IPDV (https://en.wikipedia.org/wiki/Packet_delay_variation)
\f[B](available regardless of whether server timestamps are enabled or
not)\f[]
.IP \[bu] 2
\f[I]server_processing_time\f[] a duration stats object for the time the
server took after it received the packet to when it sent the response
\f[B](only available when both send and receive timestamps are
enabled)\f[]
.IP \[bu] 2
\f[I]timer_err_percent\f[] the mean of the absolute values of the timer
error, as a percentage of the interval
.IP \[bu] 2
\f[I]timer_misses\f[] the number of times the timer missed the interval
(was at least 50% over the scheduled time)
.IP \[bu] 2
\f[I]timer_miss_percent\f[] 100 * \f[I]timer_misses\f[] / expected
packets sent
.IP \[bu] 2
\f[I]send_rate\f[] the send bitrate (bits\-per\-second and corresponding
string), calculated using the number of UDP payload bytes sent between
the time right before the first send call and the time right after the
last send call
.IP \[bu] 2
\f[I]receive_rate\f[] the receive bitrate (bits\-per\-second and
corresponding string), calculated using the number of UDP payload bytes
received between the time right after the first receive call and the
time right after the last receive call
.SS round_trips
.PP
each round\-trip is a single request to / reply from the server
.IP
.nf
\f[C]
"round_trips":\ [
\ \ \ \ {
\ \ \ \ \ \ \ \ "seqno":\ 0,
\ \ \ \ \ \ \ \ "lost":\ false,
\ \ \ \ \ \ \ \ "timestamps":\ {
\ \ \ \ \ \ \ \ \ \ \ \ "client":\ {
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "receive":\ {
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "wall":\ 1508180723502871779,
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "monotonic":\ 2921143
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ },
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "send":\ {
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "wall":\ 1508180723502727340,
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "monotonic":\ 2776704
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ }
\ \ \ \ \ \ \ \ \ \ \ \ },
\ \ \ \ \ \ \ \ \ \ \ \ "server":\ {
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "receive":\ {
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "wall":\ 1508180723502816623,
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "monotonic":\ 32644353327
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ },
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "send":\ {
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "wall":\ 1508180723502826602,
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "monotonic":\ 32644363306
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ }
\ \ \ \ \ \ \ \ \ \ \ \ }
\ \ \ \ \ \ \ \ },
\ \ \ \ \ \ \ \ "delay":\ {
\ \ \ \ \ \ \ \ \ \ \ \ "receive":\ 45177,
\ \ \ \ \ \ \ \ \ \ \ \ "rtt":\ 134460,
\ \ \ \ \ \ \ \ \ \ \ \ "send":\ 89283
\ \ \ \ \ \ \ \ },
\ \ \ \ \ \ \ \ "ipdv":\ {}
\ \ \ \ },
\ \ \ \ {
\ \ \ \ \ \ \ \ "seqno":\ 1,
\ \ \ \ \ \ \ \ "lost":\ false,
\ \ \ \ \ \ \ \ "timestamps":\ {
\ \ \ \ \ \ \ \ \ \ \ \ "client":\ {
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "receive":\ {
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "wall":\ 1508180723702917735,
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "monotonic":\ 202967099
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ },
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "send":\ {
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "wall":\ 1508180723702807328,
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "monotonic":\ 202856692
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ }
\ \ \ \ \ \ \ \ \ \ \ \ },
\ \ \ \ \ \ \ \ \ \ \ \ "server":\ {
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "receive":\ {
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "wall":\ 1508180723702861515,
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "monotonic":\ 32844398219
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ },
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "send":\ {
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "wall":\ 1508180723702872467,
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "monotonic":\ 32844409171
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ }
\ \ \ \ \ \ \ \ \ \ \ \ }
\ \ \ \ \ \ \ \ },
\ \ \ \ \ \ \ \ "delay":\ {
\ \ \ \ \ \ \ \ \ \ \ \ "receive":\ 45268,
\ \ \ \ \ \ \ \ \ \ \ \ "rtt":\ 99455,
\ \ \ \ \ \ \ \ \ \ \ \ "send":\ 54187
\ \ \ \ \ \ \ \ },
\ \ \ \ \ \ \ \ "ipdv":\ {
\ \ \ \ \ \ \ \ \ \ \ \ "receive":\ 91,
\ \ \ \ \ \ \ \ \ \ \ \ "rtt":\ \-35005,
\ \ \ \ \ \ \ \ \ \ \ \ "send":\ \-35096
\ \ \ \ \ \ \ \ }
\ \ \ \ },
\ \ \ \ {
\ \ \ \ \ \ \ \ "seqno":\ 2,
\ \ \ \ \ \ \ \ "lost":\ true,
\ \ \ \ \ \ \ \ "timestamps":\ {
\ \ \ \ \ \ \ \ \ \ \ \ "client":\ {
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "receive":\ {},
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "send":\ {
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "wall":\ 1508180723902925971,
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "monotonic":\ 402975335
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ }
\ \ \ \ \ \ \ \ \ \ \ \ },
\ \ \ \ \ \ \ \ \ \ \ \ "server":\ {
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "receive":\ {},
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ "send":\ {}
\ \ \ \ \ \ \ \ \ \ \ \ }
\ \ \ \ \ \ \ \ },
\ \ \ \ \ \ \ \ "delay":\ {},
\ \ \ \ \ \ \ \ "ipdv":\ {}
\ \ \ \ }
]
\f[]
.fi
.PP
\f[B]Note:\f[] \f[I]wall\f[] values are from Go's
\f[I]time.Time.UnixNano()\f[], the number of nanoseconds elapsed since
January 1, 1970 UTC
.PP
\f[B]Note:\f[] \f[I]monotonic\f[] values are the number of nanoseconds
since the start of the test for the client, and since start of the
process for the server
.IP \[bu] 2
\f[I]seqno\f[] the sequence number
.IP \[bu] 2
\f[I]lost\f[] the lost status of the packet, which can be one of
\f[I]false\f[], \f[I]true\f[], \f[I]true_down\f[] or \f[I]true_up\f[].
The \f[I]true_down\f[] and \f[I]true_up\f[] values are only possible if
the \f[I]ReceivedStats\f[] parameter includes
\f[I]ReceivedStatsWindow\f[] (irtt client \f[I]\-\-stats\f[] flag).
Even then, if it could not be determined whether the packet was lost
upstream or downstream, the value \f[I]true\f[] is used.
.IP \[bu] 2
\f[I]timestamps\f[] the client and server timestamps
.RS 2
.IP \[bu] 2
\f[I]client\f[] the client send and receive wall and monotonic
timestamps \f[B](\f[BI]receive\f[B] values only present if
\f[BI]lost\f[B] is false)\f[]
.IP \[bu] 2
\f[I]server\f[] the server send and receive wall and monotonic
timestamps \f[B](both \f[BI]send\f[B] and \f[BI]receive\f[B] values not
present if \f[BI]lost\f[B] is true)\f[], and additionally:
.RS 2
.IP \[bu] 2
\f[I]send\f[] values are not present if the StampAt (irtt client
\f[I]\-\-tstamp\f[] flag) does not include send timestamps
.IP \[bu] 2
\f[I]receive\f[] values are not present if the StampAt (irtt client
\f[I]\-\-tstamp\f[] flag) does not include receive timestamps
.IP \[bu] 2
\f[I]wall\f[] values are not present if the Clock (irtt client
\f[I]\-\-clock\f[] flag) does not include wall values or server
timestamps are not enabled
.IP \[bu] 2
\f[I]monotonic\f[] values are not present if the Clock (irtt client
\f[I]\-\-clock\f[] flag) does not include monotonic values or server
timestamps are not enabled
.RE
.RE
.IP \[bu] 2
\f[I]delay\f[] an object containing the delay values
.RS 2
.IP \[bu] 2
\f[I]receive\f[] the one\-way receive delay, in nanoseconds
\f[B](present only if server timestamps are enabled and at least one
wall clock value is available)\f[]
.IP \[bu] 2
\f[I]rtt\f[] the round\-trip time, in nanoseconds, always present
.IP \[bu] 2
\f[I]send\f[] the one\-way send delay, in nanoseconds \f[B](present only
if server timestamps are enabled and at least one wall clock value is
available)\f[]
.RE
.IP \[bu] 2
\f[I]ipdv\f[] an object containing the
IPDV (https://en.wikipedia.org/wiki/Packet_delay_variation) values
\f[B](attributes present only for \f[BI]seqno\f[B] > 0, and if
\f[BI]lost\f[B] is \f[BI]false\f[B] for both the current and previous
\f[BI]round_trip\f[B])\f[]
.RS 2
.IP \[bu] 2
\f[I]receive\f[] the difference in receive delay relative to the
previous packet \f[B](present only if at least one server timestamp is
available)\f[]
.IP \[bu] 2
\f[I]rtt\f[] the difference in round\-trip time relative to the previous
packet (always present for \f[I]seqno\f[] > 0)
.IP \[bu] 2
\f[I]send\f[] the difference in send delay relative to the previous
packet \f[B](present only if at least one server timestamp is
available)\f[]
.RE
.SH EXIT STATUS
.PP
\f[I]irtt client\f[] exits with one of the following status codes:
.PP
.TS
tab(@);
l l.
T{
Code
T}@T{
Meaning
T}
_
T{
0
T}@T{
Success
T}
T{
1
T}@T{
Runtime error
T}
T{
2
T}@T{
Command line error
T}
T{
3
T}@T{
Two interrupt signals received
T}
.TE
.SH WARNINGS
.PP
It is possible with the irtt client to dramatically harm network
performance by using intervals that are too low, particularly in
combination with large packet lengths.
Careful consideration should be given before using sub\-millisecond
intervals, not only because of the impact on the network, but also
because:
.IP \[bu] 2
Timer accuracy at sub\-millisecond intervals may begin to suffer without
the use of a custom kernel or the busy timer (which pins the CPU)
.IP \[bu] 2
Memory consumption for results storage and system CPU time both rise
rapidly
.IP \[bu] 2
The granularity of the results reported may very well not be required
.SH EXAMPLES
.TP
.B $ irtt client localhost
Sends requests once per second for one minute to localhost.
.RS
.RE
.TP
.B $ irtt client \-i 200ms \-d 10s \-o \- localhost
Sends requests every 0.2 sec for 10 seconds to localhost.
Writes JSON output to stdout.
.RS
.RE
.TP
.B $ irtt client \-i 20ms \-d 1m \-l 172 \-\-fill=rand \-\-sfill=rand 192.168.100.10
Sends requests every 20ms for one minute to 192.168.100.10.
Fills both the client and server payload with random data.
This simulates a G.711 VoIP conversation, one of the most commonly used
codecs for VoIP as of this writing.
.RS
.RE
.TP
.B $ irtt client \-i 0.1s \-d 5s \-6 \-\-dscp=46 irtt.example.org
Sends requests with IPv6 every 100ms for 5 seconds to irtt.example.org.
Sets the DSCP value (ToS field) of requests and responses to 46
(Expedited Forwarding).
.RS
.RE
.TP
.B $ irtt client \-\-hmac=secret \-d 10s \[lq][2001:db8:8f::2/32]:64381\[rq]
Sends requests to the specified IPv6 IP on port 64381 every second for
10 seconds.
Adds an HMAC to each packet with the key \f[I]secret\f[].
.RS
.RE
.SH SEE ALSO
.PP
irtt(1) (irtt.html), irtt\-server(1) (irtt-server.html)
.PP
IRTT GitHub repository (https://github.com/peteheist/irtt/)
irtt/doc/head.html 0000644 0001751 0001751 00000001151 13240047124 013045 0 ustar pete pete
irtt/doc/irtt.1 0000644 0001751 0001751 00000024556 13240047124 012340 0 ustar pete pete .\" Automatically generated by Pandoc 2.1.1
.\"
.TH "IRTT" "1" "February 11, 2018" "v0.9.0" "IRTT Manual"
.hy
.SH NAME
.PP
irtt \- Isochronous Round\-Trip Time
.SH SYNOPSIS
.PP
irtt \f[I]command\f[] [\f[I]args\f[]]
.PP
irtt help \f[I]command\f[]
.SH DESCRIPTION
.PP
IRTT measures round\-trip time and other latency related metrics using
UDP packets sent on a fixed period, and produces both text and JSON
output.
.SH COMMANDS
.TP
.B \f[I]client\f[]
runs the client
.RS
.RE
.TP
.B \f[I]server\f[]
runs the server
.RS
.RE
.TP
.B \f[I]bench\f[]
runs HMAC and fill benchmarks
.RS
.RE
.TP
.B \f[I]clock\f[]
runs wall vs monotonic clock test
.RS
.RE
.TP
.B \f[I]sleep\f[]
runs sleep accuracy test
.RS
.RE
.TP
.B \f[I]version\f[]
shows the version
.RS
.RE
.SH EXAMPLES
.PP
After installing IRTT, start a server:
.IP
.nf
\f[C]
$\ irtt\ server
IRTT\ server\ starting...
[ListenerStart]\ starting\ IPv6\ listener\ on\ [::]:2112
[ListenerStart]\ starting\ IPv4\ listener\ on\ 0.0.0.0:2112
\f[]
.fi
.PP
While that's running, run a client.
If no options are supplied, it will send a request once per second, like
ping.
Here we simulate a one minute G.711 VoIP conversation by using an
interval of 20ms and randomly filled payloads of 172 bytes:
.IP
.nf
\f[C]
$\ irtt\ client\ \-i\ 20ms\ \-l\ 172\ \-d\ 1m\ \-\-fill=rand\ \-\-sfill=rand\ \-q\ 192.168.100.10
[Connecting]\ connecting\ to\ 192.168.100.10
[Connected]\ connected\ to\ 192.168.100.10:2112
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ Min\ \ \ \ \ Mean\ \ \ Median\ \ \ \ \ \ Max\ \ Stddev
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \-\-\-\ \ \ \ \ \-\-\-\-\ \ \ \-\-\-\-\-\-\ \ \ \ \ \ \-\-\-\ \ \-\-\-\-\-\-
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ RTT\ \ 11.93ms\ \ 20.88ms\ \ \ 19.2ms\ \ 80.49ms\ \ 7.02ms
\ \ \ \ \ \ \ \ \ send\ delay\ \ \ 4.99ms\ \ 12.21ms\ \ 10.83ms\ \ 50.45ms\ \ 5.73ms
\ \ \ \ \ \ receive\ delay\ \ \ 6.38ms\ \ \ 8.66ms\ \ \ 7.86ms\ \ 69.11ms\ \ 2.89ms
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \
\ \ \ \ \ \ IPDV\ (jitter)\ \ \ \ 782ns\ \ \ 4.53ms\ \ \ 3.39ms\ \ 64.66ms\ \ \ 4.2ms
\ \ \ \ \ \ \ \ \ \ send\ IPDV\ \ \ \ 256ns\ \ \ 3.99ms\ \ \ 2.98ms\ \ 35.28ms\ \ 3.69ms
\ \ \ \ \ \ \ receive\ IPDV\ \ \ \ 896ns\ \ \ 1.78ms\ \ \ \ 966µs\ \ 62.28ms\ \ 2.86ms
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \
\ \ \ \ \ send\ call\ time\ \ \ 56.5µs\ \ \ 82.8µs\ \ \ \ \ \ \ \ \ \ \ 18.99ms\ \ \ 348µs
\ \ \ \ \ \ \ \ timer\ error\ \ \ \ \ \ \ 0s\ \ \ 21.7µs\ \ \ \ \ \ \ \ \ \ \ 19.05ms\ \ \ 356µs
\ \ server\ proc.\ time\ \ \ 23.9µs\ \ \ 26.9µs\ \ \ \ \ \ \ \ \ \ \ \ \ 141µs\ \ 11.2µs
\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ duration:\ 1m0s\ (wait\ 241.5ms)
\ \ \ packets\ sent/received:\ 2996/2979\ (0.57%\ loss)
\ server\ packets\ received:\ 2980/2996\ (0.53%/0.03%\ loss\ up/down)
\ \ \ \ \ bytes\ sent/received:\ 515312/512388
\ \ \ \ \ \ \ send/receive\ rate:\ 68.7\ Kbps\ /\ 68.4\ Kbps
\ \ \ \ \ \ \ \ \ \ \ packet\ length:\ 172\ bytes
\ \ \ \ \ \ \ \ \ \ \ \ \ timer\ stats:\ 4/3000\ (0.13%)\ missed,\ 0.11%\ error
\f[]
.fi
.PP
In the results above, the client and server are located at two different
sites, around 50km from one another, each of which connects to the
Internet via point\-to\-point WiFi.
The client is 3km NLOS through trees located near its transmitter, which
is likely the reason for the higher upstream packet loss, mean send
delay and IPDV.
.SH BUGS
.IP \[bu] 2
Windows is unable to set DSCP values for IPv6.
.IP \[bu] 2
Windows is unable to set the source IP address, so
\f[C]\-\-set\-src\-ip\f[] may not be used on the server.
.IP \[bu] 2
The server doesn't run well on 32\-bit Windows platforms.
When connecting with a client, you may see
\f[C]Terminated\ due\ to\ receive\ error\f[].
To work around this, disable dual timestamps from the client by
including \f[C]\-\-tstamp=midpoint\f[].
.SH LIMITATIONS
.RS
.PP
\[lq]It is the limitations of software that give it life.\[rq]
.IP
.nf
\f[C]
\-Me,\ justifying\ my\ limitations
\f[]
.fi
.RE
.SS Isochronous (fixed period) send schedule
.PP
Currently, IRTT only sends packets on a fixed period, foregoing the
ability to simulate arbitrary traffic.
Accepting this limitation offers some benefits:
.IP \[bu] 2
It's easy to implement
.IP \[bu] 2
It's easy to calculate how many packets and how much data will be sent
in a given time
.IP \[bu] 2
It simplifies timer error compensation
.PP
Also, isochronous packets are commonly seen in VoIP, games and some
streaming media, so it already simulates an array of common types of
traffic.
.SS Fixed packet lengths for a given test
.PP
Packet lengths are fixed for the duration of the test.
While this may not be an accurate simulation of some types of traffic,
it means that IPDV measurements are accurate, where they wouldn't be in
any other case.
.SS Stateful protocol
.PP
There are numerous benefits to stateless protocols, particularly for
developers and data centers, including simplified server design,
horizontal scalabity, and easily implemented zero\-downtime restarts.
However, in this case, a stateful protocol provides important benefits
to the user, including:
.IP \[bu] 2
Smaller packet sizes (a design goal) as context does not need to be
included in every request
.IP \[bu] 2
More accurate measurement of upstream vs downstream packet loss (this
gets worse in a stateless protocol as RTT approaches the test duration,
complicating interplanetary tests!)
.IP \[bu] 2
More accurate rate and test duration limiting on the server
.SS In\-memory results storage
.PP
Results for each round\-trip are stored in memory as the test is being
run.
Each result takes 72 bytes in memory (8 64\-bit timestamps and a 64\-bit
server received packet window), so this limits the effective duration of
the test, especially at very small send intervals.
However, the advantages are:
.IP \[bu] 2
It's easier to perform statistical analysis (like calculation of the
median) on fixed arrays than on running data values
.IP \[bu] 2
We don't need to either send client timestamps to the server, or
maintain a local running window of sent packet info, because they're all
in memory, no matter when server replies come back
.IP \[bu] 2
Not accessing the disk during the test to write test output prevents
inadvertently affecting the results
.IP \[bu] 2
It simplifies the API
.PP
As a consequence of storing results in memory, packet sequence numbers
are fixed at 32\-bits.
If all 2^32 sequence numbers were used, the results would require over
300 Gb of virtual memory to record while the test is running.
That is why 64\-bit sequence numbers are currently unnecessary.
.SS 64\-bit received window
.PP
In order to determine per\-packet differentiation between upstream and
downstream loss, a 64\-bit \[lq]received window\[rq] may be returned
with each packet that contains the receipt status of the previous 64
packets.
This can be enabled using \f[C]\-\-stats=window/both\f[] with the irtt
client.
Its limited width and simple bitmap format lead to some caveats:
.IP \[bu] 2
Per\-packet differentiation is not available (for any intervening
packets) if greater than 64 packets are lost in succession.
These packets will be marked with the generic \f[C]Lost\f[].
.IP \[bu] 2
While any packet marked \f[C]LostDown\f[] is guaranteed to be marked
properly, there is no confirmation of receipt of the receive window from
the client to the server, so packets may sometimes be erroneously marked
\f[C]LostUp\f[], for example, if they arrive late to the server and
slide out of the received window before they can be confirmed to the
client, or if the received window is lost on its way to the client and
not amended by a later packet's received window.
.PP
There are many ways that this simple approach could be improved, such as
by:
.IP \[bu] 2
Allowing a wider window
.IP \[bu] 2
Encoding receipt seqnos in a more intelligent way to allow a wider seqno
range
.IP \[bu] 2
Sending confirmation of window receipt from the client to the server and
re\-sending unreceived windows
.PP
However, the current strategy means that a good approximation of
per\-packet loss results can be obtained with only 8 additional bytes in
each packet.
It also requires very little computational time on the server, and
almost all computation on the client occurs during results generation,
after the test is complete.
It isn't as accurate with late (out\-of\-order) upstream packets or with
long sequences of lost packets, but high loss or high numbers of late
packets typically indicate more severe network conditions that should be
corrected first anyway, perhaps before per\-packet results matter.
Note that in case of very high packet loss, the \f[B]total\f[] number of
packets received by the server but not returned to the client (which can
be obtained using \f[C]\-\-stats=count\f[]) will still be correct, which
will still provide an accurate \f[B]average\f[] loss percentage in each
direction over the course of the test.
.SS Use of Go
.PP
IRTT is written in Go.
That carries with it:
.IP \[bu] 2
Non\-negligible system call overhead
.IP \[bu] 2
A larger executable size than with C
.IP \[bu] 2
Somewhat slower execution speed than C (although not that much
slower (https://benchmarksgame.alioth.debian.org/u64q/compare.php?lang=go&lang2=gcc))
.PP
However, Go also has characteristics that make it a good fit for this
application:
.IP \[bu] 2
Go's target is network and server applications, with a focus on
simplicity, reliability and efficiency, which is appropriate for IRTT
.IP \[bu] 2
Memory footprint tends to be significantly lower than with some
interpreted languages
.IP \[bu] 2
It's easy to support a broad array of hardware and OS combinations
.SH SEE ALSO
.PP
irtt\-client(1) (irtt-client.html), irtt\-server(1) (irtt-server.html)
.PP
IRTT GitHub repository (https://github.com/peteheist/irtt/)
.SH AUTHOR
.PP
Pete Heist
.PP
Many thanks to both Toke Høiland\-Jørgensen and Dave Täht from the
Bufferbloat project (https://www.bufferbloat.net/) for their valuable
advice.
Any problems in design or implementation are entirely my own.
.SH HISTORY
.PP
IRTT was originally written to improve the latency and packet loss
measurements for the excellent Flent (https://flent.org) tool.
Flent was developed by and for the
Bufferbloat (https://www.bufferbloat.net/projects/) project, which aims
to reduce \[lq]chaotic and laggy network performance,\[rq] making this
project valuable to anyone who values their time and sanity while using
the Internet.
irtt/doc/irtt.md 0000644 0001751 0001751 00000022250 13240047124 012565 0 ustar pete pete % IRTT(1) v0.9.0 | IRTT Manual
%
% February 11, 2018
# NAME
irtt - Isochronous Round-Trip Time
# SYNOPSIS
irtt *command* [*args*]
irtt help *command*
# DESCRIPTION
IRTT measures round-trip time and other latency related metrics using UDP
packets sent on a fixed period, and produces both text and JSON output.
# COMMANDS
*client*
: runs the client
*server*
: runs the server
*bench*
: runs HMAC and fill benchmarks
*clock*
: runs wall vs monotonic clock test
*sleep*
: runs sleep accuracy test
*version*
: shows the version
# EXAMPLES
After installing IRTT, start a server:
```
$ irtt server
IRTT server starting...
[ListenerStart] starting IPv6 listener on [::]:2112
[ListenerStart] starting IPv4 listener on 0.0.0.0:2112
```
While that's running, run a client. If no options are supplied, it will send
a request once per second, like ping. Here we simulate a one minute
G.711 VoIP conversation by using an interval of 20ms and randomly filled
payloads of 172 bytes:
```
$ irtt client -i 20ms -l 172 -d 1m --fill=rand --sfill=rand -q 192.168.100.10
[Connecting] connecting to 192.168.100.10
[Connected] connected to 192.168.100.10:2112
Min Mean Median Max Stddev
--- ---- ------ --- ------
RTT 11.93ms 20.88ms 19.2ms 80.49ms 7.02ms
send delay 4.99ms 12.21ms 10.83ms 50.45ms 5.73ms
receive delay 6.38ms 8.66ms 7.86ms 69.11ms 2.89ms
IPDV (jitter) 782ns 4.53ms 3.39ms 64.66ms 4.2ms
send IPDV 256ns 3.99ms 2.98ms 35.28ms 3.69ms
receive IPDV 896ns 1.78ms 966µs 62.28ms 2.86ms
send call time 56.5µs 82.8µs 18.99ms 348µs
timer error 0s 21.7µs 19.05ms 356µs
server proc. time 23.9µs 26.9µs 141µs 11.2µs
duration: 1m0s (wait 241.5ms)
packets sent/received: 2996/2979 (0.57% loss)
server packets received: 2980/2996 (0.53%/0.03% loss up/down)
bytes sent/received: 515312/512388
send/receive rate: 68.7 Kbps / 68.4 Kbps
packet length: 172 bytes
timer stats: 4/3000 (0.13%) missed, 0.11% error
```
In the results above, the client and server are located at two different sites,
around 50km from one another, each of which connects to the Internet via
point-to-point WiFi. The client is 3km NLOS through trees located near its
transmitter, which is likely the reason for the higher upstream packet loss, mean
send delay and IPDV.
# BUGS
- Windows is unable to set DSCP values for IPv6.
- Windows is unable to set the source IP address, so `--set-src-ip` may not be used
on the server.
- The server doesn't run well on 32-bit Windows platforms. When connecting with
a client, you may see `Terminated due to receive error`. To work around
this, disable dual timestamps from the client by including `--tstamp=midpoint`.
# LIMITATIONS
> "It is the limitations of software that give it life."
>
> -Me, justifying my limitations
## Isochronous (fixed period) send schedule
Currently, IRTT only sends packets on a fixed period, foregoing the ability to
simulate arbitrary traffic. Accepting this limitation offers some benefits:
- It's easy to implement
- It's easy to calculate how many packets and how much data will be sent in a given time
- It simplifies timer error compensation
Also, isochronous packets are commonly seen in VoIP, games and some streaming media,
so it already simulates an array of common types of traffic.
## Fixed packet lengths for a given test
Packet lengths are fixed for the duration of the test. While this may not be an
accurate simulation of some types of traffic, it means that IPDV measurements
are accurate, where they wouldn't be in any other case.
## Stateful protocol
There are numerous benefits to stateless protocols, particularly for developers
and data centers, including simplified server design, horizontal scalabity, and
easily implemented zero-downtime restarts. However, in this case, a stateful
protocol provides important benefits to the user, including:
- Smaller packet sizes (a design goal) as context does not need to be included
in every request
- More accurate measurement of upstream vs downstream packet loss (this gets
worse in a stateless protocol as RTT approaches the test duration,
complicating interplanetary tests!)
- More accurate rate and test duration limiting on the server
## In-memory results storage
Results for each round-trip are stored in memory as the test is being run. Each
result takes 72 bytes in memory (8 64-bit timestamps and a 64-bit server
received packet window), so this limits the effective duration of the test,
especially at very small send intervals. However, the advantages are:
- It's easier to perform statistical analysis (like calculation of the median)
on fixed arrays than on running data values
- We don't need to either send client timestamps to the server, or maintain a
local running window of sent packet info, because they're all in memory, no
matter when server replies come back
- Not accessing the disk during the test to write test output prevents
inadvertently affecting the results
- It simplifies the API
As a consequence of storing results in memory, packet sequence numbers are fixed
at 32-bits. If all 2^32 sequence numbers were used, the results would require
over 300 Gb of virtual memory to record while the test is running. That is
why 64-bit sequence numbers are currently unnecessary.
## 64-bit received window
In order to determine per-packet differentiation between upstream and downstream
loss, a 64-bit "received window" may be returned with each packet that contains
the receipt status of the previous 64 packets. This can be enabled using
`--stats=window/both` with the irtt client. Its limited width and simple bitmap
format lead to some caveats:
- Per-packet differentiation is not available (for any intervening packets) if
greater than 64 packets are lost in succession. These packets will be marked
with the generic `Lost`.
- While any packet marked `LostDown` is guaranteed to be marked properly, there
is no confirmation of receipt of the receive window from the client to the
server, so packets may sometimes be erroneously marked `LostUp`, for example,
if they arrive late to the server and slide out of the received window before
they can be confirmed to the client, or if the received window is lost on its
way to the client and not amended by a later packet's received window.
There are many ways that this simple approach could be improved, such as by:
- Allowing a wider window
- Encoding receipt seqnos in a more intelligent way to allow a wider seqno range
- Sending confirmation of window receipt from the client to the server and
re-sending unreceived windows
However, the current strategy means that a good approximation of per-packet loss
results can be obtained with only 8 additional bytes in each packet. It also
requires very little computational time on the server, and almost all
computation on the client occurs during results generation, after the test is
complete. It isn't as accurate with late (out-of-order) upstream packets or with
long sequences of lost packets, but high loss or high numbers of late packets
typically indicate more severe network conditions that should be corrected first
anyway, perhaps before per-packet results matter. Note that in case of very high
packet loss, the **total** number of packets received by the server but not
returned to the client (which can be obtained using `--stats=count`) will still
be correct, which will still provide an accurate **average** loss percentage in
each direction over the course of the test.
## Use of Go
IRTT is written in Go. That carries with it:
- Non-negligible system call overhead
- A larger executable size than with C
- Somewhat slower execution speed than C (although [not that much slower](https://benchmarksgame.alioth.debian.org/u64q/compare.php?lang=go&lang2=gcc))
However, Go also has characteristics that make it a good fit for this
application:
- Go's target is network and server applications, with a focus on simplicity,
reliability and efficiency, which is appropriate for IRTT
- Memory footprint tends to be significantly lower than with some interpreted
languages
- It's easy to support a broad array of hardware and OS combinations
# SEE ALSO
[irtt-client(1)](irtt-client.html), [irtt-server(1)](irtt-server.html)
[IRTT GitHub repository](https://github.com/peteheist/irtt/)
# AUTHOR
Pete Heist
Many thanks to both Toke Høiland-Jørgensen and Dave Täht from the
[Bufferbloat project](https://www.bufferbloat.net/) for their valuable advice.
Any problems in design or implementation are entirely my own.
# HISTORY
IRTT was originally written to improve the latency and packet loss measurements
for the excellent [Flent](https://flent.org) tool. Flent was developed by and
for the [Bufferbloat](https://www.bufferbloat.net/projects/) project, which aims
to reduce "chaotic and laggy network performance," making this project valuable
to anyone who values their time and sanity while using the Internet.
irtt/conn.go 0000644 0001751 0001751 00000032454 13240047124 012007 0 ustar pete pete package irtt
import (
"bytes"
"context"
"net"
"sort"
"strings"
"time"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
)
// nconn (network conn) is the embedded struct in conn and lconn connections. It
// adds IPVersion, socket options and some helpers to net.UDPConn.
type nconn struct {
conn *net.UDPConn
ipVer IPVersion
ip4conn *ipv4.PacketConn
ip6conn *ipv6.PacketConn
dscp int
dscpError error
dscpSupport bool
ttl int
df DF
}
func (n *nconn) init(conn *net.UDPConn, ipVer IPVersion) {
n.conn = conn
n.ipVer = ipVer
n.df = DFDefault
// create x/net conns for socket options
if n.ipVer&IPv4 != 0 {
n.ip4conn = ipv4.NewPacketConn(n.conn)
n.dscpError = n.ip4conn.SetTOS(1)
n.ip4conn.SetTOS(0)
} else {
n.ip6conn = ipv6.NewPacketConn(n.conn)
n.dscpError = n.ip6conn.SetTrafficClass(1)
n.ip6conn.SetTrafficClass(0)
}
n.dscpSupport = (n.dscpError == nil)
}
func (n *nconn) setDSCP(dscp int) (err error) {
if n.dscp == dscp {
return
}
if n.ip4conn != nil {
err = n.ip4conn.SetTOS(dscp)
} else {
err = n.ip6conn.SetTrafficClass(dscp)
}
if err == nil {
n.dscp = dscp
}
return
}
func (n *nconn) setTTL(ttl int) (err error) {
if n.ttl == ttl {
return
}
if n.ip4conn != nil {
err = n.ip4conn.SetTTL(ttl)
} else {
err = n.ip6conn.SetHopLimit(ttl)
}
if err == nil {
n.ttl = ttl
}
return
}
func (n *nconn) setReceiveDstAddr(b bool) (err error) {
if n.ip4conn != nil {
err = n.ip4conn.SetControlMessage(ipv4.FlagDst, b)
} else {
err = n.ip6conn.SetControlMessage(ipv6.FlagDst, b)
}
return
}
func (n *nconn) setDF(df DF) (err error) {
if n.df == df {
return
}
err = setSockoptDF(n.conn, df)
if err == nil {
n.df = df
}
return
}
func (n *nconn) localAddr() *net.UDPAddr {
if n.conn == nil {
return nil
}
a := n.conn.LocalAddr()
if a == nil {
return nil
}
return a.(*net.UDPAddr)
}
func (n *nconn) close() error {
return n.conn.Close()
}
// cconn is used for client connections
type cconn struct {
*nconn
cfg *ClientConfig
ctoken ctoken
}
func dial(ctx context.Context, cfg *ClientConfig) (cc *cconn, err error) {
// resolve (could support trying multiple addresses in succession)
cfg.LocalAddress = addPort(cfg.LocalAddress, DefaultLocalPort)
laddr, err := net.ResolveUDPAddr(cfg.IPVersion.udpNetwork(),
cfg.LocalAddress)
if err != nil {
return
}
// add default port, if necessary, and resolve server
cfg.RemoteAddress = addPort(cfg.RemoteAddress, DefaultPort)
raddr, err := net.ResolveUDPAddr(cfg.IPVersion.udpNetwork(),
cfg.RemoteAddress)
if err != nil {
return
}
// dial, using explicit network from remote address
cfg.IPVersion = IPVersionFromUDPAddr(raddr)
conn, err := net.DialUDP(cfg.IPVersion.udpNetwork(), laddr, raddr)
if err != nil {
return
}
// set resolved local and remote addresses back to Config
cfg.LocalAddr = conn.LocalAddr()
cfg.RemoteAddr = conn.RemoteAddr()
cfg.LocalAddress = cfg.LocalAddr.String()
cfg.RemoteAddress = cfg.RemoteAddr.String()
// create cconn
cc = &cconn{nconn: &nconn{}, cfg: cfg}
cc.init(conn, cfg.IPVersion)
// open connection to server
err = cc.open(ctx)
if isErrorCode(ServerClosed, err) {
cc = nil
err = nil
return
}
return
}
func (c *cconn) open(ctx context.Context) (err error) {
// validate open timeouts
for _, to := range c.cfg.OpenTimeouts {
if to < minOpenTimeout {
err = Errorf(OpenTimeoutTooShort,
"open timeout %s must be >= %s", to, minOpenTimeout)
return
}
}
errC := make(chan error)
params := &c.cfg.Params
// start receiving open replies and drop anything else
go func() {
var rerr error
defer func() {
errC <- rerr
}()
orp := newPacket(0, maxHeaderLen, c.cfg.HMACKey)
for {
if rerr = c.receive(orp); rerr != nil && !isErrorCode(ServerClosed, rerr) {
return
}
if orp.flags()&flOpen == 0 {
continue
}
if rerr = orp.addFields(fopenReply, false); rerr != nil {
return
}
if orp.flags()&flClose == 0 && orp.ctoken() == 0 {
rerr = Errorf(ConnTokenZero, "received invalid zero conn token")
return
}
var sp *Params
sp, rerr = parseParams(orp.payload())
if rerr != nil {
return
}
*params = *sp
c.ctoken = orp.ctoken()
if orp.flags()&flClose != 0 {
c.close()
}
return
}
}()
// start sending open requests
sp := newPacket(0, maxHeaderLen, c.cfg.HMACKey)
defer func() {
if err != nil {
c.close()
}
}()
sp.setFlagBits(flOpen)
if c.cfg.NoTest {
sp.setFlagBits(flClose)
}
sp.setPayload(params.bytes())
sp.updateHMAC()
var received bool
for _, to := range c.cfg.OpenTimeouts {
err = c.send(sp)
if err != nil {
return
}
select {
case <-time.After(to):
case err = <-errC:
received = true
return
case <-ctx.Done():
err = ctx.Err()
return
}
}
if !received {
defer c.nconn.close()
err = Errorf(OpenTimeout, "no reply from server")
}
return
}
func (c *cconn) send(p *packet) (err error) {
if err = c.setDSCP(p.dscp); err != nil {
return
}
var n int
n, err = c.conn.Write(p.bytes())
p.tsent = time.Now()
p.trcvd = time.Time{}
if err != nil {
return
}
if n < p.length() {
err = Errorf(ShortWrite, "only %d/%d bytes were sent", n, p.length())
}
return
}
func (c *cconn) receive(p *packet) (err error) {
var n int
n, err = c.conn.Read(p.readTo())
p.trcvd = time.Now()
p.tsent = time.Time{}
p.dscp = 0
if err != nil {
return
}
if err = p.readReset(n); err != nil {
return
}
if !p.reply() {
err = Errorf(ExpectedReplyFlag, "reply flag not set")
return
}
if p.flags()&flClose != 0 {
err = Errorf(ServerClosed, "server closed connection")
c.close()
}
return
}
func (c *cconn) newPacket() *packet {
p := newPacket(0, c.cfg.Length, c.cfg.HMACKey)
p.setConnToken(c.ctoken)
p.raddr = c.conn.RemoteAddr().(*net.UDPAddr)
return p
}
func (c *cconn) remoteAddr() *net.UDPAddr {
if c.conn == nil {
return nil
}
a := c.conn.RemoteAddr()
if a == nil {
return nil
}
return a.(*net.UDPAddr)
}
func (c *cconn) close() (err error) {
defer func() {
err = c.nconn.close()
}()
// send one close packet if necessary
if c.ctoken != 0 {
cp := newPacket(0, maxHeaderLen, c.cfg.HMACKey)
if err = cp.setFields(fcloseRequest, true); err != nil {
return
}
cp.setFlagBits(flClose)
cp.setConnToken(c.ctoken)
cp.updateHMAC()
err = c.send(cp)
}
return
}
// lconn is used for server listeners
type lconn struct {
*nconn
cm4 ipv4.ControlMessage
cm6 ipv6.ControlMessage
setSrcIP bool
}
// listen creates an lconn by listening on a UDP address.
func listen(laddr *net.UDPAddr, setSrcIP bool) (l *lconn, err error) {
ipVer := IPVersionFromUDPAddr(laddr)
var conn *net.UDPConn
conn, err = net.ListenUDP(ipVer.udpNetwork(), laddr)
if err != nil {
return
}
l = &lconn{nconn: &nconn{}, setSrcIP: setSrcIP && laddr.IP.IsUnspecified()}
l.init(conn, ipVer)
return
}
// listenAll creates lconns on multiple addresses, with separate lconns for IPv4
// and IPv6, so that socket options can be set correctly, which is not possible
// with a dual stack conn.
func listenAll(ipVer IPVersion, addrs []string, setSrcIP bool) (lconns []*lconn, err error) {
laddrs, err := resolveListenAddrs(addrs, ipVer)
if err != nil {
return
}
lconns = make([]*lconn, 0, 16)
for _, laddr := range laddrs {
var l *lconn
l, err = listen(laddr, setSrcIP)
if err != nil {
return
}
lconns = append(lconns, l)
}
if len(lconns) == 0 {
err = Errorf(NoSuitableAddressFound, "no suitable %s address found", ipVer)
return
}
return
}
func (l *lconn) send(p *packet) (err error) {
p.updateHMAC()
if err = l.setDSCP(p.dscp); err != nil {
return
}
var n int
if !l.setSrcIP {
n, err = l.conn.WriteToUDP(p.bytes(), p.raddr)
} else if l.ip4conn != nil {
l.cm4.Src = p.srcIP
n, err = l.ip4conn.WriteTo(p.bytes(), &l.cm4, p.raddr)
} else {
l.cm6.Src = p.srcIP
n, err = l.ip6conn.WriteTo(p.bytes(), &l.cm6, p.raddr)
}
p.tsent = time.Now()
p.trcvd = time.Time{}
if err != nil {
return
}
if n < p.length() {
err = Errorf(ShortWrite, "only %d/%d bytes were sent", n, p.length())
}
return
}
func (l *lconn) receive(p *packet) (err error) {
var n int
if !l.setSrcIP {
n, p.raddr, err = l.conn.ReadFromUDP(p.readTo())
p.dstIP = nil
} else if l.ip4conn != nil {
var cm *ipv4.ControlMessage
var src net.Addr
n, cm, src, err = l.ip4conn.ReadFrom(p.readTo())
if src != nil {
p.raddr = src.(*net.UDPAddr)
}
if cm != nil {
p.dstIP = cm.Dst
} else {
p.dstIP = nil
}
} else {
var cm *ipv6.ControlMessage
var src net.Addr
n, cm, src, err = l.ip6conn.ReadFrom(p.readTo())
if src != nil {
p.raddr = src.(*net.UDPAddr)
}
if cm != nil {
p.dstIP = cm.Dst
} else {
p.dstIP = nil
}
}
p.srcIP = nil
p.dscp = 0
p.trcvd = time.Now()
p.tsent = time.Time{}
if err != nil {
return
}
if err = p.readReset(n); err != nil {
return
}
if p.reply() {
err = Errorf(UnexpectedReplyFlag, "unexpected reply flag set")
return
}
return
}
// parseIfaceListenAddr parses an interface listen address into an interface
// name and service. ok is false if the string does not use the syntax
// %iface:service, where :service is optional.
func parseIfaceListenAddr(addr string) (iface, service string, ok bool) {
if !strings.HasPrefix(addr, "%") {
return
}
parts := strings.Split(addr[1:], ":")
switch len(parts) {
case 2:
service = parts[1]
if len(service) == 0 {
return
}
fallthrough
case 1:
iface = parts[0]
if len(iface) == 0 {
return
}
ok = true
return
}
return
}
// resolveIfaceListenAddr resolves an interface name and service (port name
// or number) into a slice of UDP addresses.
func resolveIfaceListenAddr(ifaceName string, service string,
ipVer IPVersion) (laddrs []*net.UDPAddr, err error) {
// get interfaces
var ifaces []net.Interface
ifaces, err = net.Interfaces()
if err != nil {
return
}
// resolve service to port
var port int
if service != "" {
port, err = net.LookupPort(ipVer.udpNetwork(), service)
if err != nil {
return
}
} else {
port = DefaultPortInt
}
// helper to get IP and zone from interface address
ifaceIP := func(a net.Addr) (ip net.IP, zone string, ok bool) {
switch v := a.(type) {
case *net.IPNet:
{
ip = v.IP
ok = true
}
case *net.IPAddr:
{
ip = v.IP
zone = v.Zone
ok = true
}
}
return
}
// helper to test if IP is one we can listen on
isUsableIP := func(ip net.IP) bool {
if IPVersionFromIP(ip)&ipVer == 0 {
return false
}
if !ip.IsLinkLocalUnicast() && !ip.IsGlobalUnicast() && !ip.IsLoopback() {
return false
}
return true
}
// get addresses
laddrs = make([]*net.UDPAddr, 0, 16)
ifaceFound := false
ifaceUp := false
for _, iface := range ifaces {
if !glob(ifaceName, iface.Name) {
continue
}
ifaceFound = true
if iface.Flags&net.FlagUp == 0 {
continue
}
ifaceUp = true
ifaceAddrs, err := iface.Addrs()
if err != nil {
return nil, err
}
for _, a := range ifaceAddrs {
ip, zone, ok := ifaceIP(a)
if ok && isUsableIP(ip) {
if ip.IsLinkLocalUnicast() && zone == "" {
zone = iface.Name
}
udpAddr := &net.UDPAddr{IP: ip, Port: port, Zone: zone}
laddrs = append(laddrs, udpAddr)
}
}
}
if !ifaceFound {
err = Errorf(NoMatchingInterfaces, "%s does not match any interfaces", ifaceName)
} else if !ifaceUp {
err = Errorf(NoMatchingInterfacesUp, "no interfaces matching %s are up", ifaceName)
}
return
}
// resolveListenAddr resolves a listen address string into a slice of UDP
// addresses.
func resolveListenAddr(addr string, ipVer IPVersion) (laddrs []*net.UDPAddr,
err error) {
laddrs = make([]*net.UDPAddr, 0, 2)
for _, v := range ipVer.Separate() {
addr = addPort(addr, DefaultPort)
laddr, err := net.ResolveUDPAddr(v.udpNetwork(), addr)
if err != nil {
continue
}
if laddr.IP == nil {
laddr.IP = v.ZeroIP()
}
laddrs = append(laddrs, laddr)
}
return
}
// resolveListenAddrs resolves a slice of listen address strings into a slice
// of UDP addresses.
func resolveListenAddrs(addrs []string, ipVer IPVersion) (laddrs []*net.UDPAddr,
err error) {
// resolve addresses
laddrs = make([]*net.UDPAddr, 0, 16)
for _, addr := range addrs {
var la []*net.UDPAddr
iface, service, ok := parseIfaceListenAddr(addr)
if ok {
la, err = resolveIfaceListenAddr(iface, service, ipVer)
} else {
la, err = resolveListenAddr(addr, ipVer)
}
if err != nil {
return
}
laddrs = append(laddrs, la...)
}
// sort addresses
sort.Slice(laddrs, func(i, j int) bool {
if bytes.Compare(laddrs[i].IP, laddrs[j].IP) < 0 {
return true
}
if laddrs[i].Port < laddrs[j].Port {
return true
}
return laddrs[i].Zone < laddrs[j].Zone
})
// remove duplicates
udpAddrsEqual := func(a *net.UDPAddr, b *net.UDPAddr) bool {
if !a.IP.Equal(b.IP) {
return false
}
if a.Port != b.Port {
return false
}
return a.Zone == b.Zone
}
for i := 1; i < len(laddrs); i++ {
if udpAddrsEqual(laddrs[i], laddrs[i-1]) {
laddrs = append(laddrs[:i], laddrs[i+1:]...)
i--
}
}
// check for combination of specified and unspecified IP addresses
m := make(map[int]int)
for _, la := range laddrs {
if la.IP.IsUnspecified() {
m[la.Port] = m[la.Port] | 1
} else {
m[la.Port] = m[la.Port] | 2
}
}
for k, v := range m {
if v > 2 {
err = Errorf(UnspecifiedWithSpecifiedAddresses,
"invalid combination of unspecified and specified IP addresses port %d", k)
break
}
}
return
}
irtt/README.md 0000644 0001751 0001751 00000047111 13240047124 011776 0 ustar pete pete # IRTT (Isochronous Round-Trip Tester)
IRTT measures round-trip time, one-way delay and other metrics using UDP
packets sent on a fixed period, and produces both user and machine parseable
output.
IRTT has reached version 0.9.0, and is usable today, but needs more work until
version 1.0.0 can be released. I would appreciate any feedback, which you can
send under Issues. However, it could be useful to first review the
[Roadmap](#roadmap) section of the documentation before submitting a new bug or
feature request.
## Table of Contents
1. [Motivation](#motivation)
2. [Goals](#goals)
3. [Features](#features)
4. [Limitations](#limitations)
5. [Installation](#installation)
6. [Documentation](#documentation)
7. [Frequently Asked Questions](#frequently-asked-questions)
8. [Roadmap](#roadmap)
9. [Changes](#changes)
10. [Thanks](#thanks)
## Motivation
Latency is an under-appreciated metric in network and application performance.
As of this writing, many broadband connections are well past the point of
diminishing returns when it comes to throughput, yet that’s what we continue to
take as the primary measure of Internet performance. This is analogous to
ordinary car buyers making top speed their first priority.
There is a certain hard to quantify but visceral “latency stress” that comes
from waiting in expectation after a web page click, straining through a delayed
and garbled VoIP conversation, or losing at your favorite online game (unless
you like “lag” as an excuse). Those who work on reducing latency and improving
network performance characteristics beyond just throughput may be driven by the
idea of helping relieve this stress for others.
IRTT was originally written to improve the latency and packet loss measurements
for the excellent [Flent](https://flent.org) tool, but should be useful as a
standalone tool as well. Flent was developed by and for the
[Bufferbloat](https://www.bufferbloat.net/projects/) project, which aims to
reduce "chaotic and laggy network performance," making this project valuable to
anyone who values their time and sanity while using the Internet.
## Goals
The goals of this project are to:
- Accurately measure latency and other relevant metrics of network behavior
- Produce statistics via both human and machine parseable output
- Provide for reasonably secure use on both public and private servers
- Support small enough packet sizes for [VoIP](https://www.cisco.com/c/en/us/support/docs/voice/voice-quality/7934-bwidth-consume.html) simulation
- Support relevant socket options, including DSCP
- Use a single UDP port for deployment simplicity
- Provide an API for embedding and extensibility
## Features:
- Measurement of:
- [RTT (round-trip time)](https://en.wikipedia.org/wiki/Round-trip_delay_time)
- [OWD (one-way delay)](https://en.wikipedia.org/wiki/End-to-end_delay), given
external clock synchronization
- [IPDV (instantaneous packet delay variation)](https://en.wikipedia.org/wiki/Packet_delay_variation), usually referred to as jitter
- [Packet loss](https://en.wikipedia.org/wiki/Packet_loss), with upstream and downstream differentiation
- [Out-of-order](https://en.wikipedia.org/wiki/Out-of-order_delivery)
(measured using late packets metric) and [duplicate](https://wiki.wireshark.org/DuplicatePackets) packets
- [Bitrate](https://en.wikipedia.org/wiki/Bit_rate)
- Timer error, send call time and server processing time
- Statistics: min, max, mean, median (for most quantities) and standard deviation
- Nanosecond time precision (where available), and robustness in the face of
clock drift and NTP corrections through the use of both the wall and monotonic
clocks
- Binary protocol with negotiated format for test packet lengths down to 16 bytes (without timestamps)
- HMAC support for private servers, preventing unauthorized discovery and use
- Support for a wide range of Go supported [platforms](https://github.com/golang/go/wiki/MinimumRequirements)
- Timer compensation to improve sleep send schedule accuracy
- Support for IPv4 and IPv6
- Public server protections, including:
- Three-way handshake with returned 64-bit connection token, preventing reply
redirection to spoofed source addresses
- Limits on maximum test duration, minimum interval and maximum packet length,
both advertised in the negotiation and enforced with hard limits to protect
against rogue clients
- Packet payload filling to prevent relaying of arbitrary traffic
- Output to JSON
## Limitations
See the
[LIMITATIONS](http://htmlpreview.github.io/?https://github.com/peteheist/irtt/blob/master/doc/irtt.html#limitations)
section of the irtt(1) man page.
## Installation
To install IRTT manually or build from source, you must:
1. [Install Go](https://golang.org/dl/)
2. Install irtt: `go get -u github.com/peteheist/irtt/cmd/irtt`
3. For convenience, copy the `irtt` executable, which should be in
`$HOME/go/bin`, or `$GOPATH/bin` if you have `$GOPATH` defined, to somewhere
on your `PATH`.
If you want to build the source for development, you must also:
1. Install the `pandoc` utility for generating man pages and HTML documentation
from their markdown source files. This can be done with `apt-get install
pandoc` on Debian flavors of Linux or `brew install pandoc` on OS/X. See the
[Pandoc](http://pandoc.org/) site for more information.
2. Install the `stringer` utility by doing
`go get -u golang.org/x/tools/cmd/stringer`.
This is only necessary if you need to re-generate the `*_string.go` files that
are generated by this tool, otherwise the checked in versions may also be
used.
3. Use `build.sh` to build during development, which helps with development
related tasks, such as generating source files and docs, and cross-compiling
for testing. For example, `build.sh min linux-amd64` would compile a
minimized binary for Linux on AMD64. See `build.sh` for more info and a
"source-documented" list of platforms that the script supports. See [this
page](http://golang.org/doc/install/source#environment) for a full list of
valid GOOS GOARCH combinations. `build.sh install` runs Go's install command,
which puts the resulting executable in `$GOPATH/bin`.
If you want to build from a branch, you should first follow the steps above,
then from the `github.com/peteheist/irtt` directory, do:
1. `git checkout branch`
2. `go get ./...`
3. `go install ./cmd/irtt` or `./build.sh` and move resulting `irtt` executable
to install location
## Documentation
After installing IRTT, see the man pages and their corresponding EXAMPLES
sections to get started quickly:
- [irtt(1)](http://htmlpreview.github.io/?https://github.com/peteheist/irtt/blob/master/doc/irtt.html) | [EXAMPLES](http://htmlpreview.github.io/?https://github.com/peteheist/irtt/blob/master/doc/irtt.html#examples)
- [irtt-client(1)](http://htmlpreview.github.io/?https://github.com/peteheist/irtt/blob/master/doc/irtt-client.html) | [EXAMPLES](http://htmlpreview.github.io/?https://github.com/peteheist/irtt/blob/master/doc/irtt-client.html#examples)
- [irtt-server(1)](http://htmlpreview.github.io/?https://github.com/peteheist/irtt/blob/master/doc/irtt-server.html) | [EXAMPLES](http://htmlpreview.github.io/?https://github.com/peteheist/irtt/blob/master/doc/irtt-server.html#examples)
## Frequently Asked Questions
1) Why not just use ping?
Ping may be the preferred tool when measuring minimum latency, or for other
reasons. IRTT's reported mean RTT is likely to be a bit higher (on the order
of hundreds of microseconds) and a bit more variable than the results
reported by ping, due to the overhead of entering userspace, together with
Go's system call overhead and scheduling variability. That said, this
overhead should be negligible at most Internet RTTs, and there are advantages
that IRTT has over ping when minimum RTT is not what you're measuring:
- In addition to round-trip time, IRTT also measures OWD, IPDV and upstream
vs downstream packet loss.
- Some device vendors prioritize ICMP, so ping may not be an accurate measure
of user-perceived latency.
- IRTT can use HMACs to protect private servers from unauthorized discovery
and use.
- IRTT has a three-way handshake to prevent test traffic redirection from
spoofed source IPs.
- IRTT can fill the payload (if included) with random or arbitrary data.
2) Why does `irtt client` use `-l` for packet length instead of following ping
and using `-s` for size?
I felt it more appropriate to follow the
[RFC 768](https://tools.ietf.org/html/rfc768) term _length_ for UDP packets,
since IRTT uses UDP.
3) Why is the send (or receive) delay negative or much larger than I expect?
The client and server clocks must be synchronized for one-way delay values to
be meaningful (although, the relative change of send and receive delay may be
useful to look at even without clock synchronization). Well-configured NTP
hosts may be able to synchronize to within a few milliseconds.
[PTP](https://en.wikipedia.org/wiki/Precision_Time_Protocol)
([Linux](http://linuxptp.sourceforge.net) implementation here) is capable of
much higher precision. For example, using two
[PCEngines APU2](http://pcengines.ch/apu2.htm) boards (which support PTP
hardware timestamps) connected directly by Ethernet, the clocks
may be synchronized within a few microseconds.
Note that client and server synchronization is not needed for either RTT or
IPDV (even send and receive IPDV) values to be correct. RTT is measured with
client times only, and since IPDV is measuring differences between successive
packets, it's not affected by time synchronization.
4) Why is the receive rate 0 when a single packet is sent?
Receive rate is measured from the time the first packet is received to the time
the last packet is received. For a single packet, those times are the same.
5) Why does a test with a one second duration and 200ms interval run for around
800ms and not one second?
The test duration is exclusive, meaning requests will not be sent exactly at
or after the test duration has elapsed. In this case, the interval is 200ms, and
the fifth and final request is sent at around 800ms from the start of the test.
The test ends when all replies have been received from the server, so it may
end shortly after 800ms. If there are any outstanding packets, the wait time
is observed, which by default is a multiple of the maximum RTT.
6) Why is IPDV not reported when only one packet is received?
[IPDV](https://en.wikipedia.org/wiki/Packet_delay_variation) is the
difference in delay between successfully returned replies, so at least two
reply packets are required to make this calculation.
7) Why does wait fall back to fixed duration when duration is less than RTT?
If a full RTT has not elapsed, there is no way to know how long an
appropriate wait time would be, so the wait falls back to a default fixed
time (default is 4 seconds, same as ping).
8) Why can't the client connect to the server, and instead I get
`Error: no reply from server`?
There are a number of possible reasons for this:
1) You've specified an incorrect hostname or IP address for the server.
2) There is a firewall blocking packets from the client to the server.
Traffic must be allowed on the chosen UDP port (default 2112).
3) There is high packet loss. By default, up to four packets are sent when
the client tries to connect to the server, using timeouts of 1, 2, 4 and 8
seconds. If all of these are lost, the client won't connect to the server.
In environments with known high packet loss, the `--timeouts` flag may
be used to send more packets with the chosen timeouts before abandoning
the connection.
4) The server has an HMAC key set with `--hmac` and the client either has
not specified a key or it's incorrect. Make sure the client has the
correct HMAC key, also specified with the `--hmac` flag.
5) You're trying to connect to a listener that's listening on an unspecified
IP address, and return packets are not routing properly, which can happen in
some network configurations. Try running the server with the `--set-src-ip`
flag, which sets the source address on all reply packets from listeners
on unspecified IP addresses. This is not done by default in order to avoid
the additional per-packet heap allocations required by the
`golang.org/x/net` packages.
9) Why can't the client connect to the server, and I either see `[Drop]
[UnknownParam] unknown negotiation param (0x8 = 0)` on the server, or a strange
message on the client like `[InvalidServerRestriction] server tried to reduce
interval to < 1s, from 1s to 92ns`?
You're using a 0.1 development version of the server with a newer client.
Make sure both client and server are up to date. Going forward, the protocol
is versioned (independently from IRTT in general), and is checked when the
client connects to the server. For now, the protocol versions must match
exactly.
10) Why don't you include median values for send call time, timer error and
server processing time?
Those values aren't stored for each round trip, and it's difficult to do a
running calculation of the median, although
[this method](https://rhettinger.wordpress.com/2010/02/06/lost-knowledge/) of
using skip lists appears to have promise. It's a possibility for the future,
but so far it isn't a high priority. If it is for you, file an
[Issue](https://github.com/peteheist/irtt/issues).
11) I see you use MD5 for the HMAC. Isn't that insecure?
MD5 should not have practical vulnerabilities when used in a message authenticate
code. See
[this page](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code#Security)
for more info.
12) Will you add unit tests?
Maybe some. I feel that the most important thing for a project of this size
is that the design is clear enough that bugs are next to impossible. IRTT
is not there yet though, particularly when it comes to packet manipulation.
13) Are there any plans for translation to other languages?
While some parts of the API were designed to keep i18n possible, there is no
support for i18n built in to the Go standard libraries. It should be possible,
but could be a challenge, and is not something I'm likely to undertake myself.
14) Why do I get `Error: failed to allocate results buffer for X round trips
(runtime error: makeslice: cap out of range)`?
Your test interval and duration probably require a results buffer that's
larger than Go can allocate on your platform. Lower either your test
interval or duration. See the following additional documentation for
reference: [In-memory results storage](#in-memory-results-storage),
`maxSliceCap` in [slice.go](https://golang.org/src/runtime/slice.go) and
`_MaxMem` in [malloc.go](https://golang.org/src/runtime/malloc.go).
15) Why is little endian byte order used in the packet format?
As for Google's [protobufs](https://github.com/google/protobuf), this was
chosen because the vast majority of modern processors use little-endian byte
order. In the future, packet manipulation may be optimized for little-endian
architecutures by doing conversions with Go's
[unsafe](https://golang.org/pkg/unsafe/) package, but so far this
optimization has not been shown to be necessary.
16) Why is the virt size (vsz) memory usage so high in Linux?
This has to do with the way Go allocates memory. See
[this article](https://deferpanic.com/blog/understanding-golang-memory-usage/)
for more information. File an Issue if your resident usage (rss/res) is high
or you feel that memory consumption is somehow a problem.
## Changes
See [CHANGES.md](CHANGES.md).
## Roadmap
### v1.0.0
_Planned for v1.0.0..._
- Refactor packet manipulation to improve readability, prevent multiple validations
and support unit tests
- Improve open/close process:
- Make timeout support automatic exponential backoff, like 4x15s
- Repeat close packets until acknowledgement, like open
- Include final stats in the close acknowledgement from the server
- Improve robustness and security of public servers:
- Add bitrate limiting
- Limit open requests rate and coordinate with sconn cleanup
- Add separate, shorter timeout for open
- Specify close timeout as param from client, which may be restricted
- Make connref mechanism robust to listener failure
- Add per-IP limiting
- Write a SmokePing probe
### Inbox
_Collection area for the future..._
- Improve induced latency and jitter:
- Use Go profiling, scheduler tracing, strace and sar
- Do more thorough tests of `chrt -r 99`, `--thread` and `--gc`
- Find or file issue with Go team over scheduler performance, if needed
- Prototype doing thread scheduling or socket i/o for Linux in C
- Add different server authentication modes:
- none (no conn token in header, for minimum packet sizes during local use)
- token (what we have today, 64-bit token in header)
- nacl-hmac (hmac key negotiated with public/private key encryption)
- Implement graceful server shutdown with sconn close
- Implement zero-downtime restarts
- Add a Scheduler interface to allow non-isochronous send schedules and variable
packet lengths
- Find some way to determine packet interval and length distributions for
captured traffic
- Determine if asymmetric send schedules (between client and server) required
- Add an overhead test mode to compare ping vs irtt
- Add client flag to skip sleep and catch up after timer misses
- Always return instance of irtt.Error? If so, look at exitOnError.
- Find better model for concurrency (one goroutine per sconn induces latency)
- Use error code (if available) as exit code
- Add seqno to the Max and maybe Min columns in the text output
- Prototype TCP throughput test and compare straight Go vs iperf/netperf
- Add a subcommand to the CLI to convert JSON to CSV
- Support a range of server ports to improve concurrency and maybe defeat
latency "slotting" on multi-queue interfaces
- Prompt to write JSON file on cancellation
- Add unit tests
- Add support for load balanced conns (multiple source addresses for same conn)
- Use unsafe package to speed up packet buffer manipulation
- Add encryption
- Add estimate for HMAC calculation time and correct send timestamp by this time
- Implement web interface for client and server
- Set DSCP per-packet, at least for IPv6
- Add NAT hole punching
- Add a flag to disable per-packet results
- Use a larger, internal received window on the server to increase up/down loss accuracy
- Implement median calculation for timer error, send call time and server processing time
- Allow specifying two out of three of interval, bitrate and packet size
- Calculate per-packet arrival order during results generation using timestamps
- Add OWD compensation at results generation stage for shifting mean value to 0
to improve readability for clocks that are badly out of sync
- Add a way to keep out "internal" info from JSON, like IP and hostname, and a
subcommand to strip these out after the JSON is created
- Make it possible to add custom per-round-trip statistics programmatically
- Add more info on outliers and possibly a textual histogram
- Allow Client Dial to try multiple IPs when a hostname is given
- Allow Server listen to listen on multiple IPs for a hostname
- What do I do for IPDV when there are out of order packets?
- Does exposing both monotonic and wall clock values open the server to any
timing attacks?
- Should I request a reserved IANA port?
## Thanks
Many thanks to both Toke Høiland-Jørgensen and Dave Täht from the
[Bufferbloat project](https://www.bufferbloat.net/) for their valuable
advice. Any problems in design or implementation are entirely my own.
irtt/cmd/ 0000755 0001751 0001751 00000000000 13240047124 011256 5 ustar pete pete irtt/cmd/irtt/ 0000755 0001751 0001751 00000000000 13240047124 012240 5 ustar pete pete irtt/cmd/irtt/main.go 0000644 0001751 0001751 00000000144 13240047124 013512 0 ustar pete pete package main
import (
"os"
"github.com/peteheist/irtt"
)
func main() {
irtt.RunCLI(os.Args)
}
irtt/averager.go 0000644 0001751 0001751 00000012312 13240047124 012635 0 ustar pete pete package irtt
import (
"fmt"
"strconv"
"strings"
)
// Averager is implemented to return an average of a series of given values.
type Averager interface {
// Push adds a value to be averaged.
Push(val float64)
// Average returns the average.
Average() float64
String() string
}
// CumulativeAverager implements the cumulative moving average (takes into account
// all values equally).
type CumulativeAverager struct {
sum float64
n float64
}
// Push adds a value.
func (ca *CumulativeAverager) Push(val float64) {
ca.sum += val
ca.n++
}
// Average gets the cumulative average.
func (ca *CumulativeAverager) Average() float64 {
if ca.n == 0 {
return 0
}
return ca.sum / ca.n
}
func (ca *CumulativeAverager) String() string {
return "avg"
}
// ExponentialAverager implements the exponential moving average. More recent
// values are given higher consideration. Alpha must be between 0 and 1, where a
// higher Alpha discounts older values faster. An Alpha of 0.1 - 0.2 may give
// good results for timer compensation, but experimentation is required as
// results are dependent on hardware and test config.
type ExponentialAverager struct {
Alpha float64
avg float64
prev float64
}
// Push adds a value.
func (ea *ExponentialAverager) Push(val float64) {
if ea.avg == 0 {
ea.prev = val
ea.avg = val
return
}
ea.prev = ea.avg
ea.avg = ea.Alpha*val + (1-ea.Alpha)*ea.prev
}
// Average gets the exponential average.
func (ea *ExponentialAverager) Average() float64 {
return ea.avg
}
func (ea *ExponentialAverager) String() string {
return fmt.Sprintf("exp:%.2f", ea.Alpha)
}
// NewExponentialAverager returns a new ExponentialAverage with the specified
// Alpha.
func NewExponentialAverager(alpha float64) *ExponentialAverager {
return &ExponentialAverager{Alpha: alpha}
}
// NewDefaultExponentialAverager returns a new ExponentialAverage with the
// default Alpha. This may be changed before used.
func NewDefaultExponentialAverager() *ExponentialAverager {
return NewExponentialAverager(DefaultExponentialAverageAlpha)
}
// WindowAverager implements the moving average with a specified window.
type WindowAverager struct {
Window int
values []float64
pos int
filled bool
}
// Push adds a value.
func (wa *WindowAverager) Push(val float64) {
wa.values[wa.pos] = val
wa.pos++
if wa.pos == wa.Window {
wa.pos = 0
wa.filled = true
}
}
// Average gets the moving average.
func (wa *WindowAverager) Average() float64 {
var sum = float64(0)
var c = wa.Window - 1
// ignore unavailable values
if !wa.filled {
c = wa.pos - 1
if c < 0 {
return 0
}
}
// sum values
var ic = 0
for i := 0; i <= c; i++ {
sum += wa.values[i]
ic++
}
// calculate average and return
avg := sum / float64(ic)
return avg
}
func (wa *WindowAverager) String() string {
return fmt.Sprintf("win:%d", wa.Window)
}
// NewWindowAverage returns a new WindowAverage with the specified window.
func NewWindowAverage(window int) *WindowAverager {
return &WindowAverager{
Window: window,
values: make([]float64, window),
pos: 0,
filled: false,
}
}
// NewDefaultWindowAverager returns a new WindowAverage with the default window.
func NewDefaultWindowAverager() *WindowAverager {
return NewWindowAverage(DefaultAverageWindow)
}
// AveragerFactories are the registered Averager factories.
var AveragerFactories = make([]AveragerFactory, 0)
// AveragerFactory is the definition for an Averager.
type AveragerFactory struct {
FactoryFunc func(string) (Averager, error)
Usage string
}
// RegisterAverager registers a new Averager.
func RegisterAverager(fn func(string) (Averager, error), usage string) {
AveragerFactories = append(AveragerFactories, AveragerFactory{fn, usage})
}
// NewAverager returns an Averager from a string.
func NewAverager(s string) (Averager, error) {
for _, fac := range AveragerFactories {
a, err := fac.FactoryFunc(s)
if err != nil {
return nil, err
}
if a != nil {
return a, nil
}
}
return nil, Errorf(NoSuchAverager, "no such Averager %s", s)
}
func init() {
RegisterAverager(
func(s string) (a Averager, err error) {
if s == "avg" {
a = &CumulativeAverager{}
}
return
},
"avg: cumulative average error",
)
RegisterAverager(
func(s string) (Averager, error) {
args := strings.Split(s, ":")
if args[0] != "win" {
return nil, nil
}
if len(args) == 1 {
return NewDefaultWindowAverager(), nil
}
w, err := strconv.Atoi(args[1])
if err != nil || w < 1 {
return nil, Errorf(InvalidWinAvgWindow, "invalid window %s to window average", args[1])
}
return NewWindowAverage(w), nil
},
fmt.Sprintf("win:#: moving average error with window # (default %d)",
DefaultAverageWindow),
)
RegisterAverager(
func(s string) (Averager, error) {
args := strings.Split(s, ":")
if args[0] != "exp" {
return nil, nil
}
if len(args) == 1 {
return NewDefaultExponentialAverager(), nil
}
a, err := strconv.ParseFloat(args[1], 64)
if err != nil || a < 0 || a > 1 {
return nil, Errorf(InvalidExpAvgAlpha, "invalid alpha %s to exponential average", args[1])
}
return NewExponentialAverager(a), nil
},
fmt.Sprintf("exp:#: exponential average with alpha # (default %.2f)",
DefaultExponentialAverageAlpha),
)
}
irtt/client.go 0000644 0001751 0001751 00000025340 13240047124 012324 0 ustar pete pete package irtt
import (
"context"
"fmt"
"math/rand"
"net"
"runtime"
"runtime/debug"
"sync"
"time"
)
// Client is the Client. It must be created with NewClient. It may not be used
// concurrently.
type Client struct {
*ClientConfig
conn *cconn
rec *Recorder
closed bool
closedM sync.Mutex
}
// NewClient returns a new client.
func NewClient(cfg *ClientConfig) *Client {
// create client
c := *cfg
c.Supplied = cfg
return &Client{
ClientConfig: &c,
}
}
// Run runs the test and returns the Result. An error is returned if the test
// could not be started. If an error occurs during the test, the error is nil,
// partial results are returned and either or both of the SendErr or
// ReceiveErr fields of Result will be non-nil. Run may only be called once.
func (c *Client) Run(ctx context.Context) (r *Result, err error) {
// validate config
if err = c.validate(); err != nil {
return
}
// notify about connecting
c.eventf(Connecting, "connecting to %s", c.RemoteAddress)
// dial server
if c.conn, err = dial(ctx, c.ClientConfig); err != nil {
return
}
defer c.close()
// check parameter changes
if err = c.checkParameters(); err != nil {
return
}
// notify about connection status
if c.conn != nil {
c.eventf(Connected, "connection established")
} else {
c.eventf(ConnectedClosed, "connection accepted and closed")
return
}
// return if NoTest is set
if c.ClientConfig.NoTest {
err = nil
c.eventf(NoTest, "skipping test at user request")
return
}
// ignore server restrictions for testing
if ignoreServerRestrictions {
fmt.Println("Ignoring server restrictions!")
c.Params = c.Supplied.Params
}
// return error if DSCP can't be used
if c.DSCP != 0 && !c.conn.dscpSupport {
err = Errorf(NoDSCPSupport, "unable to set DSCP value (%s)", c.conn.dscpError)
return
}
// set DF value on socket
if c.DF != DefaultDF {
if derr := c.conn.setDF(c.DF); derr != nil {
err = Errorf(DFError, "unable to set do not fragment bit (%s)", derr)
return
}
}
// set TTL
if c.TTL != DefaultTTL {
if terr := c.conn.setTTL(c.TTL); terr != nil {
err = Errorf(TTLError, "unable to set TTL %d (%s)", c.TTL, terr)
return
}
}
// create recorder
if c.rec, err = newRecorder(pcount(c.Duration, c.Interval), c.Handler); err != nil {
return
}
// wait group for goroutine completion
wg := sync.WaitGroup{}
// collect before test
runtime.GC()
// disable GC
debug.SetGCPercent(-1)
// start receive
var rerr error
wg.Add(1)
go func() {
defer wg.Done()
defer c.close()
rerr = c.receive()
if rerr != nil && c.isClosed() {
rerr = nil
}
}()
// start send
var serr error
wg.Add(1)
go func() {
defer wg.Done()
defer c.close()
serr = c.send(ctx)
if serr == nil {
err = c.wait(ctx)
}
if serr != nil && c.isClosed() {
serr = nil
}
}()
// wait for send and receive to complete
wg.Wait()
// re-enable GC
debug.SetGCPercent(100)
r = newResult(c.rec, c.ClientConfig, serr, rerr)
return
}
func (c *Client) close() {
c.closedM.Lock()
defer c.closedM.Unlock()
if !c.closed {
if c.conn != nil {
c.conn.close()
}
c.closed = true
}
}
func (c *Client) isClosed() bool {
c.closedM.Lock()
defer c.closedM.Unlock()
return c.closed
}
// localAddr returns the local address (non-nil after server dialed).
func (c *Client) localAddr() *net.UDPAddr {
if c.conn == nil {
return nil
}
return c.conn.localAddr()
}
// remoteAddr returns the remote address (non-nil after server dialed).
func (c *Client) remoteAddr() *net.UDPAddr {
if c.conn == nil {
return nil
}
return c.conn.remoteAddr()
}
// checkParameters checks any changes after the server returned restricted
// parameters.
func (c *Client) checkParameters() (err error) {
paramEvent := func(code Code, format string, detail ...interface{}) {
if c.Loose {
c.eventf(code, format, detail...)
} else {
err = Errorf(code, format, detail...)
}
}
if c.ProtocolVersion != ProtocolVersion {
err = Errorf(ProtocolVersionMismatch,
"client version %d != server version %d", ProtocolVersion, c.ProtocolVersion)
return
}
if c.Duration < c.Supplied.Duration {
paramEvent(ServerRestriction, "server reduced duration from %s to %s",
c.Supplied.Duration, c.Duration)
if err != nil {
return
}
}
if c.Duration > c.Supplied.Duration {
err = Errorf(InvalidServerRestriction,
"server tried to change duration from %s to %s",
c.Supplied.Duration, c.Duration)
return
}
if c.Interval > c.Supplied.Interval {
paramEvent(ServerRestriction, "server increased interval from %s to %s",
c.Supplied.Interval, c.Interval)
if err != nil {
return
}
}
if c.Interval < c.Supplied.Interval {
if c.Interval < minRestrictedInterval {
err = Errorf(InvalidServerRestriction,
"server tried to reduce interval to < %s, from %s to %s",
minRestrictedInterval, c.Supplied.Interval, c.Interval)
return
}
paramEvent(ServerRestriction,
"server reduced interval from %s to %s to avoid %s timeout",
c.Supplied.Interval, c.Interval, c.Interval*maxIntervalTimeoutFactor)
if err != nil {
return
}
}
if c.Length < c.Supplied.Length {
paramEvent(ServerRestriction, "server reduced length from %d to %d",
c.Supplied.Length, c.Length)
if err != nil {
return
}
}
if c.Length > c.Supplied.Length {
err = Errorf(InvalidServerRestriction,
"server tried to increase length from %d to %d",
c.Supplied.Length, c.Length)
return
}
if c.StampAt != c.Supplied.StampAt {
paramEvent(ServerRestriction, "server restricted timestamps from %s to %s",
c.Supplied.StampAt, c.StampAt)
if err != nil {
return
}
}
if c.Clock != c.Supplied.Clock {
paramEvent(ServerRestriction, "server restricted clocks from %s to %s",
c.Supplied.Clock, c.Clock)
if err != nil {
return
}
}
if c.DSCP != c.Supplied.DSCP {
paramEvent(ServerRestriction, "server doesn't support DSCP")
if err != nil {
return
}
}
if c.ServerFill != c.Supplied.ServerFill {
paramEvent(ServerRestriction,
"server restricted fill from %s to %s", c.Supplied.ServerFill,
c.ServerFill)
if err != nil {
return
}
}
return
}
// send sends all packets for the test to the server (called in goroutine from Run)
func (c *Client) send(ctx context.Context) error {
if c.ThreadLock {
runtime.LockOSThread()
}
// include 0 timestamp in appropriate fields
seqno := Seqno(0)
p := c.conn.newPacket()
if c.conn.dscpSupport {
p.dscp = c.DSCP
}
p.addFields(fechoRequest, true)
p.zeroReceivedStats(c.ReceivedStats)
p.stampZeroes(c.StampAt, c.Clock)
p.setSeqno(seqno)
c.Length = p.setLen(c.Length)
// fill the first packet, if necessary
if c.Filler != nil {
err := p.readPayload(c.Filler)
if err != nil {
return err
}
} else {
p.zeroPayload()
}
// lastly, set the HMAC
p.updateHMAC()
// record the start time of the test and calculate the end
t := time.Now()
c.rec.Start = t
end := c.rec.Start.Add(c.Duration)
// keep sending until the duration has passed
for {
// send to network and record times right before and after
tsend := c.rec.recordPreSend()
var err error
if clientDropsPercent == 0 || rand.Float32() > clientDropsPercent {
err = c.conn.send(p)
} else {
// simulate drop with an average send time
time.Sleep(20 * time.Microsecond)
}
// return on error
if err != nil {
c.rec.removeLastStamps()
return err
}
// record send call
c.rec.recordPostSend(tsend, p.tsent, uint64(p.length()))
// prepare next packet (before sleep, so the next send time is as
// precise as possible)
seqno++
p.setSeqno(seqno)
if c.Filler != nil && !c.FillOne {
err := p.readPayload(c.Filler)
if err != nil {
return err
}
}
p.updateHMAC()
// set the current base interval we're at
tnext := c.rec.Start.Add(
c.Interval * (time.Now().Sub(c.rec.Start) / c.Interval))
// if we're under half-way to the next interval, sleep until the next
// interval, but if we're over half-way, sleep until the interval after
// that
if p.tsent.Sub(c.rec.Start)%c.Interval < c.Interval/2 {
tnext = tnext.Add(c.Interval)
} else {
tnext = tnext.Add(2 * c.Interval)
}
// break if tnext if after the end of the test
if !tnext.Before(end) {
break
}
// calculate sleep duration
tsleep := time.Now()
dsleep := tnext.Sub(tsleep)
// sleep
t, err = c.Timer.Sleep(ctx, tsleep, dsleep)
if err != nil {
return err
}
// record timer error
c.rec.recordTimerErr(t.Sub(tsleep) - dsleep)
}
return nil
}
// receive receives packets from the server (called in goroutine from Run)
func (c *Client) receive() error {
if c.ThreadLock {
runtime.LockOSThread()
}
p := c.conn.newPacket()
for {
// read a packet
err := c.conn.receive(p)
if err != nil {
return err
}
// drop packets with open flag set
if p.flags()&flOpen != 0 {
return Errorf(UnexpectedOpenFlag, "unexpected open flag set")
}
// add expected echo reply fields
p.addFields(fechoReply, false)
// return an error if reply packet was too small
if p.length() < c.Length {
return Errorf(ShortReply, "received short reply (%d bytes)",
p.length())
}
// add expected received stats fields
p.addReceivedStatsFields(c.ReceivedStats)
// add expected timestamp fields
p.addTimestampFields(c.StampAt, c.Clock)
// get timestamps and return an error if the timestamp setting is
// different (server doesn't support timestamps)
at := p.stampAt()
if at != c.StampAt {
return Errorf(StampAtMismatch, "server stamped at %s, but %s was requested",
at, c.StampAt)
}
if at != AtNone {
cl := p.clock()
if cl != c.Clock {
return Errorf(ClockMismatch, "server clock %s, but %s was requested", cl, c.Clock)
}
}
sts := p.timestamp()
// record receive if all went well (may fail if seqno not found)
ok := c.rec.recordReceive(p, &sts)
if !ok {
return Errorf(UnexpectedSequenceNumber, "unexpected reply sequence number %d", p.seqno())
}
}
}
// wait waits for final packets
func (c *Client) wait(ctx context.Context) (err error) {
// return if all packets have been received
c.rec.RLock()
if c.rec.RTTStats.N >= c.rec.SendCallStats.N {
c.rec.RUnlock()
return
}
c.rec.RUnlock()
// wait
dwait := c.Waiter.Wait(c.rec)
if dwait > 0 {
c.rec.Wait = dwait
c.eventf(WaitForPackets, "waiting %s for final packets", rdur(dwait))
select {
case <-time.After(dwait):
case <-ctx.Done():
err = ctx.Err()
}
}
return
}
func (c *Client) eventf(code Code, format string, detail ...interface{}) {
if c.Handler != nil {
c.Handler.OnEvent(Eventf(code, c.localAddr(), c.remoteAddr(), format, detail...))
}
}
// ClientHandler is called with client events, as well as separately when
// packets are sent and received. See the documentation for Recorder for
// information on locking for concurrent access.
type ClientHandler interface {
Handler
RecorderHandler
}
irtt/error.go 0000644 0001751 0001751 00000003301 13240047124 012170 0 ustar pete pete package irtt
// Common error codes.
const (
ShortWrite Code = -1 * (iota + 1)
InvalidDFString
FieldsLengthTooLarge
FieldsCapacityTooLarge
InvalidStampAtString
InvalidStampAtInt
InvalidAllowStampString
InvalidClockString
InvalidClockInt
BadMagic
NoHMAC
BadHMAC
UnexpectedHMAC
NonexclusiveMidpointTStamp
InconsistentClocks
DFNotSupported
InvalidFlagBitsSet
ShortParamBuffer
ParamOverflow
InvalidParamValue
ProtocolVersionMismatch
)
// Server error codes.
const (
NoMatchingInterfaces Code = -1 * (iota + 1*1024)
NoMatchingInterfacesUp
UnspecifiedWithSpecifiedAddresses
InvalidGCModeString
UnexpectedReplyFlag
NoSuitableAddressFound
InvalidConnToken
ShortInterval
LargeRequest
AddressMismatch
)
// Client error codes.
const (
InvalidWinAvgWindow Code = -1 * (iota + 2*1024)
InvalidExpAvgAlpha
AllocateResultsPanic
UnexpectedOpenFlag
DFError
TTLError
ExpectedReplyFlag
ShortReply
StampAtMismatch
ClockMismatch
UnexpectedSequenceNumber
InvalidSleepFactor
InvalidWaitString
InvalidWaitFactor
InvalidWaitDuration
NoSuchAverager
NoSuchFiller
NoSuchTimer
NoSuchWaiter
IntervalNonPositive
DurationNonPositive
ConnTokenZero
ServerClosed
OpenTimeout
InvalidServerRestriction
InvalidReceivedStatsInt
InvalidReceivedStatsString
OpenTimeoutTooShort
ServerFillTooLong
)
// Error is an IRTT error.
type Error struct {
*Event
}
// Errorf returns a new Error.
func Errorf(code Code, format string, detail ...interface{}) *Error {
return &Error{Eventf(code, nil, nil, format, detail...)}
}
func (e *Error) Error() string {
return e.Event.String()
}
func isErrorCode(code Code, err error) (matches bool) {
if e, ok := err.(*Error); ok {
matches = (e.Code == code)
}
return
}
irtt/CHANGES.md 0000644 0001751 0001751 00000004157 13240047124 012114 0 ustar pete pete # Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## 0.9.0 - 2018-02-11
### Added
- Server fills are now supported, and may be restricted on the server. See
`--sfill` for the client and `--allow-fills` on the server. As an example, one
can do `irtt client --fill=rand --sfill=rand -l 172 server` for random
payloads in both directions. The server default is `--allow-fills=rand` so
that arbitrary data cannot be relayed between two hosts. `server_fill` now
appears under `params` in the JSON.
- Version information has been added to the JSON output.
### Changed
- Due to adoption of the [pflag](https://github.com/ogier/pflag) package, all long
options now start with -- and must use = with values (e.g. `--fill=rand`).
After the subcommand, flags and arguments may now appear in any order.
- `irtt client` syntax changes:
- `-rs` is renamed to `--stats`
- `-strictparams` is removed and is now the default. `--loose` may be used
instead to accept and use server restricted parameters, with a warning.
- `-ts` is renamed to `--tstamp`
- `-qq` is renamed to `-Q`
- `-fillall` is removed and is now the default. `--fill-one` may be used as
a small optimization, but should rarely be needed.
- `irtt server` syntax changes:
- `-nodscp` is renamed to `--no-dscp`
- `-setsrcip` is renamed to `--set-src-ip`
- The communication protocol has changed, so clients and servers must both be
updated.
- The handshake now includes a protocol version, which may change independently
of the release version, and must match exactly between the client and server
or the client will refuse to connect.
- The default server minimum interval is now `10ms`.
- The default client duration has been changed from `1h` to `1m`.
- Some author info was changed in the commit history, so the rewritten history
must be fetched in all forks and any changes rebased.
## 0.1.0 - 2017-10-15
### Added
- Initial, untagged development release.
irtt/.gitignore 0000644 0001751 0001751 00000000450 13240047124 012502 0 ustar pete pete # Log files
*.log
# Profiles
client.pprof
server.pprof
# Executables
/irtt
/irtt.exe
# Private directory
private
# Debian package files
dpkg
# JSON output files
*.json
*.json.gz
# VIM
*.swp
.tags
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
irtt/mtu.go 0000644 0001751 0001751 00000004066 13240047124 011655 0 ustar pete pete package irtt
import (
"fmt"
"net"
)
// detectMTU autodetects and returns the MTU either for the interface associated
// with the specified IP, or if the ip parameter is nil, the max MTU of all
// interfaces, and if it cannot be determined, a fallback default.
func detectMTU(ip net.IP) (int, string) {
if ip != nil {
iface, err := interfaceByIP(ip)
if err != nil || iface == nil {
return maxOrDefaultMTU()
}
return iface.MTU, iface.Name
}
return maxOrDefaultMTU()
}
// maxOrDefaultMTU returns the maximum MTU for all interfaces, or the
// compiled-in default if it could not be determined.
func maxOrDefaultMTU() (int, string) {
mtu, _, err := largestMTU(false)
msg := "all"
if err != nil || mtu < minValidMTU {
msg = fmt.Sprintf("fallback (%s)", err)
mtu = maxMTU
} else if mtu < minValidMTU {
msg = fmt.Sprintf("fallback (MTU %d too small)", mtu)
mtu = maxMTU
}
return mtu, msg
}
// largestMTU queries all interfaces and returns the largest MTU. If the up
// parameter is true, only interfaces that are up are considered.
func largestMTU(up bool) (lmtu int, ifaces []string, err error) {
ifcs, err := net.Interfaces()
if err != nil {
return
}
for _, iface := range ifcs {
ifaces = append(ifaces, iface.Name)
if (!up || ((iface.Flags & net.FlagUp) != 0)) && iface.MTU > lmtu {
lmtu = iface.MTU
}
}
return
}
// interfaceByIP returns the first interface whose network contains the given
// IP address. An interface of nil is returned if no matching interface is
// found.
func interfaceByIP(ip net.IP) (*net.Interface, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
continue
}
// I've only ever seen *net.IPNet returned by the Addrs() method,
//but I'll test for *net.IPAddr just in case.
for _, a := range addrs {
switch ipv := a.(type) {
case *net.IPNet:
if ipv.IP.Equal(ip) {
return &iface, nil
}
case *net.IPAddr:
if ipv.IP.Equal(ip) {
return &iface, nil
}
}
}
}
return nil, nil
}
irtt/server.go 0000644 0001751 0001751 00000021007 13240047124 012350 0 ustar pete pete package irtt
import (
"encoding/binary"
"math/rand"
"net"
"runtime"
"runtime/debug"
"sync"
"time"
)
// Server is the irtt server.
type Server struct {
*ServerConfig
start time.Time
connRefs int
connRefMtx sync.Mutex
shutdown bool
shutdownMtx sync.Mutex
shutdownC chan struct{}
}
// NewServer returns a new server.
func NewServer(cfg *ServerConfig) *Server {
return &Server{
ServerConfig: cfg,
shutdownC: make(chan struct{}),
}
}
// ListenAndServe creates listeners for all requested addresses and serves
// requests indefinitely. It exits after the listeners have exited. Errors for
// individual listeners may be handled with a ServerHandler, and will not be
// returned from this method.
func (s *Server) ListenAndServe() error {
// start is the base time that monotonic timestamp values are from
s.start = time.Now()
// send ServerStart event
if s.Handler != nil {
s.Handler.OnEvent(Eventf(ServerStart, nil, nil,
"starting IRTT server version %s", Version))
}
// make listeners
listeners, err := s.makeListeners()
if err != nil {
return err
}
// start listeners
errC := make(chan error)
for _, l := range listeners {
// send ListenerStart event
l.eventf(ListenerStart, nil, "starting %s listener on %s", l.conn.ipVer,
l.conn.localAddr())
go l.listenAndServe(errC)
}
// disable GC, if requested
if s.GCMode == GCOff {
debug.SetGCPercent(-1)
}
// wait on shutdown chan
go func() {
<-s.shutdownC
for _, l := range listeners {
l.shutdown()
}
}()
// wait for all listeners, and out of an abundance of caution, shut down
// all other listeners if any one of them fails
for i := 0; i < len(listeners); i++ {
if err := <-errC; err != nil {
s.Shutdown()
}
}
// send ServerStop event
if s.Handler != nil {
s.Handler.OnEvent(Eventf(ServerStop, nil, nil,
"stopped IRTT server"))
}
return nil
}
// Shutdown stops the Server. After this call, the Server may no longer be used.
func (s *Server) Shutdown() {
s.shutdownMtx.Lock()
defer s.shutdownMtx.Unlock()
if !s.shutdown {
close(s.shutdownC)
s.shutdown = true
}
}
func (s *Server) makeListeners() ([]*listener, error) {
lconns, err := listenAll(s.IPVersion, s.Addrs, s.SetSrcIP)
if err != nil {
return nil, err
}
ls := make([]*listener, 0, len(lconns))
for _, lconn := range lconns {
ls = append(ls, newListener(s.ServerConfig, lconn, s.connRef))
}
return ls, nil
}
func (s *Server) connRef(b bool) {
s.connRefMtx.Lock()
defer s.connRefMtx.Unlock()
if b {
s.connRefs++
if s.connRefs == 1 {
runtime.GC()
if s.GCMode == GCIdle {
debug.SetGCPercent(-1)
}
}
} else {
s.connRefs--
if s.connRefs == 0 {
if s.GCMode == GCIdle {
debug.SetGCPercent(100)
}
runtime.GC()
}
}
}
// listener is a server listener.
type listener struct {
*ServerConfig
conn *lconn
pktPool *pktPool
cmgr *connmgr
closed bool
closedMtx sync.Mutex
}
func newListener(cfg *ServerConfig, lc *lconn, cref func(bool)) *listener {
cap, _ := detectMTU(lc.localAddr().IP)
pp := newPacketPool(func() *packet {
return newPacket(0, cap, cfg.HMACKey)
}, 16)
return &listener{
ServerConfig: cfg,
conn: lc,
pktPool: pp,
cmgr: newConnMgr(cfg, cref),
}
}
func (l *listener) listenAndServe(errC chan<- error) (err error) {
// always return error to channel
defer func() {
errC <- err
}()
// always close conn
defer func() {
l.conn.close()
}()
// always log error or stoppage
defer func() {
if err != nil {
l.eventf(ListenerError, nil, "error for listener on %s (%s)",
l.conn.localAddr(), err)
} else {
l.eventf(ListenerStop, nil, "stopped listener on %s",
l.conn.localAddr())
}
}()
// lock to thread
if l.ThreadLock {
runtime.LockOSThread()
}
// set TTL
if l.TTL != 0 {
err = l.conn.setTTL(l.TTL)
if err != nil {
return
}
}
// warn if DSCP not supported
if l.AllowDSCP && !l.conn.dscpSupport {
l.eventf(NoDSCPSupport, nil, "[%s] no %s DSCP support available (%s)",
l.conn.localAddr(), l.conn.ipVer, l.conn.dscpError)
}
// enable receipt of destination IP
if l.SetSrcIP && l.conn.localAddr().IP.IsUnspecified() {
if rdsterr := l.conn.setReceiveDstAddr(true); rdsterr != nil {
l.eventf(NoReceiveDstAddrSupport, nil,
"no support for determining packet destination address (%s)", rdsterr)
if err := l.warnOnMultipleAddresses(); err != nil {
return err
}
}
}
err = l.readAndReply()
if l.isClosed() {
err = nil
}
return
}
func (l *listener) readAndReply() (err error) {
p := l.pktPool.new()
for {
if err = l.readOneAndReply(p); err != nil {
if l.isFatalError(err) {
return
}
l.eventf(Drop, p.raddr, "%s", err.Error())
}
}
}
func (l *listener) readOneAndReply(p *packet) (err error) {
// read a packet
if err = l.conn.receive(p); err != nil {
return
}
// handle open
if p.flags()&flOpen != 0 {
_, err = accept(l, p)
return
}
// handle packet for sconn
if err = p.addFields(fRequest, false); err != nil {
return
}
ct := p.ctoken()
sc := l.cmgr.get(ct)
if sc == nil {
err = Errorf(InvalidConnToken, "invalid conn token %016x", ct)
return
}
_, err = sc.serve(p)
return
}
func (l *listener) eventf(code Code, raddr *net.UDPAddr, format string,
detail ...interface{}) {
if l.Handler != nil {
l.Handler.OnEvent(Eventf(code, l.conn.localAddr(), raddr, format, detail...))
}
}
func (l *listener) isFatalError(err error) (fatal bool) {
if nerr, ok := err.(net.Error); ok {
fatal = !nerr.Temporary()
}
return
}
func (l *listener) warnOnMultipleAddresses() error {
ifaces, err := net.Interfaces()
if err != nil {
return err
}
n := 0
for _, i := range ifaces {
addrs, err := i.Addrs()
if err != nil {
return err
}
for _, addr := range addrs {
switch v := addr.(type) {
case *net.IPNet:
if v.IP.IsGlobalUnicast() {
n++
}
case *net.IPAddr:
if v.IP.IsGlobalUnicast() {
n++
}
}
}
}
if n > 1 {
l.eventf(MultipleAddresses, nil, "warning: multiple IP addresses, "+
"all bind addresses should be explicitly specified with -b or "+
"clients may not be able to connect")
}
return nil
}
func (l *listener) isClosed() bool {
l.closedMtx.Lock()
defer l.closedMtx.Unlock()
return l.closed
}
func (l *listener) shutdown() {
l.closedMtx.Lock()
defer l.closedMtx.Unlock()
if !l.closed {
if l.conn != nil {
l.conn.close()
}
l.closed = true
}
}
// pktPool pools packets to reduce per-packet heap allocations
type pktPool struct {
pool []*packet
mtx sync.Mutex
new func() *packet
}
func newPacketPool(new func() *packet, cap int) *pktPool {
pp := &pktPool{
pool: make([]*packet, 0, cap),
new: new,
}
return pp
}
func (po *pktPool) get() *packet {
po.mtx.Lock()
defer po.mtx.Unlock()
l := len(po.pool)
if l == 0 {
return po.new()
}
p := po.pool[l-1]
po.pool = po.pool[:l-1]
return p
}
func (po *pktPool) put(p *packet) {
po.mtx.Lock()
defer po.mtx.Unlock()
po.pool = append(po.pool, p)
}
// connmgr manages server connections
type connmgr struct {
*ServerConfig
ref func(bool)
sconns map[ctoken]*sconn
}
func newConnMgr(cfg *ServerConfig, ref func(bool)) *connmgr {
return &connmgr{
ServerConfig: cfg,
ref: ref,
sconns: make(map[ctoken]*sconn, sconnsInitSize),
}
}
func (cm *connmgr) put(sc *sconn) {
cm.removeSomeExpired()
ct := cm.newCtoken()
sc.ctoken = ct
cm.sconns[ct] = sc
cm.ref(true)
}
func (cm *connmgr) get(ct ctoken) (sc *sconn) {
if sc = cm.sconns[ct]; sc == nil {
return
}
if sc.expired() {
cm.delete(ct)
}
return
}
func (cm *connmgr) remove(ct ctoken) (sc *sconn) {
var ok bool
if sc, ok = cm.sconns[ct]; ok {
cm.delete(ct)
}
return
}
// removeSomeExpired checks checkExpiredCount sconns for expiration and removes
// them if expired. Yes, I know, I'm depending on Go's random map iteration
// start point, which per the language spec, I should not depend on. That said,
// this makes for a highly CPU efficient way to eventually clean up expired
// sconns, and because the Go team very intentionally made map order traversal
// random for a good reason, I don't think that's going to change any time soon.
func (cm *connmgr) removeSomeExpired() {
i := 0
for ct, sc := range cm.sconns {
if sc.expired() {
cm.delete(ct)
}
if i++; i >= checkExpiredCount {
break
}
}
}
func (cm *connmgr) newCtoken() ctoken {
var ct ctoken
b := make([]byte, 8)
for {
rand.Read(b)
ct = ctoken(binary.LittleEndian.Uint64(b))
if _, ok := cm.sconns[ct]; !ok {
break
}
}
return ct
}
func (cm *connmgr) delete(ct ctoken) {
delete(cm.sconns, ct)
cm.ref(false)
}
irtt/packet.go 0000644 0001751 0001751 00000027271 13240047124 012322 0 ustar pete pete package irtt
import (
"crypto/hmac"
"crypto/md5"
"encoding/binary"
"hash"
"io"
"math"
"net"
"time"
)
// -------------------------------------------------------------------------------
// | Oct | 0 | 1 | 2 | 3 |
// -------------------------------------------------------------------------------
// | | 0 1 2 3 4 5 6 7 | 0 1 2 3 4 5 6 7 | 0 1 2 3 4 5 6 7 | 0 1 2 3 4 5 6 7 |
// |------------------------------------------------------------------------------
// | 0 | Magic | Flags |
// |------------------------------------------------------------------------------
// | 4 | Conn Token |
// |------------------------------------------------------------------------------
// | 8 | Conn Token |
// |------------------------------------------------------------------------------
// | 12 | Seqno |
// |------------------------------------------------------------------------------
// | 16 | HMAC (if HMAC flag set) |
// |------------------------------------------------------------------------------
// | 20 | HMAC (if HMAC flag set) |
// |------------------------------------------------------------------------------
// | 24 | HMAC (if HMAC flag set) |
// |------------------------------------------------------------------------------
// | 28 | HMAC (if HMAC flag set) |
// |------------------------------------------------------------------------------
// | 32..| Optional Fields and Payload |
// |------------------------------------------------------------------------------
// little endian used for multi-byte ints
var endian = binary.LittleEndian
// Seqno is a sequence number.
type Seqno uint32
// InvalidSeqno indicates a sequence number that is not valid.
const InvalidSeqno = Seqno(math.MaxUint32)
// ReceivedCount is the received packet count.
type ReceivedCount uint32
// ReceivedWindow is the received packet window.
type ReceivedWindow uint64
// ctoken is a conn token
type ctoken uint64
// magic bytes
var magic = []byte{0x14, 0xa7, 0x5b}
// packet flags
type flags byte
func (f flags) set(fset flags) flags {
return f | fset
}
func (f flags) clear(fcl flags) flags {
return f &^ fcl
}
func (f flags) isset(fl flags) bool {
return f&fl != 0
}
const (
// flOpen is set when opening a new conn, both in the initial request from
// the client to the server, and in the reply from the server.
flOpen flags = 1 << iota
// flReply is set in all packets from the server to the client, and unset in
// all packets from the client to the server.
flReply
// flClose is set when closing a conn, both in the final request from the
// client to the server, and in the reply from the server.
flClose
// flHMAC is set if an HMAC hash is included (so we can tell the
// difference between a missing and invalid HMAC).
flHMAC
)
const flAll = flOpen | flReply | flClose | flHMAC
// field indexes
const (
fMagic fidx = iota
fFlags
fHMAC
fConnToken
fSeqno
fRCount
fRWindow
fRWall
fRMono
fMWall
fMMono
fSWall
fSMono
)
const fcount = fSMono + 1
const foptidx = fHMAC
// field capacities (sync with field constants)
var fcaps = []int{3, 1, md5.Size, 8, 4, 4, 8, 8, 8, 8, 8, 8, 8}
// field index definitions
var finit = []fidx{fMagic, fFlags}
var finitHMAC = []fidx{fMagic, fFlags, fHMAC}
var fopenReply = []fidx{fMagic, fFlags, fConnToken}
var fRequest = []fidx{fMagic, fFlags, fConnToken}
var fcloseRequest = []fidx{fMagic, fFlags, fConnToken}
var fechoRequest = []fidx{fMagic, fFlags, fConnToken, fSeqno}
var fechoReply = []fidx{fMagic, fFlags, fConnToken, fSeqno}
// minHeaderLen is the minimum header length (set in init).
var minHeaderLen int
// maxHeaderLen is the maximum header length (set in init).
var maxHeaderLen int
func init() {
for i := fidx(0); i < fcount; i++ {
if i < foptidx {
minHeaderLen += fcaps[i]
}
maxHeaderLen += fcaps[i]
}
}
func newFields() []field {
f := make([]field, fcount)
for i := fidx(0); i < fcount; i++ {
f[i].cap = fcaps[i]
}
return f
}
// Decorations for Time
func (t *Time) setWallFromBytes(b []byte) {
t.Wall = int64(endian.Uint64(b[:]))
}
func (t *Time) setMonoFromBytes(b []byte) {
t.Mono = time.Duration(endian.Uint64(b[:]))
}
func (t *Time) wallToBytes(b []byte) {
endian.PutUint64(b[:], uint64(t.Wall))
}
func (t *Time) monoToBytes(b []byte) {
endian.PutUint64(b[:], uint64(t.Mono))
}
// Packet struct and construction/set methods
type packet struct {
*fbuf
md5Hash hash.Hash
hmacKey []byte
raddr *net.UDPAddr
tsent time.Time
trcvd time.Time
srcIP net.IP
dstIP net.IP
dscp int
}
func newPacket(tlen int, cap int, hmacKey []byte) *packet {
if cap < maxHeaderLen {
cap = maxHeaderLen
}
p := &packet{fbuf: newFbuf(newFields(), tlen, cap)}
if len(hmacKey) > 0 {
p.setFields(finitHMAC, true)
p.md5Hash = hmac.New(md5.New, hmacKey)
p.hmacKey = hmacKey
} else {
p.setFields(finit, true)
}
p.set(fMagic, magic)
return p
}
func (p *packet) readReset(n int) error {
if p.md5Hash != nil {
p.setFields(finitHMAC, false)
} else {
p.setFields(finit, false)
}
p.buf = p.buf[:n]
p.tlen = n
if err := p.fbuf.validate(); err != nil {
return err
}
return p.validate()
}
func (p *packet) readTo() []byte {
p.buf = p.buf[:cap(p.buf)]
return p.buf
}
func (p *packet) validate() error {
// magic
if !bytesEqual(p.get(fMagic), magic) {
return Errorf(BadMagic, "bad magic: %x != %x", p.get(fMagic), magic)
}
// flags
if p.flags() > flAll {
return Errorf(InvalidFlagBitsSet, "invalid flag bits set (%x)", p.flags())
}
// if there's a midpoint timestamp, there should be nothing else
if p.hasMidpointStamp() && (p.hasReceiveStamp() || p.hasSendStamp()) {
return Errorf(NonexclusiveMidpointTStamp, "non-exclusive midpoint timestamp")
}
// clock mode should be consistent for both stamps
if p.hasReceiveStamp() && p.hasSendStamp() {
rclock := clockFromBools(p.isset(fRWall), p.isset(fRMono))
sclock := clockFromBools(p.isset(fSWall), p.isset(fSMono))
if sclock != rclock {
return Errorf(InconsistentClocks,
"inconsistent clock mode between send and receive timestamps, %s != %s",
sclock, rclock)
}
}
// validate HMAC
if p.md5Hash != nil {
if p.flags()&flHMAC == 0 {
return Errorf(NoHMAC, "no HMAC present")
}
p.addFields([]fidx{fHMAC}, false)
y := make([]byte, md5.Size)
copy(y[:], p.get(fHMAC))
p.zero(fHMAC)
p.md5Hash.Reset()
p.md5Hash.Write(p.bytes())
x := p.md5Hash.Sum(nil)
if !hmac.Equal(y, x) {
return Errorf(BadHMAC, "invalid HMAC: %x != %x", y, x)
}
} else if p.flags()&flHMAC != 0 {
return Errorf(UnexpectedHMAC, "unexpected HMAC present")
}
return nil
}
// flags
func (p *packet) flags() flags {
return flags(p.getb(fFlags))
}
func (p *packet) setFlagBits(f flags) {
p.setb(fFlags, byte(p.flags().set(f)))
}
func (p *packet) clearFlagBits(f flags) {
p.setb(fFlags, byte(p.flags().clear(f)))
}
// Reply
func (p *packet) reply() bool {
return p.flags()&flReply != 0
}
func (p *packet) setReply(r bool) {
if r {
p.setFlagBits(flReply)
} else {
p.clearFlagBits(flReply)
}
}
// Token
func (p *packet) ctoken() ctoken {
return ctoken(endian.Uint64(p.get(fConnToken)))
}
func (p *packet) setConnToken(ctoken ctoken) {
endian.PutUint64(p.setTo(fConnToken), uint64(ctoken))
}
// Sequence Number
func (p *packet) seqno() Seqno {
return Seqno(endian.Uint32(p.get(fSeqno)))
}
func (p *packet) setSeqno(seqno Seqno) {
endian.PutUint32(p.setTo(fSeqno), uint32(seqno))
}
// Received packet stats
func (p *packet) receivedCount() ReceivedCount {
return ReceivedCount(endian.Uint32(p.get(fRCount)))
}
func (p *packet) setReceivedCount(n ReceivedCount) {
endian.PutUint32(p.setTo(fRCount), uint32(n))
}
func (p *packet) receivedWindow() ReceivedWindow {
return ReceivedWindow(endian.Uint64(p.get(fRWindow)))
}
func (p *packet) setReceivedWindow(w ReceivedWindow) {
endian.PutUint64(p.setTo(fRWindow), uint64(w))
}
func (p *packet) hasReceivedCount() bool {
return p.isset(fRCount)
}
func (p *packet) hasReceivedWindow() bool {
return p.isset(fRWindow)
}
func (p *packet) zeroReceivedStats(rs ReceivedStats) {
if rs&ReceivedStatsCount != 0 {
p.zero(fRCount)
} else {
p.remove(fRCount)
}
if rs&ReceivedStatsWindow != 0 {
p.zero(fRWindow)
} else {
p.remove(fRWindow)
}
}
func (p *packet) addReceivedStatsFields(rs ReceivedStats) {
rfs := make([]fidx, 0, 2)
if rs&ReceivedStatsCount != 0 {
rfs = append(rfs, fRCount)
}
if rs&ReceivedStatsWindow != 0 {
rfs = append(rfs, fRWindow)
}
p.addFields(rfs, false)
}
// Timestamps
func (p *packet) tget(wf fidx, mf fidx, t *Time) {
wb := p.get(wf)
if len(wb) > 0 {
t.setWallFromBytes(wb)
}
mb := p.get(mf)
if len(mb) > 0 {
t.setMonoFromBytes(mb)
}
}
func (p *packet) timestamp() (ts Timestamp) {
p.tget(fRWall, fRMono, &ts.Receive)
p.tget(fMWall, fMMono, &ts.Receive)
p.tget(fMWall, fMMono, &ts.Send)
p.tget(fSWall, fSMono, &ts.Send)
return
}
func (p *packet) tset(t *Time, wf fidx, mf fidx) {
if t.Wall != 0 {
t.wallToBytes(p.setTo(wf))
}
if t.Mono != 0 {
t.monoToBytes(p.setTo(mf))
}
}
func (p *packet) setTimestamp(ts Timestamp) {
if ts.IsMidpoint() {
p.tset(&ts.Receive, fMWall, fMMono)
return
}
if !ts.Receive.IsZero() {
p.tset(&ts.Receive, fRWall, fRMono)
}
if !ts.Send.IsZero() {
p.tset(&ts.Send, fSWall, fSMono)
}
}
func (p *packet) hasReceiveStamp() bool {
return p.isset(fRWall) || p.isset(fRMono)
}
func (p *packet) hasMidpointStamp() bool {
return p.isset(fMWall) || p.isset(fMMono)
}
func (p *packet) hasSendStamp() bool {
return p.isset(fSWall) || p.isset(fSMono)
}
func (p *packet) clock() Clock {
c := Clock(0)
if p.isset(fRWall) || p.isset(fSWall) || p.isset(fMWall) {
c |= Wall
}
if p.isset(fRMono) || p.isset(fSMono) || p.isset(fMMono) {
c |= Monotonic
}
return c
}
func (p *packet) stampAt() (a StampAt) {
if p.isset(fMWall) || p.isset(fMMono) {
a = AtMidpoint
return
}
if p.isset(fRWall) || p.isset(fRMono) {
a |= AtReceive
}
if p.isset(fSWall) || p.isset(fSMono) {
a |= AtSend
}
return
}
func (p *packet) stampZeroes(at StampAt, c Clock) {
zts := func(a StampAt, wf fidx, mf fidx) {
if at&a != 0 {
if c&Wall != 0 {
p.zero(wf)
}
if c&Monotonic != 0 {
p.zero(mf)
}
} else {
p.remove(wf)
p.remove(mf)
}
}
zts(AtReceive, fRWall, fRMono)
zts(AtMidpoint, fMWall, fMMono)
zts(AtSend, fSWall, fSMono)
}
func (p *packet) addTimestampFields(at StampAt, c Clock) {
tfs := make([]fidx, 0, 4)
atf := func(a StampAt, wf fidx, mf fidx) {
if at&a != 0 {
if c&Wall != 0 {
tfs = append(tfs, wf)
}
if c&Monotonic != 0 {
tfs = append(tfs, mf)
}
}
}
atf(AtReceive, fRWall, fRMono)
atf(AtMidpoint, fMWall, fMMono)
atf(AtSend, fSWall, fSMono)
p.addFields(tfs, false)
}
func (p *packet) removeTimestamps() {
p.remove(fRWall)
p.remove(fRMono)
p.remove(fMWall)
p.remove(fMMono)
p.remove(fSWall)
p.remove(fSMono)
}
// HMAC
func (p *packet) updateHMAC() {
if p.md5Hash != nil {
// calculate and set hmac, with zeroed hmac field
p.setFlagBits(flHMAC)
p.zero(fHMAC)
p.md5Hash.Reset()
p.md5Hash.Write(p.buf)
mac := p.md5Hash.Sum(nil)
p.set(fHMAC, mac)
} else if p.isset(fHMAC) {
// clear field and flags
p.clearFlagBits(flHMAC)
p.remove(fHMAC)
}
}
// Payload
func (p *packet) readPayload(r io.Reader) (err error) {
payload := p.payload()
if len(payload) > 0 {
_, err = io.ReadFull(r, p.payload())
}
return err
}
irtt/sys_bsd.go 0000644 0001751 0001751 00000000475 13240047124 012516 0 ustar pete pete // +build openbsd freebsd
package irtt
import (
"net"
"golang.org/x/sys/unix"
)
func setSockoptDF(conn *net.UDPConn, df DF) error {
var value int
switch df {
case DFDefault:
value = 0
case DFTrue:
value = 1
case DFFalse:
value = 0
}
return setSockoptInt(conn, unix.IPPROTO_IP, unix.IP_DF, value)
}
irtt/result.go 0000644 0001751 0001751 00000020040 13240047124 012354 0 ustar pete pete package irtt
import (
"encoding/json"
"math"
"sort"
"time"
)
// Result is returned from Run.
type Result struct {
VersionInfo *VersionInfo `json:"version"`
SystemInfo *SystemInfo `json:"system_info"`
Config *ClientConfig `json:"config"`
SendErr error `json:"send_err,omitempty"`
ReceiveErr error `json:"receive_err,omitempty"`
*Stats `json:"stats"`
RoundTrips []RoundTrip `json:"round_trips"`
}
func newResult(rec *Recorder, cfg *ClientConfig, serr error, rerr error) *Result {
stats := &Stats{Recorder: rec}
r := &Result{
VersionInfo: NewVersionInfo(),
SystemInfo: NewSystemInfo(),
Config: cfg,
SendErr: serr,
ReceiveErr: rerr,
Stats: stats,
}
// calculate total duration
r.Duration = time.Since(r.Start)
// create RoundTrips array
r.RoundTrips = make([]RoundTrip, len(rec.RoundTripData))
for i := 0; i < len(r.RoundTrips); i++ {
rt := &r.RoundTrips[i]
rt.Seqno = Seqno(i)
rt.RoundTripData = &r.RoundTripData[i]
// use received window to update lost status of previous round trips
if rt.ReplyReceived() {
rt.Lost = LostFalse
rwin := rt.RoundTripData.receivedWindow
if cfg.Params.ReceivedStats&ReceivedStatsWindow != 0 && (rwin&0x1 != 0) {
rwin >>= 1
wend := i - 63
if wend < 0 {
wend = 0
}
for j := i - 1; j >= wend; j-- {
rcvd := (rwin&0x1 != 0)
prt := &r.RoundTrips[j]
if rcvd {
if prt.Lost != LostFalse {
prt.Lost = LostDown
}
} else if prt.Lost == LostTrue || prt.Lost == LostUp {
prt.Lost = LostUp
} // else don't allow a transition from not lost to lost
rwin >>= 1
}
}
}
// calculate IPDV
rt.IPDV = InvalidDuration
rt.SendIPDV = InvalidDuration
rt.ReceiveIPDV = InvalidDuration
if i > 0 {
rtp := &r.RoundTrips[i-1]
if rt.ReplyReceived() && rtp.ReplyReceived() {
rt.IPDV = rt.IPDVSince(rtp.RoundTripData)
rt.SendIPDV = rt.SendIPDVSince(rtp.RoundTripData)
rt.ReceiveIPDV = rt.ReceiveIPDVSince(rtp.RoundTripData)
}
}
}
// do median calculations (could figure out a rolling median one day)
r.visitStats(&r.RTTStats, false, func(rt *RoundTrip) time.Duration {
return rt.RTT()
})
r.visitStats(&r.SendDelayStats, false, func(rt *RoundTrip) time.Duration {
return rt.SendDelay()
})
r.visitStats(&r.ReceiveDelayStats, false, func(rt *RoundTrip) time.Duration {
return rt.ReceiveDelay()
})
// IPDV
r.visitStats(&r.RoundTripIPDVStats, true, func(rt *RoundTrip) time.Duration {
return AbsDuration(rt.IPDV)
})
// send IPDV
r.visitStats(&r.SendIPDVStats, true, func(rt *RoundTrip) time.Duration {
return AbsDuration(rt.SendIPDV)
})
// receive IPDV
r.visitStats(&r.ReceiveIPDVStats, true, func(rt *RoundTrip) time.Duration {
return AbsDuration(rt.ReceiveIPDV)
})
// calculate server processing time, if available
for _, rt := range rec.RoundTripData {
spt := rt.ServerProcessingTime()
if spt != InvalidDuration {
r.ServerProcessingTimeStats.push(spt)
}
}
// set packets sent and received
r.PacketsSent = r.SendCallStats.N
r.PacketsReceived = r.RTTStats.N + r.Duplicates
// calculate expected packets sent based on the time between the first and
// last send
r.ExpectedPacketsSent = pcount(r.LastSent.Sub(r.FirstSend), r.Config.Interval)
// calculate timer stats
r.TimerErrPercent = 100 * float64(r.TimerErrorStats.Mean()) / float64(r.Config.Interval)
// for some reason, occasionally one more packet is sent than expected, which
// wraps around the uint, so just punt and hard prevent this for now
if r.ExpectedPacketsSent < r.PacketsSent {
r.TimerMisses = 0
r.ExpectedPacketsSent = r.PacketsSent
} else {
r.TimerMisses = r.ExpectedPacketsSent - r.PacketsSent
}
r.TimerMissPercent = 100 * float64(r.TimerMisses) / float64(r.ExpectedPacketsSent)
// calculate send rate
r.SendRate = calculateBitrate(r.BytesSent, r.LastSent.Sub(r.FirstSend))
// calculate receive rate (start from time of first receipt)
r.ReceiveRate = calculateBitrate(r.BytesReceived, r.LastReceived.Sub(r.FirstReceived))
// calculate packet loss percent
if r.RTTStats.N > 0 {
r.PacketLossPercent = 100 * float64(r.SendCallStats.N-r.RTTStats.N) /
float64(r.SendCallStats.N)
} else {
r.PacketLossPercent = float64(100)
}
// calculate upstream and downstream loss percent
if r.ServerPacketsReceived > 0 {
r.UpstreamLossPercent = 100 *
float64(r.SendCallStats.N-uint(r.ServerPacketsReceived)) /
float64(r.SendCallStats.N)
r.DownstreamLossPercent = 100.0 *
float64(uint(r.ServerPacketsReceived)-r.PacketsReceived) /
float64(r.ServerPacketsReceived)
}
// calculate duplicate percent
if r.PacketsReceived > 0 {
r.DuplicatePercent = 100 * float64(r.Duplicates) / float64(r.PacketsReceived)
}
// calculate late packets percent
if r.PacketsReceived > 0 {
r.LatePacketsPercent = 100 * float64(r.LatePackets) / float64(r.PacketsReceived)
}
return r
}
// visitStats visits each RoundTrip, optionally pushes to a DurationStats, and
// at the end, sets the median value on the DurationStats.
func (r *Result) visitStats(ds *DurationStats, push bool,
fn func(*RoundTrip) time.Duration) {
fs := make([]float64, 0, len(r.RoundTrips))
for i := 0; i < len(r.RoundTrips); i++ {
d := fn(&r.RoundTrips[i])
if d != InvalidDuration {
if push {
ds.push(d)
}
fs = append(fs, float64(d))
}
}
if len(fs) > 0 {
ds.setMedian(median(fs))
}
}
// RoundTrip stores the Timestamps and statistics for a single round trip.
type RoundTrip struct {
Seqno Seqno `json:"seqno"`
Lost Lost `json:"lost"`
*RoundTripData `json:"timestamps"`
IPDV time.Duration `json:"-"`
SendIPDV time.Duration `json:"-"`
ReceiveIPDV time.Duration `json:"-"`
}
// MarshalJSON implements the json.Marshaler interface.
func (rt *RoundTrip) MarshalJSON() ([]byte, error) {
type Alias RoundTrip
delay := make(map[string]interface{})
if rt.RTT() != InvalidDuration {
delay["rtt"] = rt.RTT()
}
if rt.SendDelay() != InvalidDuration {
delay["send"] = rt.SendDelay()
}
if rt.ReceiveDelay() != InvalidDuration {
delay["receive"] = rt.ReceiveDelay()
}
ipdv := make(map[string]interface{})
if rt.IPDV != InvalidDuration {
ipdv["rtt"] = rt.IPDV
}
if rt.SendIPDV != InvalidDuration {
ipdv["send"] = rt.SendIPDV
}
if rt.ReceiveIPDV != InvalidDuration {
ipdv["receive"] = rt.ReceiveIPDV
}
j := &struct {
*Alias
Delay map[string]interface{} `json:"delay"`
IPDV map[string]interface{} `json:"ipdv"`
}{
Alias: (*Alias)(rt),
Delay: delay,
IPDV: ipdv,
}
return json.Marshal(j)
}
// Stats are the statistics in the Result.
type Stats struct {
*Recorder
Duration time.Duration `json:"duration"`
ExpectedPacketsSent uint `json:"-"`
PacketsSent uint `json:"packets_sent"`
PacketsReceived uint `json:"packets_received"`
PacketLossPercent float64 `json:"packet_loss_percent"`
UpstreamLossPercent float64 `json:"upstream_loss_percent"`
DownstreamLossPercent float64 `json:"downstream_loss_percent"`
DuplicatePercent float64 `json:"duplicate_percent"`
LatePacketsPercent float64 `json:"late_packets_percent"`
SendIPDVStats DurationStats `json:"ipdv_send"`
ReceiveIPDVStats DurationStats `json:"ipdv_receive"`
RoundTripIPDVStats DurationStats `json:"ipdv_round_trip"`
ServerProcessingTimeStats DurationStats `json:"server_processing_time"`
TimerErrPercent float64 `json:"timer_err_percent"`
TimerMisses uint `json:"timer_misses"`
TimerMissPercent float64 `json:"timer_miss_percent"`
SendRate Bitrate `json:"send_rate"`
ReceiveRate Bitrate `json:"receive_rate"`
}
// median calculates the median value of the supplied float64 slice. The array
// is sorted in place, so its original order is modified.
func median(f []float64) float64 {
sort.Float64s(f)
l := len(f)
if l == 0 {
return math.NaN()
}
if l%2 == 0 {
return (float64(f[l/2-1]) + float64(f[l/2])) / 2.0
}
return float64(f[l/2])
}
irtt/irtt_print.go 0000644 0001751 0001751 00000001341 13240047124 013237 0 ustar pete pete package irtt
import (
"bufio"
"fmt"
"io"
"os"
"text/tabwriter"
)
var printTo io.Writer = os.Stdout
type flusher interface {
Flush() error
}
func printf(format string, args ...interface{}) {
fmt.Fprintf(printTo, fmt.Sprintf("%s\n", format), args...)
}
func println(s string) {
fmt.Fprintln(printTo, s)
}
func setTabWriter(flags uint) {
printTo = tabwriter.NewWriter(printTo, 0, 0, 2, ' ', flags)
}
func setBufio() {
printTo = bufio.NewWriter(printTo)
}
func flush() {
if f, ok := printTo.(flusher); ok {
f.Flush()
}
}
func exitOnError(err error, code int) {
if err != nil {
printTo = os.Stderr
if _, ok := err.(*Error); ok {
printf("%s", err)
} else {
printf("Error: %s", err)
}
os.Exit(code)
}
}
irtt/irtt@.service 0000644 0001751 0001751 00000001110 13240047124 013150 0 ustar pete pete [Unit]
Description=irtt server bound to interface %i
After=network.target
BindsTo=sys-subsystem-net-devices-%i.device
After=sys-subsystem-net-devices-%i.device
Documentation=man:irtt(1)
Documentation=man:irtt-server(1)
[Service]
ExecStart=/usr/bin/irtt server -b %%%i
User=nobody
Restart=on-failure
# Sandboxing
# Some of these are not present in old versions of systemd.
# Comment out as appropriate.
PrivateTmp=yes
PrivateDevices=yes
ProtectControlGroups=yes
ProtectKernelTunables=yes
ProtectSystem=strict
ProtectHome=yes
NoNewPrivileges=yes
[Install]
WantedBy=multi-user.target
irtt/irtt_version.go 0000644 0001751 0001751 00000000445 13240047124 013574 0 ustar pete pete package irtt
import "runtime"
func runVersion(args []string) {
printf("irtt version: %s", Version)
printf("protocol version: %d", ProtocolVersion)
printf("json format version: %d", JSONFormatVersion)
printf("go version: %s on %s/%s", runtime.Version(),
runtime.GOOS, runtime.GOARCH)
}
irtt/gc.go 0000644 0001751 0001751 00000001133 13240047124 011431 0 ustar pete pete package irtt
import (
"fmt"
)
// GCMode selects when the garbage collector is run.
type GCMode int
// StampAt constants.
const (
GCOn GCMode = iota
GCOff
GCIdle
)
var gcms = [...]string{"on", "off", "idle"}
func (gm GCMode) String() string {
if int(gm) < 0 || int(gm) >= len(gcms) {
return fmt.Sprintf("GCMode:%d", gm)
}
return gcms[gm]
}
// ParseGCMode returns a GCMode value from its string.
func ParseGCMode(s string) (GCMode, error) {
for i, v := range gcms {
if v == s {
return GCMode(i), nil
}
}
return GCOn, Errorf(InvalidGCModeString, "invalid GC mode string: %s", s)
}
irtt/fbuf.go 0000644 0001751 0001751 00000012022 13240047124 011761 0 ustar pete pete package irtt
import "fmt"
// field
type field struct {
pos int
len int
cap int
}
// field index
type fidx int
// fbuf provides access to fields in a byte buffer, each with a position, length
// and capacity. Each field must have a length of either 0 or the field's
// capacity, so that the structure of the buffer can be externalized simply as
// which fields are set. tlen sets a target buffer length, and the payload is
// the padding after the fields needed to meet the target length. The length of
// the buffer must always be at least the length of the fields.
type fbuf struct {
// buffer
buf []byte
// fields
fields []field
// target length
tlen int
}
func newFbuf(fields []field, tlen int, cap int) *fbuf {
blen, fcap := sumFields(fields)
if tlen > blen {
blen = tlen
}
if fcap > cap {
cap = fcap
}
return &fbuf{make([]byte, blen, cap), fields, tlen}
}
func (fb *fbuf) validate() error {
flen, fcap := fb.sumFields()
if flen > len(fb.buf) {
return Errorf(FieldsLengthTooLarge,
"fields length exceeds buffer length, %d > %d", flen, len(fb.buf))
}
if fcap > cap(fb.buf) {
return Errorf(FieldsCapacityTooLarge,
"fields capacity exceeds buffer capacity, %d > %d", fcap, cap(fb.buf))
}
return nil
}
// setFields and addFields are used when changing fields for an existing buffer
func (fb *fbuf) setFields(fidxs []fidx, setLen bool) error {
pos := 0
j := 0
for i := 0; i < len(fidxs); i, j = i+1, j+1 {
for ; j < len(fb.fields); j++ {
if j == int(fidxs[i]) {
fb.fields[j].pos = pos
fb.fields[j].len = fb.fields[j].cap
pos += fb.fields[j].len
break
}
fb.fields[j].len = 0
fb.fields[j].pos = pos
}
}
for ; j < len(fb.fields); j++ {
fb.fields[j].len = 0
fb.fields[j].pos = pos
}
if setLen {
fb.setLen(pos)
}
return fb.validate()
}
func (fb *fbuf) addFields(fidxs []fidx, setLen bool) error {
pos := 0
j := 0
for i := 0; i < len(fidxs); i, j = i+1, j+1 {
for ; j < len(fb.fields); j++ {
if j == int(fidxs[i]) {
fb.fields[j].pos = pos
fb.fields[j].len = fb.fields[j].cap
pos += fb.fields[j].len
break
}
fb.fields[j].pos = pos
pos += fb.fields[j].len
}
}
for ; j < len(fb.fields); j++ {
fb.fields[j].pos = pos
pos += fb.fields[j].len
}
if setLen {
fb.setLen(pos)
}
return fb.validate()
}
// setters
func (fb *fbuf) set(f fidx, b []byte) {
p, l, c := fb.field(f)
if len(b) != c {
panic(fmt.Sprintf("set for field %d with size %d != field cap %d", f, len(b), c))
}
if l != c {
fb.setFieldLen(f, c)
}
copy(fb.buf[p:p+c], b)
}
func (fb *fbuf) setTo(f fidx) []byte {
p, l, c := fb.field(f)
if l != c {
fb.setFieldLen(f, c)
}
return fb.buf[p : p+c]
}
func (fb *fbuf) setb(f fidx, b byte) {
p, l, c := fb.field(f)
if c != 1 {
panic("setb only for one byte fields")
}
if l != 1 {
fb.setFieldLen(f, 1)
}
fb.buf[p] = b
}
func (fb *fbuf) setPayload(b []byte) {
flen := fb.sumLens()
fb.buf = fb.buf[:flen+len(b)]
copy(fb.buf[flen:], b)
}
func (fb *fbuf) zeroPayload() {
zero(fb.payload())
}
func (fb *fbuf) zero(f fidx) {
zero(fb.setTo(f))
}
// getters
func (fb *fbuf) get(f fidx) []byte {
p, l, _ := fb.field(f)
return fb.buf[p : p+l]
}
func (fb *fbuf) getb(f fidx) byte {
p, l, _ := fb.field(f)
if l != 1 {
panic(fmt.Sprintf("getb for non-byte field %d", f))
}
return fb.buf[p]
}
func (fb *fbuf) isset(f fidx) bool {
return fb.fields[f].len > 0
}
func (fb *fbuf) bytes() []byte {
return fb.buf
}
func (fb *fbuf) payload() []byte {
flen := fb.sumLens()
return fb.buf[flen:]
}
// length and capacity
func (fb *fbuf) length() int {
return len(fb.buf)
}
func (fb *fbuf) capacity() int {
return cap(fb.buf)
}
func (fb *fbuf) setLen(tlen int) int {
fb.tlen = tlen
flen := fb.sumLens()
l := tlen
if l < flen {
l = flen
}
if l > cap(fb.buf) {
l = cap(fb.buf)
}
fb.buf = fb.buf[:l]
return l
}
// removal
func (fb *fbuf) remove(f fidx) {
if fb.fields[f].len > 0 {
fb.setFieldLen(f, 0)
}
}
// internal methods
func (fb *fbuf) field(f fidx) (int, int, int) {
return fb.fields[f].pos, fb.fields[f].len, fb.fields[f].cap
}
func (fb *fbuf) setFieldLen(f fidx, newlen int) {
p, l, _ := fb.field(f)
grow := newlen - l
if grow != 0 {
// grow or shrink the buffer and shift bytes
//fmt.Printf("f=%d, newlen=%d, l=%d, len=%d, cap=%d, grow=%d\n",
// f, newlen, l, len(fb.buf), cap(fb.buf), grow)
fb.buf = fb.buf[:len(fb.buf)+grow]
copy(fb.buf[p+grow:], fb.buf[p:])
// update field length
fb.fields[f].len = newlen
// update field positions
newp := fb.fields[f].pos
for i := f; i < fidx(len(fb.fields)); i++ {
fb.fields[i].pos = newp
newp += fb.fields[i].len
}
// update total field length and reset to target length
flen := fb.sumLens()
if fb.tlen >= flen {
fb.buf = fb.buf[0:fb.tlen]
}
}
}
func (fb *fbuf) sumFields() (flen int, fcap int) {
return sumFields(fb.fields)
}
func (fb *fbuf) sumLens() (flen int) {
for _, f := range fb.fields {
flen += f.len
}
return
}
func sumFields(fields []field) (flen int, fcap int) {
for _, f := range fields {
flen += f.len
fcap += f.cap
}
return
}
irtt/sys_nodf.go 0000644 0001751 0001751 00000000266 13240047124 012672 0 ustar pete pete // +build !linux,!openbsd,!freebsd
package irtt
import (
"net"
)
func setSockoptDF(conn *net.UDPConn, df DF) error {
return Errorf(DFNotSupported, "DF sockopt not supported")
}
irtt/irtt_clock.go 0000644 0001751 0001751 00000001251 13240047124 013176 0 ustar pete pete package irtt
import (
"time"
)
func runClock(args []string) {
printf("Testing wall vs monotonic clocks...")
start := time.Now()
i := 0
for {
now := time.Now()
sinceStartMono := now.Sub(start)
sinceStartWall := now.Round(0).Sub(start)
wallMonoDiff := time.Duration(sinceStartWall - sinceStartMono)
driftPerSecond := time.Duration(float64(wallMonoDiff) * float64(1000000000) /
float64(sinceStartMono))
if i%10 == 0 {
printf("")
printf(" Monotonic Wall Wall-Monotonic Wall Drift / Second\t")
}
printf("%18s%18s%17s%22s",
sinceStartMono, sinceStartWall, wallMonoDiff, driftPerSecond)
time.Sleep(1 * time.Second)
i++
}
}
irtt/prof_off.go 0000644 0001751 0001751 00000000205 13240047124 012637 0 ustar pete pete // +build !profile
package irtt
const profileEnabled = false
func startProfile(path string) interface {
Stop()
} {
return nil
}
irtt/glob.go 0000644 0001751 0001751 00000001533 13240047124 011767 0 ustar pete pete package irtt
import (
"strings"
)
const globChar = "*"
func globAny(patterns []string, subj string) bool {
for _, p := range patterns {
if glob(p, subj) {
return true
}
}
return false
}
func glob(pattern, subj string) bool {
if pattern == "" {
return subj == pattern
}
if pattern == globChar {
return true
}
parts := strings.Split(pattern, globChar)
if len(parts) == 1 {
return subj == pattern
}
leadingGlob := strings.HasPrefix(pattern, globChar)
trailingGlob := strings.HasSuffix(pattern, globChar)
end := len(parts) - 1
for i := 0; i < end; i++ {
idx := strings.Index(subj, parts[i])
switch i {
case 0:
if !leadingGlob && idx != 0 {
return false
}
default:
if idx < 0 {
return false
}
}
subj = subj[idx+len(parts[i]):]
}
return trailingGlob || strings.HasSuffix(subj, parts[end])
}
irtt/sys_linux.go 0000644 0001751 0001751 00000000567 13240047124 013107 0 ustar pete pete // +build linux
package irtt
import (
"net"
"golang.org/x/sys/unix"
)
func setSockoptDF(conn *net.UDPConn, df DF) error {
var value int
switch df {
case DFDefault:
value = unix.IP_PMTUDISC_WANT
case DFTrue:
value = unix.IP_PMTUDISC_DO
case DFFalse:
value = unix.IP_PMTUDISC_DONT
}
return setSockoptInt(conn, unix.IPPROTO_IP, unix.IP_MTU_DISCOVER, value)
}
irtt/time.go 0000644 0001751 0001751 00000017270 13240047124 012007 0 ustar pete pete package irtt
import (
"encoding/json"
"fmt"
"math"
"strings"
"time"
)
var monotonicStart = time.Now()
// InvalidDuration indicates a duration that is not valid.
const InvalidDuration = time.Duration(math.MaxInt64)
// Durations contains a slice of time.Duration.
type Durations []time.Duration
func (ds Durations) String() string {
dss := make([]string, len(ds))
for i, d := range ds {
dss[i] = d.String()
}
return strings.Join(dss, ",")
}
// ParseDurations returns a Durations value from a comma separated list of
// time.Duration string representations.
func ParseDurations(sdurs string) (durs Durations, err error) {
ss := strings.Split(sdurs, ",")
durs = make([]time.Duration, len(ss))
for i, s := range ss {
var err error
durs[i], err = time.ParseDuration(s)
if err != nil {
return nil, err
}
}
return durs, nil
}
// Time contains both wall clock (subject to system time adjustments) and
// monotonic clock (relative to a fixed start time, and not subject to system
// time adjustments) times in nanoseconds. The monotonic value should be used
// for calculating time differences, and the wall value must be used for
// comparing wall clock time. Comparisons between wall clock values are only as
// accurate as the synchronization between the clocks that produced the values.
type Time struct {
Wall int64 `json:"wall,omitempty"`
Mono time.Duration `json:"monotonic,omitempty"`
}
func newTime(t time.Time, clock Clock) Time {
switch clock {
case Wall:
return Time{t.UnixNano(), time.Duration(0)}
case Monotonic:
return Time{0, t.Sub(monotonicStart)}
case BothClocks:
return Time{t.UnixNano(), t.Sub(monotonicStart)}
default:
panic(fmt.Sprintf("unknown clock %s", clock))
}
}
func (ts *Time) set(t time.Time) {
ts.Wall = t.UnixNano()
ts.Mono = t.Sub(monotonicStart)
}
// IsWallZero returns true if Wall is zero.
func (ts Time) IsWallZero() bool {
return ts.Wall == 0
}
// IsMonoZero returns true if Mono is zero.
func (ts Time) IsMonoZero() bool {
return ts.Mono == 0
}
// IsZero returns true if both Wall and Mono are zero.
func (ts Time) IsZero() bool {
return ts.IsWallZero() && ts.IsMonoZero()
}
// Timestamp stores receive and send times. If the Timestamp was set to the
// midpoint on the server, Receive and Send will be the same.
type Timestamp struct {
Receive Time `json:"receive"`
Send Time `json:"send"`
}
// IsMidpoint returns true if this Timestamp was made with the midpoint time
// (halfway between send and receive). If so, Send and Receive are both non-zero
// and the same.
func (t Timestamp) IsMidpoint() bool {
return !t.Receive.IsZero() && !t.Send.IsZero() && t.Receive == t.Send
}
// IsBothMono returns true if there are both send and receive times from the
// monotonic clock.
func (t Timestamp) IsBothMono() bool {
return !t.Receive.IsMonoZero() && !t.Send.IsMonoZero()
}
// IsBothWall returns true if there are both send and receive times from the
// wall clock.
func (t Timestamp) IsBothWall() bool {
return !t.Receive.IsWallZero() && !t.Send.IsWallZero()
}
// BestSend returns the best send time. It prefers the actual send time, but
// returns the receive time if it's not available.
func (t Timestamp) BestSend() Time {
if t.Send.IsZero() {
return t.Receive
}
return t.Send
}
// BestReceive returns the best receive time. It prefers the actual receive
// time, but returns the receive time if it's not available.
func (t Timestamp) BestReceive() Time {
if t.Receive.IsZero() {
return t.Send
}
return t.Receive
}
// StampAt selects the time/s when timestamps are made on the server.
type StampAt int
// StampAt constants.
const (
AtNone StampAt = 0x00
AtSend StampAt = 0x01
AtReceive StampAt = 0x02
AtBoth StampAt = AtSend | AtReceive
AtMidpoint StampAt = 0x04
)
var sas = [...]string{"none", "send", "receive", "both", "midpoint"}
func (sa StampAt) String() string {
if int(sa) < 0 || int(sa) >= len(sas) {
return fmt.Sprintf("StampAt:%d", sa)
}
return sas[sa]
}
// StampAtFromInt returns a StampAt value from its int constant.
func StampAtFromInt(v int) (StampAt, error) {
if v < int(AtNone) || v > int(AtMidpoint) {
return AtNone, Errorf(InvalidStampAtInt, "invalid StampAt int: %d", v)
}
return StampAt(v), nil
}
// MarshalJSON implements the json.Marshaler interface.
func (sa StampAt) MarshalJSON() ([]byte, error) {
return json.Marshal(sa.String())
}
// ParseStampAt returns a StampAt value from its string.
func ParseStampAt(s string) (StampAt, error) {
for i, v := range sas {
if v == s {
return StampAt(i), nil
}
}
return AtNone, Errorf(InvalidStampAtString, "invalid StampAt string: %s", s)
}
// Clock selects the clock/s to use for timestamps.
type Clock int
// Clock constants.
const (
Wall Clock = 0x01
Monotonic Clock = 0x02
BothClocks Clock = Wall | Monotonic
)
var tcs = [...]string{"wall", "monotonic", "both"}
func (tc Clock) String() string {
if int(tc) < 1 || int(tc) > len(tcs) {
return fmt.Sprintf("Clock:%d", tc)
}
return tcs[tc-1]
}
// MarshalJSON implements the json.Marshaler interface.
func (tc Clock) MarshalJSON() ([]byte, error) {
return json.Marshal(tc.String())
}
// ClockFromInt returns a Clock value from its int constant.
func ClockFromInt(v int) (Clock, error) {
if v < int(Wall) || v > int(BothClocks) {
return Clock(0), Errorf(InvalidClockInt, "invalid Clock int: %d", v)
}
return Clock(v), nil
}
// ParseClock returns a Clock from a string.
func ParseClock(s string) (Clock, error) {
for i, v := range tcs {
if s == v {
return Clock(i + 1), nil
}
}
return Clock(0), Errorf(InvalidClockString, "invalid Clock string: %s", s)
}
// clockFromBools returns a Clock for wall and monotonic booleans. Either w or m
// must be true.
func clockFromBools(w bool, m bool) Clock {
if w {
if m {
return BothClocks
}
return Wall
}
if m {
return Monotonic
}
panic(fmt.Sprintf("invalid clock booleans %t, %t", w, m))
}
// AllowStamp selects the timestamps that are allowed by the server.
type AllowStamp int
// AllowStamp constants.
const (
NoStamp AllowStamp = iota
SingleStamp
DualStamps
)
var als = [...]string{"none", "single", "dual"}
// Restrict returns the StampAt allowed for a given StampAt requested.
func (a AllowStamp) Restrict(at StampAt) StampAt {
if at == AtNone {
return AtNone
}
switch a {
case NoStamp:
return AtNone
case SingleStamp:
switch at {
case AtBoth:
return AtMidpoint
default:
return at
}
case DualStamps:
return at
default:
panic(fmt.Sprintf("unknown AllowStamp %d", a))
}
}
func (a AllowStamp) String() string {
if int(a) < 0 || int(a) >= len(als) {
return fmt.Sprintf("AllowStamp:%d", a)
}
return als[a]
}
// ParseAllowStamp returns an AllowStamp from a string.
func ParseAllowStamp(s string) (AllowStamp, error) {
for i, v := range als {
if s == v {
return AllowStamp(i), nil
}
}
return NoStamp, Errorf(InvalidAllowStampString, "invalid AllowStamp string: %s", s)
}
// midpoint returns the midpoint between two times.
func midpoint(t1 time.Time, t2 time.Time) time.Time {
// we'll live without nanosecond rounding here
return t1.Add(t2.Sub(t1) / 2)
}
// rdur rounds a Duration for improved readability.
func rdur(dur time.Duration) time.Duration {
d := dur
if d < 0 {
d = -d
}
if d < 1000 {
return dur
} else if d < 10000 {
return dur.Round(10 * time.Nanosecond)
} else if d < 100000 {
return dur.Round(100 * time.Nanosecond)
} else if d < 1000000 {
return dur.Round(1 * time.Microsecond)
} else if d < 100000000 {
return dur.Round(10 * time.Microsecond)
} else if d < 1000000000 {
return dur.Round(100 * time.Microsecond)
} else if d < 10000000000 {
return dur.Round(10 * time.Millisecond)
} else if d < 60000000000 {
return dur.Round(100 * time.Millisecond)
}
return dur.Round(time.Second)
}
irtt/recorder.go 0000644 0001751 0001751 00000033407 13240047124 012656 0 ustar pete pete package irtt
import (
"encoding/json"
"math"
"sync"
"time"
)
// Recorder is used to record data during the test. It is available to the
// Handler during the test for display of basic statistics, and may be used
// later to create a Result for further statistical analysis and storage.
// Recorder is accessed concurrently while the test is running, so its RLock and
// RUnlock methods must be used during read access to prevent race conditions.
// When RecorderHandler is called, it is already locked and must not be locked
// again. It is not possible to lock Recorder externally for write, since
// all recording should be done internally.
type Recorder struct {
Start time.Time `json:"start_time"`
FirstSend time.Time `json:"-"`
LastSent time.Time `json:"-"`
FirstReceived time.Time `json:"-"`
LastReceived time.Time `json:"-"`
SendCallStats DurationStats `json:"send_call"`
TimerErrorStats DurationStats `json:"timer_error"`
RTTStats DurationStats `json:"rtt"`
SendDelayStats DurationStats `json:"send_delay"`
ReceiveDelayStats DurationStats `json:"receive_delay"`
ServerPacketsReceived ReceivedCount `json:"server_packets_received"`
BytesSent uint64 `json:"bytes_sent"`
BytesReceived uint64 `json:"bytes_received"`
Duplicates uint `json:"duplicates"`
LatePackets uint `json:"late_packets"`
Wait time.Duration `json:"wait"`
RoundTripData []RoundTripData `json:"-"`
RecorderHandler RecorderHandler `json:"-"`
lastSeqno Seqno
mtx sync.RWMutex
}
// RLock locks the Recorder for reading.
func (r *Recorder) RLock() {
r.mtx.RLock()
}
// RUnlock unlocks the Recorder for reading.
func (r *Recorder) RUnlock() {
r.mtx.RUnlock()
}
func newRecorder(rtrips uint, h RecorderHandler) (rec *Recorder, err error) {
defer func() {
if r := recover(); r != nil {
err = Errorf(AllocateResultsPanic,
"failed to allocate results buffer for %d round trips (%s)",
rtrips, r)
}
}()
rec = &Recorder{
RoundTripData: make([]RoundTripData, 0, rtrips),
RecorderHandler: h,
}
return
}
func (r *Recorder) recordPreSend() time.Time {
r.mtx.Lock()
defer r.mtx.Unlock()
// add round trip before timestamp, so any re-allocation happens before the
// time is set
r.RoundTripData = append(r.RoundTripData, RoundTripData{})
tsend := time.Now()
r.RoundTripData[len(r.RoundTripData)-1].Client.Send.set(tsend)
return tsend
}
func (r *Recorder) removeLastStamps() {
r.mtx.Lock()
defer r.mtx.Unlock()
r.RoundTripData = r.RoundTripData[:len(r.RoundTripData)-1]
}
func (r *Recorder) recordPostSend(tsend time.Time, tsent time.Time, n uint64) {
r.mtx.Lock()
defer r.mtx.Unlock()
// add send call duration
r.SendCallStats.push(tsent.Sub(tsend))
// update bytes sent
r.BytesSent += n
// update send and sent times
if r.FirstSend.IsZero() {
r.FirstSend = tsend
}
r.LastSent = tsent
// call handler
if r.RecorderHandler != nil {
seqno := Seqno(len(r.RoundTripData)) - 1
r.RecorderHandler.OnSent(seqno, &r.RoundTripData[seqno])
}
}
func (r *Recorder) recordTimerErr(terr time.Duration) {
r.mtx.Lock()
defer r.mtx.Unlock()
r.TimerErrorStats.push(AbsDuration(terr))
}
func (r *Recorder) recordReceive(p *packet, sts *Timestamp) bool {
r.mtx.Lock()
defer r.mtx.Unlock()
// check for invalid sequence number
seqno := p.seqno()
if int(seqno) >= len(r.RoundTripData) {
return false
}
// valid result
rtd := &r.RoundTripData[seqno]
var prtd *RoundTripData
if seqno > 0 {
prtd = &r.RoundTripData[seqno-1]
}
// check for lateness
late := seqno < r.lastSeqno
// check for duplicate (don't update stats for duplicates)
if !rtd.Client.Receive.IsZero() {
r.Duplicates++
// call recorder handler
if r.RecorderHandler != nil {
r.RecorderHandler.OnReceived(p.seqno(), rtd, prtd, late, true)
}
return true
}
// record late packet
if late {
r.LatePackets++
}
r.lastSeqno = seqno
// update client received times
rtd.Client.Receive.set(p.trcvd)
// update RTT and RTT stats
rtd.Server = *sts
r.RTTStats.push(rtd.RTT())
// update one-way delay stats
if !rtd.Server.BestReceive().IsWallZero() {
r.SendDelayStats.push(rtd.SendDelay())
}
if !rtd.Server.BestSend().IsWallZero() {
r.ReceiveDelayStats.push(rtd.ReceiveDelay())
}
// set received times
if r.FirstReceived.IsZero() {
r.FirstReceived = p.trcvd
}
r.LastReceived = p.trcvd
// update server packets received
if p.hasReceivedCount() {
r.ServerPacketsReceived = p.receivedCount()
}
// set received window
if p.hasReceivedWindow() {
rtd.receivedWindow = p.receivedWindow()
}
// update bytes received
r.BytesReceived += uint64(p.length())
// call recorder handler
if r.RecorderHandler != nil {
r.RecorderHandler.OnReceived(p.seqno(), rtd, prtd, late, false)
}
return true
}
// RoundTripData contains the information recorded for each round trip during
// the test.
type RoundTripData struct {
Client Timestamp `json:"client"`
Server Timestamp `json:"server"`
receivedWindow ReceivedWindow
}
// ReplyReceived returns true if a reply was received from the server.
func (ts *RoundTripData) ReplyReceived() bool {
return !ts.Client.Receive.IsZero()
}
// RTT returns the round-trip time. The monotonic clock values are used
// for accuracy, and the server processing time is subtracted out if
// both send and receive timestamps are enabled.
func (ts *RoundTripData) RTT() (rtt time.Duration) {
if !ts.ReplyReceived() {
return InvalidDuration
}
rtt = ts.Client.Receive.Mono - ts.Client.Send.Mono
spt := ts.ServerProcessingTime()
if spt != InvalidDuration {
rtt -= ts.ServerProcessingTime()
}
return
}
// IPDVSince returns the instantaneous packet delay variation since the
// specified RoundTripData.
func (ts *RoundTripData) IPDVSince(pts *RoundTripData) time.Duration {
if !ts.ReplyReceived() || !pts.ReplyReceived() {
return InvalidDuration
}
return ts.RTT() - pts.RTT()
}
// SendIPDVSince returns the send instantaneous packet delay variation since the
// specified RoundTripData.
func (ts *RoundTripData) SendIPDVSince(pts *RoundTripData) (d time.Duration) {
d = InvalidDuration
if ts.IsTimestamped() && pts.IsTimestamped() {
if ts.IsMonoTimestamped() && pts.IsMonoTimestamped() {
d = ts.SendMonoDiff() - pts.SendMonoDiff()
} else if ts.IsWallTimestamped() && pts.IsWallTimestamped() {
d = ts.SendWallDiff() - pts.SendWallDiff()
}
}
return
}
// ReceiveIPDVSince returns the receive instantaneous packet delay variation
// since the specified RoundTripData.
func (ts *RoundTripData) ReceiveIPDVSince(pts *RoundTripData) (d time.Duration) {
d = InvalidDuration
if ts.IsTimestamped() && pts.IsTimestamped() {
if ts.IsMonoTimestamped() && pts.IsMonoTimestamped() {
d = ts.ReceiveMonoDiff() - pts.ReceiveMonoDiff()
} else if ts.IsWallTimestamped() && pts.IsWallTimestamped() {
d = ts.ReceiveWallDiff() - pts.ReceiveWallDiff()
}
}
return
}
// SendDelay returns the estimated one-way send delay, valid only if wall clock timestamps
// are available and the server's system time has been externally synchronized.
func (ts *RoundTripData) SendDelay() time.Duration {
if !ts.IsWallTimestamped() {
return InvalidDuration
}
return time.Duration(ts.Server.BestReceive().Wall - ts.Client.Send.Wall)
}
// ReceiveDelay returns the estimated one-way receive delay, valid only if wall
// clock timestamps are available and the server's system time has been
// externally synchronized.
func (ts *RoundTripData) ReceiveDelay() time.Duration {
if !ts.IsWallTimestamped() {
return InvalidDuration
}
return time.Duration(ts.Client.Receive.Wall - ts.Server.BestSend().Wall)
}
// SendMonoDiff returns the difference in send values from the monotonic clock.
// This is useful for measuring send IPDV (jitter), but not for absolute send delay.
func (ts *RoundTripData) SendMonoDiff() time.Duration {
return ts.Server.BestReceive().Mono - ts.Client.Send.Mono
}
// ReceiveMonoDiff returns the difference in receive values from the monotonic
// clock. This is useful for measuring receive IPDV (jitter), but not for
// absolute receive delay.
func (ts *RoundTripData) ReceiveMonoDiff() time.Duration {
return ts.Client.Receive.Mono - ts.Server.BestSend().Mono
}
// SendWallDiff returns the difference in send values from the wall
// clock. This is useful for measuring receive IPDV (jitter), but not for
// absolute send delay. Because the wall clock is used, it is subject to wall
// clock variability.
func (ts *RoundTripData) SendWallDiff() time.Duration {
return time.Duration(ts.Server.BestReceive().Wall - ts.Client.Send.Wall)
}
// ReceiveWallDiff returns the difference in receive values from the wall
// clock. This is useful for measuring receive IPDV (jitter), but not for
// absolute receive delay. Because the wall clock is used, it is subject to wall
// clock variability.
func (ts *RoundTripData) ReceiveWallDiff() time.Duration {
return time.Duration(ts.Client.Receive.Wall - ts.Server.BestSend().Wall)
}
// IsTimestamped returns true if the server returned any timestamp.
func (ts *RoundTripData) IsTimestamped() bool {
return ts.IsReceiveTimestamped() || ts.IsSendTimestamped()
}
// IsMonoTimestamped returns true if the server returned any timestamp with a
// valid monotonic clock value.
func (ts *RoundTripData) IsMonoTimestamped() bool {
return !ts.Server.Receive.IsMonoZero() || !ts.Server.Send.IsMonoZero()
}
// IsWallTimestamped returns true if the server returned any timestamp with a
// valid wall clock value.
func (ts *RoundTripData) IsWallTimestamped() bool {
return !ts.Server.Receive.IsWallZero() || !ts.Server.Send.IsWallZero()
}
// IsReceiveTimestamped returns true if the server returned a receive timestamp.
func (ts *RoundTripData) IsReceiveTimestamped() bool {
return !ts.Server.Receive.IsZero()
}
// IsSendTimestamped returns true if the server returned a send timestamp.
func (ts *RoundTripData) IsSendTimestamped() bool {
return !ts.Server.Send.IsZero()
}
// IsBothTimestamped returns true if the server returned both a send and receive
// timestamp.
func (ts *RoundTripData) IsBothTimestamped() bool {
return ts.IsReceiveTimestamped() && ts.IsSendTimestamped()
}
// ServerProcessingTime returns the amount of time between when the server
// received a request and when it sent its reply.
func (ts *RoundTripData) ServerProcessingTime() (d time.Duration) {
d = InvalidDuration
if ts.Server.IsMidpoint() {
return
}
if ts.Server.IsBothMono() {
d = time.Duration(ts.Server.Send.Mono - ts.Server.Receive.Mono)
} else if ts.Server.IsBothWall() {
d = time.Duration(ts.Server.Send.Wall - ts.Server.Receive.Wall)
}
return
}
// DurationStats keeps basic time.Duration statistics. Welford's method is used
// to keep a running mean and standard deviation. In testing, this seemed to be
// worth the extra muls and divs necessary to maintain these stats. Worst case,
// there was a 2% reduction in the send rate on a Raspberry Pi 2 when sending
// the smallest packets possible at the smallest interval possible. This is not
// a typical test, however, and the argument is, it's worth paying this price to
// add standard deviation and variance for timer error and send call time, and
// running standard deviation for all packet times.
type DurationStats struct {
Total time.Duration `json:"total"`
N uint `json:"n"`
Min time.Duration `json:"min"`
Max time.Duration `json:"max"`
m float64
s float64
mean float64
median float64
medianOk bool
}
func (s *DurationStats) push(d time.Duration) {
if s.N == 0 {
s.Min = d
s.Max = d
s.Total = d
} else {
if d < s.Min {
s.Min = d
}
if d > s.Max {
s.Max = d
}
s.Total += d
}
s.N++
om := s.mean
fd := float64(d)
s.mean += (fd - om) / float64(s.N)
s.s += (fd - om) * (fd - s.mean)
}
// IsZero returns true if DurationStats has no recorded values.
func (s *DurationStats) IsZero() bool {
return s.N == 0
}
// Mean returns the arithmetical mean.
func (s *DurationStats) Mean() time.Duration {
return time.Duration(s.mean)
}
// Variance returns the variance.
func (s *DurationStats) Variance() float64 {
if s.N > 1 {
return s.s / float64(s.N-1)
}
return 0.0
}
// Stddev returns the standard deviation.
func (s *DurationStats) Stddev() time.Duration {
return time.Duration(math.Sqrt(s.Variance()))
}
// Median returns the median (externally calculated).
func (s *DurationStats) Median() (dur time.Duration, ok bool) {
ok = s.medianOk
dur = time.Duration(s.median)
return
}
func (s *DurationStats) setMedian(m float64) {
s.median = m
s.medianOk = true
}
// MarshalJSON implements the json.Marshaler interface.
func (s *DurationStats) MarshalJSON() ([]byte, error) {
type Alias DurationStats
j := &struct {
*Alias
Mean time.Duration `json:"mean"`
Median time.Duration `json:"median,omitempty"`
Stddev time.Duration `json:"stddev"`
Variance time.Duration `json:"variance"`
}{
Alias: (*Alias)(s),
Mean: s.Mean(),
Stddev: s.Stddev(),
Variance: time.Duration(s.Variance()),
}
if m, ok := s.Median(); ok {
j.Median = m
}
return json.Marshal(j)
}
// AbsDuration returns the absolute value of a duration.
func AbsDuration(d time.Duration) time.Duration {
if d > 0 {
return d
}
return time.Duration(-d)
}
// pcount returns the number of packets that should be sent for a given
// duration and interval.
func pcount(d time.Duration, i time.Duration) uint {
return 1 + uint(d/i)
}
// RecorderHandler is called when the Recorder records a sent or received
// packet.
type RecorderHandler interface {
// OnSent is called when a packet is sent.
OnSent(seqno Seqno, rtd *RoundTripData)
// OnReceived is called when a packet is received.
OnReceived(seqno Seqno, rtd *RoundTripData, pred *RoundTripData, late bool, dup bool)
}
irtt/irtt.go 0000644 0001751 0001751 00000004421 13240047124 012025 0 ustar pete pete package irtt
import "os"
const exitCodeSuccess = 0
const exitCodeRuntimeError = 1
const exitCodeBadCommandLine = 2
const exitCodeDoubleSignal = 3
const defaultQuiet = false
const defaultReallyQuiet = false
const defaultHMACKey = ""
type command struct {
name string
desc string
run func([]string)
usage func()
}
var commands []command
func registerCommand(name, desc string, run func([]string), usage func()) {
commands = append(commands, command{name, desc, run, usage})
}
func getCommand(name string) *command {
for _, c := range commands {
if c.name == name {
return &c
}
}
return nil
}
func init() {
registerCommand("client", "runs the client", runClientCLI, clientUsage)
registerCommand("server", "runs the server", runServerCLI, serverUsage)
registerCommand("bench", "runs HMAC and fill benchmarks", runBench, nil)
registerCommand("clock", "runs wall vs monotonic clock test", runClock, nil)
registerCommand("sleep", "runs sleep accuracy test", runSleep, nil)
registerCommand("version", "shows the version", runVersion, nil)
}
// RunCLI runs the command line interface with the given arguments (typically
// os.Args).
func RunCLI(args []string) {
if len(args) < 2 {
usageAndExit(cliUsage, exitCodeBadCommandLine)
}
unknownCommandAndExit := func(cmd string) {
printf("Error: unknown command %s\n", cmd)
usageAndExit(cliUsage, exitCodeBadCommandLine)
}
if args[1] == "help" {
if len(args) < 3 {
usageAndExit(cliUsage, exitCodeBadCommandLine)
}
c := getCommand(args[2])
if c == nil {
unknownCommandAndExit(args[2])
}
if c.usage == nil {
printf("%s: %s", c.name, c.desc)
os.Exit(exitCodeSuccess)
}
usageAndExit(c.usage, exitCodeSuccess)
}
c := getCommand(args[1])
if c == nil {
unknownCommandAndExit(args[1])
}
c.run(args[2:])
}
func cliUsage() {
setTabWriter(0)
printf("irtt: measures round-trip time with isochronous UDP packets")
printf("")
printf("Usage:")
printf("")
printf("\t\t\t\tirtt command [arguments]")
printf("\t\t\t\tirtt help command")
printf("")
printf("Commands:")
printf("")
for _, c := range commands {
printf("\t\t\t\t%s\t%s\t", c.name, c.desc)
}
}
func usageAndExit(usage func(), exitCode int) {
if exitCode != exitCodeSuccess {
printTo = os.Stderr
}
usage()
flush()
os.Exit(exitCode)
}
irtt/irtt.service 0000644 0001751 0001751 00000000724 13240047124 013062 0 ustar pete pete [Unit]
Description=irtt server
After=network.target
Documentation=man:irtt(1)
Documentation=man:irtt-server(1)
[Service]
ExecStart=/usr/bin/irtt server
User=nobody
Restart=on-failure
# Sandboxing
# Some of these are not present in old versions of systemd.
# Comment out as appropriate.
PrivateTmp=yes
PrivateDevices=yes
ProtectControlGroups=yes
ProtectKernelTunables=yes
ProtectSystem=strict
ProtectHome=yes
NoNewPrivileges=yes
[Install]
WantedBy=multi-user.target
irtt/irtt_sleep.go 0000644 0001751 0001751 00000001741 13240047124 013217 0 ustar pete pete package irtt
import (
"time"
)
func runSleep(args []string) {
printf("Testing sleep accuracy...")
printf("")
durations := []time.Duration{1 * time.Nanosecond,
10 * time.Nanosecond,
100 * time.Nanosecond,
1 * time.Microsecond,
10 * time.Microsecond,
100 * time.Microsecond,
1 * time.Millisecond,
10 * time.Millisecond,
100 * time.Millisecond,
200 * time.Millisecond,
500 * time.Millisecond,
}
printf("Sleep Duration Mean Error %% Error")
for _, d := range durations {
iterations := int(2 * time.Second / d)
if iterations < 5 {
iterations = 5
}
errTotal := time.Duration(0)
start0 := time.Now()
i := 0
for ; i < iterations && time.Since(start0) < 2*time.Second; i++ {
start := time.Now()
time.Sleep(d)
elapsed := time.Since(start)
errTotal += (elapsed - d)
}
errorNs := float64(errTotal) / float64(i)
percentError := 100 * errorNs / float64(d)
printf("%14s%18s%14.1f", d, time.Duration(errorNs), percentError)
}
}
irtt/rstats.go 0000644 0001751 0001751 00000003515 13240047124 012366 0 ustar pete pete package irtt
import (
"encoding/json"
"fmt"
)
// ReceivedStats selects what information to gather about received packets.
type ReceivedStats int
// ReceivedStats constants.
const (
ReceivedStatsNone ReceivedStats = 0x00
ReceivedStatsCount ReceivedStats = 0x01
ReceivedStatsWindow ReceivedStats = 0x02
ReceivedStatsBoth ReceivedStats = ReceivedStatsCount | ReceivedStatsWindow
)
var rss = [...]string{"none", "count", "window", "both"}
func (rs ReceivedStats) String() string {
if int(rs) < 0 || int(rs) >= len(rss) {
return fmt.Sprintf("ReceivedStats:%d", rs)
}
return rss[rs]
}
// ReceivedStatsFromInt returns a ReceivedStats value from its int constant.
func ReceivedStatsFromInt(v int) (ReceivedStats, error) {
if v < int(ReceivedStatsNone) || v > int(ReceivedStatsBoth) {
return ReceivedStatsNone, Errorf(InvalidReceivedStatsInt,
"invalid ReceivedStats int: %d", v)
}
return ReceivedStats(v), nil
}
// MarshalJSON implements the json.Marshaler interface.
func (rs ReceivedStats) MarshalJSON() ([]byte, error) {
return json.Marshal(rs.String())
}
// ParseReceivedStats returns a ReceivedStats value from its string.
func ParseReceivedStats(s string) (ReceivedStats, error) {
for i, v := range rss {
if v == s {
return ReceivedStats(i), nil
}
}
return ReceivedStatsNone, Errorf(InvalidReceivedStatsString,
"invalid ReceivedStats string: %s", s)
}
// Lost indicates the lost status of a packet.
type Lost int
// Lost constants.
const (
LostTrue Lost = iota
LostDown
LostUp
LostFalse
)
var lsts = [...]string{"true", "true_down", "true_up", "false"}
func (l Lost) String() string {
if int(l) < 0 || int(l) >= len(lsts) {
return fmt.Sprintf("Lost:%d", l)
}
return lsts[l]
}
// MarshalJSON implements the json.Marshaler interface.
func (l Lost) MarshalJSON() ([]byte, error) {
return json.Marshal(l.String())
}
irtt/sconn.go 0000644 0001751 0001751 00000015320 13240047124 012163 0 ustar pete pete package irtt
import (
"math/rand"
"net"
"time"
)
// sconn stores the state for a client's connection to the server
type sconn struct {
*listener
ctoken ctoken
raddr *net.UDPAddr
params *Params
filler Filler
created time.Time
firstUsed time.Time
lastUsed time.Time
packetBucket float64
lastSeqno Seqno
receivedCount ReceivedCount
receivedWindow ReceivedWindow
rwinValid bool
bytes uint64
}
func newSconn(l *listener, raddr *net.UDPAddr) *sconn {
return &sconn{
listener: l,
raddr: raddr,
filler: l.Filler,
created: time.Now(),
lastSeqno: InvalidSeqno,
packetBucket: float64(l.PacketBurst),
}
}
func accept(l *listener, p *packet) (sc *sconn, err error) {
// create sconn
sc = newSconn(l, p.raddr)
// parse, restrict and set params
var params *Params
params, err = parseParams(p.payload())
if err != nil {
return
}
sc.restrictParams(params)
sc.params = params
// set filler
if len(sc.params.ServerFill) > 0 &&
sc.params.ServerFill != DefaultServerFiller.String() {
sc.filler, err = NewFiller(sc.params.ServerFill)
if err != nil {
l.eventf(InvalidServerFill, p.raddr,
"invalid server fill %s requested, defaulting to %s (%s)",
sc.params.ServerFill, DefaultServerFiller.String(), err.Error())
sc.filler = l.Filler
sc.params.ServerFill = DefaultServerFiller.String()
}
}
// determine state of connection
if params.ProtocolVersion != ProtocolVersion {
l.eventf(ProtocolVersionMismatch, p.raddr,
"close connection, client version %d != server version %d",
params.ProtocolVersion, ProtocolVersion)
p.setFlagBits(flClose)
} else if p.flags()&flClose != 0 {
l.eventf(OpenClose, p.raddr, "open-close connection")
} else {
l.cmgr.put(sc)
l.eventf(NewConn, p.raddr, "new connection, token=%016x", sc.ctoken)
}
// prepare and send open reply
if sc.SetSrcIP {
p.srcIP = p.dstIP
}
p.setConnToken(sc.ctoken)
p.setReply(true)
p.setPayload(params.bytes())
err = l.conn.send(p)
return
}
func (sc *sconn) serve(p *packet) (closed bool, err error) {
if !udpAddrsEqual(p.raddr, sc.raddr) {
err = Errorf(AddressMismatch, "address mismatch (expected %s for %016x)",
sc.raddr, p.ctoken())
return
}
if p.flags()&flClose != 0 {
closed = true
err = sc.serveClose(p)
return
}
closed, err = sc.serveEcho(p)
return
}
func (sc *sconn) serveClose(p *packet) (err error) {
if err = p.addFields(fcloseRequest, false); err != nil {
return
}
sc.eventf(CloseConn, p.raddr, "close connection, token=%016x", sc.ctoken)
if scr := sc.cmgr.remove(sc.ctoken); scr == nil {
sc.eventf(RemoveNoConn, p.raddr,
"sconn not in connmgr, token=%016x", sc.ctoken)
}
return
}
func (sc *sconn) serveEcho(p *packet) (closed bool, err error) {
// handle echo request
if err = p.addFields(fechoRequest, false); err != nil {
return
}
// check that request isn't too large
if sc.MaxLength > 0 && p.length() > sc.MaxLength {
err = Errorf(LargeRequest, "request too large (%d > %d)",
p.length(), sc.MaxLength)
return
}
// update first used
now := time.Now()
if sc.firstUsed.IsZero() {
sc.firstUsed = now
}
// enforce minimum interval
if sc.MinInterval > 0 {
if !sc.lastUsed.IsZero() {
earned := float64(now.Sub(sc.lastUsed)) / float64(sc.MinInterval)
sc.packetBucket += earned
if sc.packetBucket > float64(sc.PacketBurst) {
sc.packetBucket = float64(sc.PacketBurst)
}
}
if sc.packetBucket < 1 {
sc.lastUsed = now
err = Errorf(ShortInterval, "drop due to short packet interval")
return
}
sc.packetBucket--
}
// set reply flag
p.setReply(true)
// update last used
sc.lastUsed = now
// slide received seqno window
seqno := p.seqno()
sinceLastSeqno := seqno - sc.lastSeqno
if sinceLastSeqno > 0 {
sc.receivedWindow <<= sinceLastSeqno
}
if sinceLastSeqno >= 0 { // new, duplicate or first packet
sc.receivedWindow |= 0x1
sc.rwinValid = true
} else { // late packet
sc.receivedWindow |= (0x1 << -sinceLastSeqno)
sc.rwinValid = false
}
// update received count
sc.receivedCount++
// update seqno and last used times
sc.lastSeqno = seqno
// check if max test duration exceeded (but still return packet)
if sc.MaxDuration > 0 && time.Since(sc.firstUsed) >
sc.MaxDuration+maxDurationGrace {
sc.eventf(ExceededDuration, p.raddr,
"closing connection due to duration limit exceeded")
sc.cmgr.remove(sc.ctoken)
p.setFlagBits(flClose)
closed = true
}
// set packet dscp value
if sc.AllowDSCP && sc.conn.dscpSupport {
p.dscp = sc.params.DSCP
}
// set source IP, if necessary
if sc.SetSrcIP {
p.srcIP = p.dstIP
}
// initialize test packet
p.setLen(0)
// set received stats
if sc.params.ReceivedStats&ReceivedStatsCount != 0 {
p.setReceivedCount(sc.receivedCount)
}
if sc.params.ReceivedStats&ReceivedStatsWindow != 0 {
if sc.rwinValid {
p.setReceivedWindow(sc.receivedWindow)
} else {
p.setReceivedWindow(0)
}
}
// set timestamps
at := sc.params.StampAt
cl := sc.params.Clock
if at != AtNone {
var rt Time
var st Time
if at == AtMidpoint {
mt := midpoint(p.trcvd, time.Now())
rt = newTime(mt, cl)
st = newTime(mt, cl)
} else {
if at&AtReceive != 0 {
rt = newTime(p.trcvd, cl)
}
if at&AtSend != 0 {
st = newTime(time.Now(), cl)
}
}
p.setTimestamp(Timestamp{rt, st})
} else {
p.removeTimestamps()
}
// set length
p.setLen(sc.params.Length)
// fill payload
if sc.filler != nil {
if err = p.readPayload(sc.filler); err != nil {
return
}
}
// simulate dropped packets, if necessary
if serverDropsPercent > 0 && rand.Float32() < serverDropsPercent {
return
}
// simulate duplicates, if necessary
if serverDupsPercent > 0 {
for rand.Float32() < serverDupsPercent {
if err = sc.conn.send(p); err != nil {
return
}
}
}
// send reply
err = sc.conn.send(p)
return
}
func (sc *sconn) expired() bool {
if sc.Timeout == 0 {
return false
}
return !sc.lastUsed.IsZero() &&
time.Since(sc.lastUsed) > sc.Timeout+timeoutGrace
}
func (sc *sconn) restrictParams(p *Params) {
if p.ProtocolVersion != ProtocolVersion {
p.ProtocolVersion = ProtocolVersion
}
if sc.MaxDuration > 0 && p.Duration > sc.MaxDuration {
p.Duration = sc.MaxDuration
}
if sc.MinInterval > 0 && p.Interval < sc.MinInterval {
p.Interval = sc.MinInterval
}
if sc.Timeout > 0 && p.Interval > sc.Timeout/maxIntervalTimeoutFactor {
p.Interval = sc.Timeout / maxIntervalTimeoutFactor
}
if sc.MaxLength > 0 && p.Length > sc.MaxLength {
p.Length = sc.MaxLength
}
p.StampAt = sc.AllowStamp.Restrict(p.StampAt)
if !sc.AllowDSCP || !sc.conn.dscpSupport {
p.DSCP = 0
}
if len(p.ServerFill) > 0 && !globAny(sc.AllowFills, p.ServerFill) {
p.ServerFill = DefaultServerFiller.String()
}
return
}
irtt/rand.go 0000644 0001751 0001751 00000000141 13240047124 011762 0 ustar pete pete package irtt
import (
"math/rand"
"time"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
irtt/code_string.go 0000644 0001751 0001751 00000005072 13240047124 013346 0 ustar pete pete // Code generated by "stringer -type=Code"; DO NOT EDIT.
package irtt
import "strconv"
const (
_Code_name_0 = "ServerFillTooLongOpenTimeoutTooShortInvalidReceivedStatsStringInvalidReceivedStatsIntInvalidServerRestrictionOpenTimeoutServerClosedConnTokenZeroDurationNonPositiveIntervalNonPositiveNoSuchWaiterNoSuchTimerNoSuchFillerNoSuchAveragerInvalidWaitDurationInvalidWaitFactorInvalidWaitStringInvalidSleepFactorUnexpectedSequenceNumberClockMismatchStampAtMismatchShortReplyExpectedReplyFlagTTLErrorDFErrorUnexpectedOpenFlagAllocateResultsPanicInvalidExpAvgAlphaInvalidWinAvgWindow"
_Code_name_1 = "AddressMismatchLargeRequestShortIntervalInvalidConnTokenNoSuitableAddressFoundUnexpectedReplyFlagInvalidGCModeStringUnspecifiedWithSpecifiedAddressesNoMatchingInterfacesUpNoMatchingInterfaces"
_Code_name_2 = "ProtocolVersionMismatchInvalidParamValueParamOverflowShortParamBufferInvalidFlagBitsSetDFNotSupportedInconsistentClocksNonexclusiveMidpointTStampUnexpectedHMACBadHMACNoHMACBadMagicInvalidClockIntInvalidClockStringInvalidAllowStampStringInvalidStampAtIntInvalidStampAtStringFieldsCapacityTooLargeFieldsLengthTooLargeInvalidDFStringShortWrite"
_Code_name_3 = "MultipleAddressesServerStartServerStopListenerStartListenerStopListenerErrorDropNewConnOpenCloseCloseConnNoDSCPSupportExceededDurationNoReceiveDstAddrSupportRemoveNoConnInvalidServerFill"
_Code_name_4 = "ConnectingConnectedWaitForPacketsServerRestrictionNoTestConnectedClosed"
)
var (
_Code_index_0 = [...]uint16{0, 17, 36, 62, 85, 109, 120, 132, 145, 164, 183, 195, 206, 218, 232, 251, 268, 285, 303, 327, 340, 355, 365, 382, 390, 397, 415, 435, 453, 472}
_Code_index_1 = [...]uint8{0, 15, 27, 40, 56, 78, 97, 116, 149, 171, 191}
_Code_index_2 = [...]uint16{0, 23, 40, 53, 69, 87, 101, 119, 145, 159, 166, 172, 180, 195, 213, 236, 253, 273, 295, 315, 330, 340}
_Code_index_3 = [...]uint8{0, 17, 28, 38, 51, 63, 76, 80, 87, 96, 105, 118, 134, 157, 169, 186}
_Code_index_4 = [...]uint8{0, 10, 19, 33, 50, 56, 71}
)
func (i Code) String() string {
switch {
case -2076 <= i && i <= -2048:
i -= -2076
return _Code_name_0[_Code_index_0[i]:_Code_index_0[i+1]]
case -1033 <= i && i <= -1024:
i -= -1033
return _Code_name_1[_Code_index_1[i]:_Code_index_1[i+1]]
case -21 <= i && i <= -1:
i -= -21
return _Code_name_2[_Code_index_2[i]:_Code_index_2[i+1]]
case 1024 <= i && i <= 1038:
i -= 1024
return _Code_name_3[_Code_index_3[i]:_Code_index_3[i+1]]
case 2048 <= i && i <= 2053:
i -= 2048
return _Code_name_4[_Code_index_4[i]:_Code_index_4[i+1]]
default:
return "Code(" + strconv.FormatInt(int64(i), 10) + ")"
}
}
irtt/sysinfo.go 0000644 0001751 0001751 00000000747 13240047124 012544 0 ustar pete pete package irtt
import (
"os"
"runtime"
)
// SystemInfo stores based system information.
type SystemInfo struct {
OS string `json:"os"`
NumCPU int `json:"cpus"`
GoVersion string `json:"go_version"`
Hostname string `json:"hostname"`
}
// NewSystemInfo returns a new SystemInfo.
func NewSystemInfo() *SystemInfo {
s := &SystemInfo{
OS: runtime.GOOS,
NumCPU: runtime.NumCPU(),
GoVersion: runtime.Version(),
}
s.Hostname, _ = os.Hostname()
return s
}
irtt/net.go 0000644 0001751 0001751 00000005335 13240047124 011636 0 ustar pete pete package irtt
import (
"encoding/json"
"fmt"
"net"
)
// IPVersion is an IP version, or dual stack for IPv4 and IPv6.
type IPVersion int
// IPVersion constants.
const (
IPv4 IPVersion = 1 << iota
IPv6
DualStack = IPv4 | IPv6
)
// IPVersionFromBooleans returns an IPVersion from booleans. If both ipv4 and
// ipv6 are true, DualStack is returned. If neither are true, the value of dfl
// is returned.
func IPVersionFromBooleans(ipv4 bool, ipv6 bool, dfl IPVersion) IPVersion {
if ipv4 {
if ipv6 {
return DualStack
}
return IPv4
}
if ipv6 {
return IPv6
}
return dfl
}
// IPVersionFromIP returns an IPVersion from a net.IP.
func IPVersionFromIP(ip net.IP) IPVersion {
if ip.To4() != nil {
return IPv4
}
return IPv6
}
// IPVersionFromUDPAddr returns an IPVersion from a net.UDPAddr.
func IPVersionFromUDPAddr(addr *net.UDPAddr) IPVersion {
return IPVersionFromIP(addr.IP)
}
var udpNets = [...]string{"udp4", "udp6", "udp"}
func (v IPVersion) udpNetwork() string {
if int(v-1) < 0 || int(v-1) > len(udpNets) {
return fmt.Sprintf("IPVersion.udpNetwork:%d", v)
}
return udpNets[v-1]
}
// 28 == 20 (min IPv4 header) + 8 (UDP header)
// 48 == 40 (min IPv4 header) + 8 (UDP header)
var muhs = [...]int{28, 48, 28}
func (v IPVersion) minUDPHeaderSize() int {
return muhs[v-1]
}
var ipvs = [...]string{"IPv4", "IPv6", "IPv4+6"}
func (v IPVersion) String() string {
if int(v-1) < 0 || int(v-1) > len(ipvs) {
return fmt.Sprintf("IPVersion:%d", v)
}
return ipvs[v-1]
}
var ipvi = [...]int{4, 6, 46}
// Separate returns a slice of IPVersions, separating DualStack into IPv4 and
// IPv6 if necessary.
func (v IPVersion) Separate() []IPVersion {
if v == IPv4 {
return []IPVersion{IPv4}
}
if v == IPv6 {
return []IPVersion{IPv6}
}
return []IPVersion{IPv4, IPv6}
}
// ZeroIP returns the zero IP for the IPVersion (net.IPv4zero for IPv4 and
// otherwise net.IPv6zero).
func (v IPVersion) ZeroIP() net.IP {
if v == IPv4 {
return net.IPv4zero
}
return net.IPv6zero
}
// MarshalJSON implements the json.Marshaler interface.
func (v IPVersion) MarshalJSON() ([]byte, error) {
return json.Marshal(v.String())
}
// addPort adds the default port to a string, if the string does not
// already contain a port.
func addPort(hostport, port string) string {
if _, _, err := net.SplitHostPort(hostport); err != nil {
// JoinHostPort doesn't seem to work with IPv6 addresses with [], so I
// join manually.
return fmt.Sprintf("%s:%s", hostport, port)
}
return hostport
}
// udpAddrsEqual returns true if all fields of the passed in UDP addresses are
// equal.
func udpAddrsEqual(a1 *net.UDPAddr, a2 *net.UDPAddr) bool {
if !a1.IP.Equal(a2.IP) {
return false
}
if a1.Port != a2.Port {
return false
}
return a1.Zone == a2.Zone
}
irtt/waiter.go 0000644 0001751 0001751 00000007043 13240047124 012341 0 ustar pete pete package irtt
import (
"fmt"
"strconv"
"strings"
"time"
)
// Waiter is implemented to return a wait time for final replies. See the
// documentation for Recorder for information on locking for concurrent access.
type Waiter interface {
// Wait returns the wait duration.
Wait(r *Recorder) time.Duration
String() string
}
// WaitDuration waits for a specific period of time.
type WaitDuration struct {
D time.Duration `json:"d"`
}
// Wait returns the wait duration.
func (w *WaitDuration) Wait(r *Recorder) time.Duration {
return w.D
}
func (w *WaitDuration) String() string {
return w.D.String()
}
// WaitMaxRTT waits for a factor of the maximum RTT
type WaitMaxRTT struct {
D time.Duration `json:"d"`
Factor int `json:"factor"`
}
// Wait returns the wait duration.
func (w *WaitMaxRTT) Wait(r *Recorder) time.Duration {
r.RLock()
defer r.RUnlock()
if r.RTTStats.N == 0 {
return w.D
}
return time.Duration(w.Factor) * r.RTTStats.Max
}
func (w *WaitMaxRTT) String() string {
return fmt.Sprintf("%dx%s", w.Factor, w.D)
}
// WaitMeanRTT waits for a factor of the mean RTT.
type WaitMeanRTT struct {
D time.Duration `json:"d"`
Factor int `json:"factor"`
}
// Wait returns the wait duration.
func (w *WaitMeanRTT) Wait(r *Recorder) time.Duration {
r.RLock()
defer r.RUnlock()
if r.RTTStats.N == 0 {
return w.D
}
return time.Duration(w.Factor) * r.RTTStats.Mean()
}
func (w *WaitMeanRTT) String() string {
return fmt.Sprintf("%dr%s", w.Factor, w.D)
}
// WaiterFactories are the registered Waiter factories.
var WaiterFactories = make([]WaiterFactory, 0)
// WaiterFactory can create a Waiter from a string.
type WaiterFactory struct {
FactoryFunc func(string) (Waiter, error)
Usage string
}
// RegisterWaiter registers a new Waiter.
func RegisterWaiter(fn func(string) (Waiter, error), usage string) {
WaiterFactories = append(WaiterFactories, WaiterFactory{fn, usage})
}
// NewWaiter returns a Waiter from a string.
func NewWaiter(s string) (Waiter, error) {
for _, fac := range WaiterFactories {
t, err := fac.FactoryFunc(s)
if err != nil {
return nil, err
}
if t != nil {
return t, nil
}
}
return nil, Errorf(NoSuchWaiter, "no such Waiter %s", s)
}
func init() {
RegisterWaiter(
func(s string) (t Waiter, err error) {
i := strings.Index(s, "x")
if i != -1 {
f, d, err := parseWait(s[:i], s[i+1:])
if err != nil {
return nil, Errorf(InvalidWaitString, "invalid wait %s (%s)", s, err)
}
return &WaitMaxRTT{D: d, Factor: f}, nil
}
return nil, nil
},
"#xduration: # times max RTT, or duration if no response",
)
RegisterWaiter(
func(s string) (t Waiter, err error) {
i := strings.Index(s, "r")
if i != -1 {
f, d, err := parseWait(s[:i], s[i+1:])
if err != nil {
return nil, Errorf(InvalidWaitString, "invalid wait %s (%s)", s, err)
}
return &WaitMeanRTT{D: d, Factor: f}, nil
}
return nil, nil
},
"#rduration: # times RTT, or duration if no response",
)
RegisterWaiter(
func(s string) (Waiter, error) {
if d, err := time.ParseDuration(s); err == nil {
return &WaitDuration{D: d}, nil
}
return nil, nil
},
"duration: fixed duration (see Duration units below)",
)
}
func parseWait(fstr string, dstr string) (factor int, dur time.Duration, err error) {
factor, err = strconv.Atoi(fstr)
if err != nil {
err = Errorf(InvalidWaitFactor, "unparseable factor %s", fstr)
return
}
dur, err = time.ParseDuration(dstr)
if err != nil {
err = Errorf(InvalidWaitDuration, "not a duration %s", dstr)
return
}
return
}
irtt/packet_test.go 0000644 0001751 0001751 00000006767 13240047124 013370 0 ustar pete pete package irtt
import (
"bytes"
"fmt"
"testing"
"time"
)
// request & reply
var testFiller = NewPatternFiller([]byte{0xff, 0xfe, 0xfd, 0xfc})
// request
const testReqCtoken = ctoken(0x886bc9a722b33eea)
const testReqSeqno = Seqno(0x6fe2a1bb)
var testReqHMACKey = []byte{0x3c, 0x68, 0x1d, 0x39, 0x41, 0x1d, 0x72, 0x43}
var testReqBytes = []byte{0x14, 0xa7, 0x5b, 0x8, 0xe7, 0x3, 0x41, 0xe7, 0xd4,
0x8, 0xcf, 0x69, 0x41, 0xf3, 0xf4, 0x78, 0x5a, 0x56, 0xc, 0x4c, 0xea, 0x3e,
0xb3, 0x22, 0xa7, 0xc9, 0x6b, 0x88, 0xbb, 0xa1, 0xe2, 0x6f, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xfe, 0xfd, 0xfc, 0xff, 0xfe,
0xfd, 0xfc, 0xff, 0xfe, 0xfd, 0xfc, 0xff, 0xfe, 0xfd, 0xfc}
// reply
const testRepCtoken = ctoken(0xe666ceb6766fcbc3)
const testRepSeqno = Seqno(0x1d3b0706)
var testRepHMACKey = []byte{0xda, 0xb3, 0xe9, 0x04, 0xa6, 0x87, 0x92, 0x49}
var testRepReceivedCount = ReceivedCount(0xa3bc9f19)
var testRepReceivedWindow = ReceivedWindow(0xd7e939e586f83b9b)
var testRepTimestamp = Timestamp{
Time{0x525af13dee2a75a1, time.Duration(0x2d5562223e4ac69a)},
Time{0x589705d446293f69, time.Duration(0x461df12fdd2c5066)},
}
var testRepBytes = []byte{0x14, 0xa7, 0x5b, 0x8, 0xd2, 0x98, 0xa3, 0x4a, 0x6a,
0x13, 0x41, 0x2, 0x68, 0xb2, 0x67, 0xa8, 0xd6, 0x7e, 0x28, 0x25, 0xc3, 0xcb,
0x6f, 0x76, 0xb6, 0xce, 0x66, 0xe6, 0x6, 0x7, 0x3b, 0x1d, 0x19, 0x9f, 0xbc,
0xa3, 0x9b, 0x3b, 0xf8, 0x86, 0xe5, 0x39, 0xe9, 0xd7, 0xa1, 0x75, 0x2a, 0xee,
0x3d, 0xf1, 0x5a, 0x52, 0x9a, 0xc6, 0x4a, 0x3e, 0x22, 0x62, 0x55, 0x2d, 0x69,
0x3f, 0x29, 0x46, 0xd4, 0x5, 0x97, 0x58, 0x66, 0x50, 0x2c, 0xdd, 0x2f, 0xf1,
0x1d, 0x46, 0xff, 0xfe, 0xfd, 0xfc, 0xff, 0xfe, 0xfd, 0xfc, 0xff, 0xfe, 0xfd,
0xfc, 0xff, 0xfe, 0xfd, 0xfc}
// TestRequestPacket tests a typical filled request with HMAC.
func TestRequestPacket(t *testing.T) {
p := newPacket(0, maxHeaderLen, testReqHMACKey)
p.setConnToken(testReqCtoken)
p.addFields(fechoRequest, true)
p.zeroReceivedStats(ReceivedStatsBoth)
p.stampZeroes(AtBoth, BothClocks)
p.setSeqno(testReqSeqno)
p.setLen(256)
err := p.readPayload(testFiller)
if err != nil {
t.Error(err)
}
p.updateHMAC()
t.Logf("Request bytes:\n% x", p.bytes())
//t.Log(byteArrayLiteral(p.bytes()))
if !bytesEqual(p.bytes(), testReqBytes) {
t.Errorf("Unexpected request bytes:\nexpected:\n% x\ngot:\n% x",
testReqBytes, p.bytes())
}
}
// TestReplyPacket tests a typical filled reply with HMAC.
func TestReplyPacket(t *testing.T) {
p := newPacket(0, maxHeaderLen, testRepHMACKey)
p.setConnToken(testRepCtoken)
p.addFields(fechoReply, true)
p.addReceivedStatsFields(ReceivedStatsBoth)
p.setReceivedCount(testRepReceivedCount)
p.setReceivedWindow(testRepReceivedWindow)
p.addTimestampFields(AtBoth, BothClocks)
p.setTimestamp(testRepTimestamp)
p.setSeqno(testRepSeqno)
p.setLen(256)
err := p.readPayload(testFiller)
if err != nil {
t.Error(err)
}
p.updateHMAC()
t.Logf("Reply bytes:\n% x", p.bytes())
//t.Log(byteArrayLiteral(p.bytes()))
if !bytesEqual(p.bytes(), testRepBytes) {
t.Errorf("Unexpected reply bytes:\nexpected:\n% x\ngot:\n% x",
testRepBytes, p.bytes())
}
}
func byteArrayLiteral(b []byte) string {
buf := bytes.NewBufferString("")
fmt.Fprint(buf, "[]byte{")
for i, x := range b {
if i != 0 {
fmt.Fprint(buf, ",")
}
fmt.Fprint(buf, " ")
fmt.Fprintf(buf, "0x%x", x)
}
fmt.Fprint(buf, " }")
return buf.String()
}
irtt/filler.go 0000644 0001751 0001751 00000005544 13240047124 012327 0 ustar pete pete package irtt
import (
"encoding/hex"
"fmt"
"io"
"math/rand"
"strings"
"time"
)
// Filler is a Reader used for filling the payload in packets.
type Filler interface {
io.Reader
String() string
}
// PatternFiller can be used to fill with a repeating byte pattern.
type PatternFiller struct {
Bytes []byte
buf []byte
pos int
}
// NewPatternFiller returns a new PatternFiller.
func NewPatternFiller(bytes []byte) *PatternFiller {
var blen int
if len(bytes) > patternMaxInitLen {
blen = len(bytes)
} else {
blen = patternMaxInitLen / len(bytes) * (len(bytes) + 1)
}
buf := make([]byte, blen)
for i := 0; i < len(buf); i += len(bytes) {
copy(buf[i:], bytes)
}
return &PatternFiller{bytes, buf, 0}
}
// NewDefaultPatternFiller returns a new PatternFiller with the default pattern.
func NewDefaultPatternFiller() *PatternFiller {
return NewPatternFiller(DefaultFillPattern)
}
func (f *PatternFiller) Read(p []byte) (n int, err error) {
l := 0
for l < len(p) {
c := copy(p[l:], f.buf[f.pos:])
l += c
f.pos = (f.pos + c) % len(f.Bytes)
}
return l, nil
}
func (f *PatternFiller) String() string {
return fmt.Sprintf("pattern:%x", f.Bytes)
}
// RandFiller is a Filler that fills with data from math.rand.
type RandFiller struct {
*rand.Rand
}
// NewRandFiller returns a new RandFiller.
func NewRandFiller() *RandFiller {
return &RandFiller{rand.New(rand.NewSource(time.Now().UnixNano()))}
}
func (rf *RandFiller) String() string {
return "rand"
}
// FillerFactories are the registered Filler factories.
var FillerFactories = make([]FillerFactory, 0)
// FillerFactory can create a Filler from a string.
type FillerFactory struct {
FactoryFunc func(string) (Filler, error)
Usage string
}
// RegisterFiller registers a new Filler.
func RegisterFiller(fn func(string) (Filler, error), usage string) {
FillerFactories = append(FillerFactories, FillerFactory{fn, usage})
}
// NewFiller returns a Filler from a string.
func NewFiller(s string) (Filler, error) {
if s == "none" {
return nil, nil
}
for _, fac := range FillerFactories {
f, err := fac.FactoryFunc(s)
if err != nil {
return nil, err
}
if f != nil {
return f, nil
}
}
return nil, Errorf(NoSuchFiller, "no such Filler %s", s)
}
func init() {
RegisterFiller(
func(s string) (f Filler, err error) {
if s == "rand" {
f = NewRandFiller()
}
return
},
"rand: use random bytes from Go's math.rand",
)
RegisterFiller(
func(s string) (Filler, error) {
args := strings.Split(s, ":")
if args[0] != "pattern" {
return nil, nil
}
var b []byte
if len(args) == 1 {
b = DefaultFillPattern
} else {
var err error
b, err = hex.DecodeString(args[1])
if err != nil {
return nil, err
}
}
return NewPatternFiller(b), nil
},
fmt.Sprintf("pattern:XX: use repeating pattern of hex (default %x)",
DefaultFillPattern),
)
}
irtt/user_prod.go 0000644 0001751 0001751 00000000776 13240047124 013056 0 ustar pete pete // +build prod
package irtt
import (
"os"
"time"
)
const minNonRootInterval = 10 * time.Millisecond
// Note that Windows always reports a UID of -1. Therefore, Windows users will
// not be subject to this restriction.
func validateInterval(i time.Duration) error {
// do not allow non-root users an interval of less than 10ms
if i < minNonRootInterval && os.Geteuid() > 0 {
return Errorf(IntervalNotPermitted, "interval < %s not permitted for non-root user",
minNonRootInterval)
}
return nil
}
irtt/sys_posix.go 0000644 0001751 0001751 00000001544 13240047124 013106 0 ustar pete pete // +build darwin dragonfly freebsd linux netbsd openbsd solaris
package irtt
import (
"net"
"golang.org/x/sys/unix"
)
/*
// old syscall code, not used with golang.org/x/net
func setSockoptTOS(conn *net.UDPConn, tos int) error {
return setSockoptInt(conn, unix.IPPROTO_IP, unix.IP_TOS, tos)
}
func setSockoptTrafficClass(conn *net.UDPConn, tclass int) error {
return setSockoptInt(conn, unix.IPPROTO_IPV6, unix.IPV6_TCLASS, tclass)
}
func setSockoptTTL(conn *net.UDPConn, ttl int) error {
return setSockoptInt(conn, unix.IPPROTO_IP, unix.IP_TTL, ttl)
}
*/
func setSockoptInt(conn *net.UDPConn, level int, opt int, value int) error {
cfile, err := conn.File()
if err != nil {
return err
}
defer cfile.Close()
fd := int(cfile.Fd())
err = unix.SetsockoptInt(fd, level, opt, value)
if err != nil {
return err
}
return unix.SetNonblock(fd, true)
}
irtt/context.go 0000644 0001751 0001751 00000000213 13240047124 012522 0 ustar pete pete package irtt
import "context"
func isContextError(err error) bool {
return err == context.Canceled || err == context.DeadlineExceeded
}
irtt/irtt_client.go 0000644 0001751 0001751 00000040444 13240047124 013370 0 ustar pete pete package irtt
import (
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"text/tabwriter"
"time"
flag "github.com/ogier/pflag"
)
func clientUsage() {
setBufio()
printf("Usage: client [flags] host|host:port (see Host formats below)")
printf("")
printf("Flags:")
printf("------")
printf("")
printf("-d duration total time to send (default %s, see Duration units below)", DefaultDuration)
printf("-i interval send interval (default %s, see Duration units below)", DefaultInterval)
printf("-l length length of packet (including irtt headers, default %d)", DefaultLength)
printf(" increased as necessary for irtt headers, common values:")
printf(" 1472 (max unfragmented size of IPv4 datagram for 1500 byte MTU)")
printf(" 1452 (max unfragmented size of IPv6 datagram for 1500 byte MTU)")
printf("-o file write JSON output to file (use '-' for stdout)")
printf(" if file has no extension, .json.gz is added, output is gzipped")
printf(" if extension is .json.gz, output is gzipped")
printf(" if extension is .gz, it's changed to .json.gz, output is gzipped")
printf(" if extension is .json, output is not gzipped")
printf(" output to stdout is not gzipped, pipe to gzip if needed")
printf("-q quiet, suppress per-packet output")
printf("-Q really quiet, suppress all output except errors to stderr")
printf("-n no test, connect to the server and validate test parameters")
printf(" but don't run the test")
printf("--stats=stats server stats on received packets (default %s)", DefaultReceivedStats.String())
printf(" none: no server stats on received packets")
printf(" count: total count of received packets")
printf(" window: receipt status of last 64 packets with each reply")
printf(" both: both count and window")
printf("--tstamp=mode server timestamp mode (default %s)", DefaultStampAt.String())
printf(" none: request no timestamps")
printf(" send: request timestamp at server send")
printf(" receive: request timestamp at server receive")
printf(" both: request both send and receive timestamps")
printf(" midpoint: request midpoint timestamp (send/receive avg)")
printf("--clock=clock clock/s used for server timestamps (default %s)", DefaultClock)
printf(" wall: wall clock only")
printf(" monotonic: monotonic clock only")
printf(" both: both clocks")
printf("--dscp=dscp DSCP (ToS) value (default %s, 0x for hex), common values:", strconv.Itoa(DefaultDSCP))
printf(" 0 (Best effort)")
printf(" 8 (CS1- Bulk)")
printf(" 40 (CS5- Video)")
printf(" 46 (EF- Expedited forwarding)")
printf(" https://www.tucny.com/Home/dscp-tos")
printf("--df=DF setting for do not fragment (DF) bit in all packets")
printf(" default: OS default")
printf(" false: DF bit not set")
printf(" true: DF bit set")
printf("--wait=wait wait time at end of test for unreceived replies (default %s)", DefaultWait.String())
printf(" - Valid formats -")
for _, wfac := range WaiterFactories {
printf(" %s", wfac.Usage)
}
printf(" - Examples -")
printf(" 3x4s: 3 times max RTT, or 4 seconds if no response")
printf(" 1500ms: fixed 1500 milliseconds")
printf("--timer=timer timer for waiting to send packets (default %s)", DefaultTimer.String())
for _, tfac := range TimerFactories {
printf(" %s", tfac.Usage)
}
printf("--tcomp=alg comp timer averaging algorithm (default %s)", DefaultCompTimerAverage.String())
for _, afac := range AveragerFactories {
printf(" %s", afac.Usage)
}
printf("--fill=fill fill payload with given data (default none)")
printf(" none: leave payload as all zeroes")
for _, ffac := range FillerFactories {
printf(" %s", ffac.Usage)
}
printf("--fill-one fill only once and repeat for all packets")
printf("--sfill=fill request server fill (default not specified)")
printf(" see options for --fill")
printf(" server must support and allow this fill with --allow-fills")
printf("--local=addr local address (default from OS), valid formats:")
printf(" :port (all IPv4/IPv6 addresses with port)")
printf(" host (host with dynamic port, see Host formats below)")
printf(" host:port (host with specified port, see Host formats below)")
printf("--hmac=key add HMAC with key (0x for hex) to all packets, provides:")
printf(" dropping of all packets without a correct HMAC")
printf(" protection for server against unauthorized discovery and use")
printf("-4 IPv4 only")
printf("-6 IPv6 only")
printf("--timeouts=drs timeouts used when connecting to server (default %s)", DefaultOpenTimeouts.String())
printf(" comma separated list of durations (see Duration units below)")
printf(" total wait time will be up to the sum of these Durations")
printf(" max packets sent is up to the number of Durations")
printf(" minimum timeout duration is %s", minOpenTimeout)
printf("--ttl=ttl time to live (default %d, meaning use OS default)", DefaultTTL)
printf("--loose accept and use any server restricted test parameters instead")
printf(" of exiting with nonzero status")
printf("--thread lock sending and receiving goroutines to OS threads")
printf("-h show help")
printf("-v show version")
printf("")
hostUsage()
printf("")
durationUsage()
}
func hostUsage() {
printf("Host formats:")
printf("-------------")
printf("")
printf("Hosts may be either hostnames (for IPv4 or IPv6) or IP addresses. IPv6")
printf("addresses must be surrounded by brackets and may include a zone after the %%")
printf("character. Examples:")
printf("")
printf("IPv4 IP: 192.168.1.10")
printf("IPv6 IP: [fe80::426c:8fff:fe13:9feb%%en0]")
printf("IPv4/6 hostname: localhost")
printf("")
printf("Note: IPv6 addresses must be quoted in most shells.")
}
func durationUsage() {
printf("Duration units:")
printf("---------------")
printf("")
printf("Durations are a sequence of decimal numbers, each with optional fraction, and")
printf("unit suffix, such as: \"300ms\", \"1m30s\" or \"2.5m\". Sanity not enforced.")
printf("")
printf("h hours")
printf("m minutes")
printf("s seconds")
printf("ms milliseconds")
printf("ns nanoseconds")
}
// runClientCLI runs the client command line interface.
func runClientCLI(args []string) {
// client flags
fs := flag.NewFlagSet("client", flag.ContinueOnError)
fs.Usage = func() {
usageAndExit(clientUsage, exitCodeBadCommandLine)
}
var durationStr = fs.StringP("d", "d", DefaultDuration.String(), "total time to send")
var intervalStr = fs.StringP("i", "i", DefaultInterval.String(), "send interval")
var length = fs.IntP("l", "l", DefaultLength, "packet length")
var noTest = fs.BoolP("n", "n", false, "no test")
var rsStr = fs.String("stats", DefaultReceivedStats.String(), "received stats")
var tsatStr = fs.String("tstamp", DefaultStampAt.String(), "stamp at")
var clockStr = fs.String("clock", DefaultClock.String(), "clock")
var outputStr = fs.StringP("o", "o", "", "output file")
var quiet = fs.BoolP("q", "q", defaultQuiet, "quiet")
var reallyQuiet = fs.BoolP("Q", "Q", defaultReallyQuiet, "really quiet")
var dscpStr = fs.String("dscp", strconv.Itoa(DefaultDSCP), "dscp value")
var dfStr = fs.String("df", DefaultDF.String(), "do not fragment")
var waitStr = fs.String("wait", DefaultWait.String(), "wait")
var timerStr = fs.String("timer", DefaultTimer.String(), "timer")
var tcompStr = fs.String("tcomp", DefaultCompTimerAverage.String(),
"timer compensation algorithm")
var fillStr = fs.String("fill", "none", "fill")
var fillOne = fs.Bool("fill-one", false, "fill one")
var sfillStr = fs.String("sfill", "", "sfill")
var laddrStr = fs.String("local", DefaultLocalAddress, "local address")
var hmacStr = fs.String("hmac", defaultHMACKey, "HMAC key")
var ipv4 = fs.BoolP("4", "4", false, "IPv4 only")
var ipv6 = fs.BoolP("6", "6", false, "IPv6 only")
var timeoutsStr = fs.String("timeouts", DefaultOpenTimeouts.String(), "open timeouts")
var ttl = fs.Int("ttl", DefaultTTL, "IP time to live")
var loose = fs.Bool("loose", DefaultLoose, "loose")
var threadLock = fs.Bool("thread", DefaultThreadLock, "thread")
var version = fs.BoolP("version", "v", false, "version")
err := fs.Parse(args)
// start profiling, if enabled in build
if profileEnabled {
defer startProfile("./client.pprof").Stop()
}
// version
if *version {
runVersion(args)
os.Exit(0)
}
// parse duration
duration, err := time.ParseDuration(*durationStr)
if err != nil {
exitOnError(fmt.Errorf("%s (use s for seconds)", err),
exitCodeBadCommandLine)
}
// parse interval
interval, err := time.ParseDuration(*intervalStr)
if err != nil {
exitOnError(fmt.Errorf("%s (use s for seconds)", err),
exitCodeBadCommandLine)
}
// determine IP version
ipVer := IPVersionFromBooleans(*ipv4, *ipv6, DualStack)
// parse DSCP
dscp, err := strconv.ParseInt(*dscpStr, 0, 32)
exitOnError(err, exitCodeBadCommandLine)
// parse DF
df, err := ParseDF(*dfStr)
exitOnError(err, exitCodeBadCommandLine)
// parse wait
waiter, err := NewWaiter(*waitStr)
exitOnError(err, exitCodeBadCommandLine)
// parse received stats
rs, err := ParseReceivedStats(*rsStr)
exitOnError(err, exitCodeBadCommandLine)
// parse timestamp string
at, err := ParseStampAt(*tsatStr)
exitOnError(err, exitCodeBadCommandLine)
// parse clock
clock, err := ParseClock(*clockStr)
exitOnError(err, exitCodeBadCommandLine)
// parse timer compensation
timerComp, err := NewAverager(*tcompStr)
exitOnError(err, exitCodeBadCommandLine)
// parse timer
timer, err := NewTimer(*timerStr, timerComp)
exitOnError(err, exitCodeBadCommandLine)
// parse fill
filler, err := NewFiller(*fillStr)
exitOnError(err, exitCodeBadCommandLine)
// parse open timeouts
timeouts, err := ParseDurations(*timeoutsStr)
if err != nil {
exitOnError(fmt.Errorf("%s (use s for seconds)", err),
exitCodeBadCommandLine)
}
// parse HMAC key
var hmacKey []byte
if *hmacStr != "" {
hmacKey, err = decodeHexOrNot(*hmacStr)
exitOnError(err, exitCodeBadCommandLine)
}
// check for remote address argument
if len(fs.Args()) != 1 {
usageAndExit(clientUsage, exitCodeBadCommandLine)
}
raddrStr := fs.Args()[0]
// send regular output to stderr if json going to stdout
if *outputStr == "-" {
printTo = os.Stderr
}
// create context
ctx, cancel := context.WithCancel(context.Background())
// install signal handler to cancel context, which stops the test
sigs := make(chan os.Signal, 1)
signal.Notify(sigs,
syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
sig := <-sigs
if !*reallyQuiet {
printf("%s", sig)
}
cancel()
sig = <-sigs
if !*reallyQuiet {
printf("second interrupt, exiting")
}
os.Exit(exitCodeDoubleSignal)
}()
// create config
cfg := NewClientConfig()
cfg.LocalAddress = *laddrStr
cfg.RemoteAddress = raddrStr
cfg.OpenTimeouts = timeouts
cfg.NoTest = *noTest
cfg.Duration = duration
cfg.Interval = interval
cfg.Length = *length
cfg.ReceivedStats = rs
cfg.StampAt = at
cfg.Clock = clock
cfg.DSCP = int(dscp)
cfg.ServerFill = *sfillStr
cfg.Loose = *loose
cfg.IPVersion = ipVer
cfg.DF = df
cfg.TTL = int(*ttl)
cfg.Timer = timer
cfg.Waiter = waiter
cfg.Filler = filler
cfg.FillOne = *fillOne
cfg.HMACKey = hmacKey
cfg.Handler = &clientHandler{*quiet, *reallyQuiet}
cfg.ThreadLock = *threadLock
// run test
c := NewClient(cfg)
r, err := c.Run(ctx)
if err != nil {
exitOnError(err, exitCodeRuntimeError)
}
// exit if NoTest set
if cfg.NoTest {
return
}
// print results
if !*reallyQuiet {
printResult(r)
}
// write results to JSON
if *outputStr != "" {
if err := writeResultJSON(r, *outputStr, ctx.Err() != nil); err != nil {
exitOnError(err, exitCodeRuntimeError)
}
}
}
func printResult(r *Result) {
// set some stat variables for later brevity
rtts := r.RTTStats
rttvs := r.RoundTripIPDVStats
sds := r.SendDelayStats
svs := r.SendIPDVStats
rds := r.ReceiveDelayStats
rvs := r.ReceiveIPDVStats
ss := r.SendCallStats
tes := r.TimerErrorStats
sps := r.ServerProcessingTimeStats
if r.SendErr != nil {
if r.SendErr != context.Canceled {
printf("\nTerminated due to send error: %s", r.SendErr)
}
}
if r.ReceiveErr != nil {
printf("\nTerminated due to receive error: %s", r.ReceiveErr)
}
printf("")
printStats := func(title string, s DurationStats) {
if s.N > 0 {
var med string
if m, ok := s.Median(); ok {
med = rdur(m).String()
}
printf("%s\t%s\t%s\t%s\t%s\t%s\t", title, rdur(s.Min), rdur(s.Mean()),
med, rdur(s.Max), rdur(s.Stddev()))
}
}
setTabWriter(tabwriter.AlignRight)
printf("\tMin\tMean\tMedian\tMax\tStddev\t")
printf("\t---\t----\t------\t---\t------\t")
printStats("RTT", rtts)
printStats("send delay", sds)
printStats("receive delay", rds)
printf("\t\t\t\t\t\t")
printStats("IPDV (jitter)", rttvs)
printStats("send IPDV", svs)
printStats("receive IPDV", rvs)
printf("\t\t\t\t\t\t")
printStats("send call time", ss)
printStats("timer error", tes)
printStats("server proc. time", sps)
printf("")
printf(" duration: %s (wait %s)", rdur(r.Duration), rdur(r.Wait))
printf(" packets sent/received: %d/%d (%.2f%% loss)", r.PacketsSent,
r.PacketsReceived, r.PacketLossPercent)
if r.PacketsReceived > 0 && r.ServerPacketsReceived > 0 {
printf(" server packets received: %d/%d (%.2f%%/%.2f%% loss up/down)",
r.ServerPacketsReceived, r.PacketsSent, r.UpstreamLossPercent,
r.DownstreamLossPercent)
}
if r.Duplicates > 0 {
printf(" *** DUPLICATES: %d (%.2f%%)", r.Duplicates,
r.DuplicatePercent)
}
if r.LatePackets > 0 {
printf("late (out-of-order) pkts: %d (%.2f%%)", r.LatePackets,
r.LatePacketsPercent)
}
printf(" bytes sent/received: %d/%d", r.BytesSent, r.BytesReceived)
printf(" send/receive rate: %s / %s", r.SendRate, r.ReceiveRate)
printf(" packet length: %d bytes", r.Config.Length)
printf(" timer stats: %d/%d (%.2f%%) missed, %.2f%% error",
r.TimerMisses, r.ExpectedPacketsSent, r.TimerMissPercent,
r.TimerErrPercent)
flush()
}
func writeResultJSON(r *Result, output string, cancelled bool) error {
var jout io.Writer
var gz bool
if output == "-" {
if cancelled {
return nil
}
jout = os.Stdout
} else {
gz = true
if strings.HasSuffix(output, ".json") {
gz = false
} else if !strings.HasSuffix(output, ".json.gz") {
if strings.HasSuffix(output, ".gz") {
output = output[:len(output)-3] + ".json.gz"
} else {
output = output + ".json.gz"
}
}
of, err := os.Create(output)
if err != nil {
exitOnError(err, exitCodeRuntimeError)
}
defer of.Close()
jout = of
}
if gz {
gzw := gzip.NewWriter(jout)
defer func() {
gzw.Flush()
gzw.Close()
}()
jout = gzw
}
e := json.NewEncoder(jout)
e.SetIndent("", " ")
return e.Encode(r)
}
type clientHandler struct {
quiet bool
reallyQuiet bool
}
func (c *clientHandler) OnSent(seqno Seqno, rtd *RoundTripData) {
}
func (c *clientHandler) OnReceived(seqno Seqno, rtd *RoundTripData,
prtd *RoundTripData, late bool, dup bool) {
if !c.reallyQuiet {
if dup {
printf("DUP! seq=%d", seqno)
return
}
if !c.quiet {
ipdv := "n/a"
if prtd != nil {
dv := rtd.IPDVSince(prtd)
if dv != InvalidDuration {
ipdv = rdur(AbsDuration(dv)).String()
}
}
rd := ""
if rtd.ReceiveDelay() != InvalidDuration {
rd = fmt.Sprintf(" rd=%s", rdur(rtd.ReceiveDelay()))
}
sd := ""
if rtd.SendDelay() != InvalidDuration {
sd = fmt.Sprintf(" sd=%s", rdur(rtd.SendDelay()))
}
sl := ""
if late {
sl = " (LATE)"
}
printf("seq=%d rtt=%s%s%s ipdv=%s%s", seqno, rdur(rtd.RTT()),
rd, sd, ipdv, sl)
}
}
}
func (c *clientHandler) OnEvent(ev *Event) {
if !c.reallyQuiet {
printf("%s", ev)
}
}
irtt/user_dev.go 0000644 0001751 0001751 00000000153 13240047124 012655 0 ustar pete pete // +build !prod
package irtt
import "time"
func validateInterval(i time.Duration) error {
return nil
}