irtt/0000755000175100017510000000000013240047124010513 5ustar petepeteirtt/event.go0000644000175100017510000000241713240047124012167 0ustar petepetepackage 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.go0000644000175100017510000001065713240047124012336 0ustar petepetepackage 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.go0000644000175100017510000001565213240047124013423 0ustar petepetepackage 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.go0000644000175100017510000000125013240047124012525 0ustar petepetepackage 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.go0000644000175100017510000000106213240047124011432 0ustar petepetepackage 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.go0000644000175100017510000000212413240047124012471 0ustar petepetepackage 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.go0000644000175100017510000000600313240047124012451 0ustar petepetepackage 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.go0000644000175100017510000000252213240047124012475 0ustar petepetepackage 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.sh0000755000175100017510000000320213240047124012146 0ustar petepete#!/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.go0000644000175100017510000000273113240047124013166 0ustar petepetepackage 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/LICENSE0000644000175100017510000010450513240047124011525 0ustar petepete 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.go0000644000175100017510000000737313240047124012663 0ustar petepetepackage 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.go0000644000175100017510000000042613240047124012506 0ustar petepete// +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.go0000644000175100017510000000125313240047124012171 0ustar petepetepackage 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.go0000644000175100017510000001654413240047124012174 0ustar petepetepackage 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/0000755000175100017510000000000013240047124011260 5ustar petepeteirtt/doc/irtt-server.html0000644000175100017510000002433313240047124014441 0ustar petepete IRTT-SERVER(1) v0.9.0 | IRTT Manual

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).

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 below
host:port host with specified port, see 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 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:

--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:

--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:

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:

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)
$ 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-client(1)

IRTT GitHub repository

irtt/doc/irtt-client.html0000644000175100017510000010557013240047124014414 0ustar petepete IRTT-CLIENT(1) v0.9.0 | IRTT Manual

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).

OPTIONS

-d duration
Total time to send (default 1m0s, see Duration units below)
-i interval
Send interval (default 1s, see Duration units below)
-l length

Length of packet (default 0, increased as necessary for required headers), common values:

-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

--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
#xduration # times max RTT, or duration if no response
#rduration # times RTT, or duration if no response
duration fixed duration (see 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 below
host:port Host with specified port, see Host formats below
--hmac=key

Add HMAC with key (0x for hex) to all packets, provides:

-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:

  1. version
  2. system_info
  3. config
  4. stats
  5. 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
},

system_info

a few basic pieces of system information

"system_info": {
    "os": "darwin",
    "cpus": 8,
    "go_version": "go1.9.2",
    "hostname": "tron.local"
},

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
    }
},

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:

The regular attributes in stats are as follows:

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

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:

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-server(1)

IRTT GitHub repository

irtt/doc/irtt.html0000644000175100017510000002767213240047124013146 0ustar petepete 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:

$ 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

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:

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:

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:

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:

There are many ways that this simple approach could be improved, such as by:

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:

However, Go also has characteristics that make it a good fit for this application:

SEE ALSO

irtt-client(1), irtt-server(1)

IRTT GitHub repository

AUTHOR

Pete Heist pete@eventide.io

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.10000644000175100017510000001547513240047124013644 0ustar petepete.\"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.md0000644000175100017510000006037513240047124014053 0ustar petepete% 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.md0000644000175100017510000001424513240047124014076 0ustar petepete% 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.10000644000175100017510000007320713240047124013611 0ustar petepete.\"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.html0000644000175100017510000000115113240047124013045 0ustar petepete irtt/doc/irtt.10000644000175100017510000002455613240047124012340 0ustar petepete.\" 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.md0000644000175100017510000002225013240047124012565 0ustar petepete% 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.go0000644000175100017510000003245413240047124012007 0ustar petepetepackage 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.md0000644000175100017510000004711113240047124011776 0ustar petepete# 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/0000755000175100017510000000000013240047124011256 5ustar petepeteirtt/cmd/irtt/0000755000175100017510000000000013240047124012240 5ustar petepeteirtt/cmd/irtt/main.go0000644000175100017510000000014413240047124013512 0ustar petepetepackage main import ( "os" "github.com/peteheist/irtt" ) func main() { irtt.RunCLI(os.Args) } irtt/averager.go0000644000175100017510000001231213240047124012635 0ustar petepetepackage 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.go0000644000175100017510000002534013240047124012324 0ustar petepetepackage 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.go0000644000175100017510000000330113240047124012170 0ustar petepetepackage 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.md0000644000175100017510000000415713240047124012114 0ustar petepete# 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/.gitignore0000644000175100017510000000045013240047124012502 0ustar petepete# 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.go0000644000175100017510000000406613240047124011655 0ustar petepetepackage 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.go0000644000175100017510000002100713240047124012350 0ustar petepetepackage 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.go0000644000175100017510000002727113240047124012322 0ustar petepetepackage 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.go0000644000175100017510000000047513240047124012516 0ustar petepete// +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.go0000644000175100017510000002004013240047124012354 0ustar petepetepackage 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.go0000644000175100017510000000134113240047124013237 0ustar petepetepackage 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@.service0000644000175100017510000000111013240047124013150 0ustar petepete[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.go0000644000175100017510000000044513240047124013574 0ustar petepetepackage 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.go0000644000175100017510000000113313240047124011431 0ustar petepetepackage 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.go0000644000175100017510000001202213240047124011761 0ustar petepetepackage 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.go0000644000175100017510000000026613240047124012672 0ustar petepete// +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.go0000644000175100017510000000125113240047124013176 0ustar petepetepackage 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.go0000644000175100017510000000020513240047124012637 0ustar petepete// +build !profile package irtt const profileEnabled = false func startProfile(path string) interface { Stop() } { return nil } irtt/glob.go0000644000175100017510000000153313240047124011767 0ustar petepetepackage 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.go0000644000175100017510000000056713240047124013107 0ustar petepete// +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.go0000644000175100017510000001727013240047124012007 0ustar petepetepackage 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.go0000644000175100017510000003340713240047124012656 0ustar petepetepackage 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.go0000644000175100017510000000442113240047124012025 0ustar petepetepackage 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.service0000644000175100017510000000072413240047124013062 0ustar petepete[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.go0000644000175100017510000000174113240047124013217 0ustar petepetepackage 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.go0000644000175100017510000000351513240047124012366 0ustar petepetepackage 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.go0000644000175100017510000001532013240047124012163 0ustar petepetepackage 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.go0000644000175100017510000000014113240047124011762 0ustar petepetepackage irtt import ( "math/rand" "time" ) func init() { rand.Seed(time.Now().UnixNano()) } irtt/code_string.go0000644000175100017510000000507213240047124013346 0ustar petepete// 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.go0000644000175100017510000000074713240047124012544 0ustar petepetepackage 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.go0000644000175100017510000000533513240047124011636 0ustar petepetepackage 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.go0000644000175100017510000000704313240047124012341 0ustar petepetepackage 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.go0000644000175100017510000000676713240047124013370 0ustar petepetepackage 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.go0000644000175100017510000000554413240047124012327 0ustar petepetepackage 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.go0000644000175100017510000000077613240047124013056 0ustar petepete// +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.go0000644000175100017510000000154413240047124013106 0ustar petepete// +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.go0000644000175100017510000000021313240047124012522 0ustar petepetepackage irtt import "context" func isContextError(err error) bool { return err == context.Canceled || err == context.DeadlineExceeded } irtt/irtt_client.go0000644000175100017510000004044413240047124013370 0ustar petepetepackage 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.go0000644000175100017510000000015313240047124012655 0ustar petepete// +build !prod package irtt import "time" func validateInterval(i time.Duration) error { return nil }