pax_global_header00006660000000000000000000000064140506523140014512gustar00rootroot0000000000000052 comment=c5ed230a672f5233f4a87b2a28dd6d5c86b6feba heistp-irtt-d858e7f/000077500000000000000000000000001405065231400144225ustar00rootroot00000000000000heistp-irtt-d858e7f/.gitignore000066400000000000000000000004501405065231400164110ustar00rootroot00000000000000# 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 heistp-irtt-d858e7f/CHANGES.md000066400000000000000000000065021405065231400160170ustar00rootroot00000000000000# 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). ## Unreleased ## 0.9.1 - 2021-05-18 ### Added - Improve Windows time support by using `QueryPerformanceCounter` and `GetSystemTimePreciseAsFileTime`. Time precision on Windows 10 can now reach 100ns. - Add syslog support for the server (`--syslog` flag). - Add a [SmokePing](https://oss.oetiker.ch/smokeping/) probe, available from SmokePing 2.7.2. It's possible to copy the [code](https://github.com/oetiker/SmokePing/blob/master/lib/Smokeping/probes/IRTT.pm) into older versions. - Update OM2P build to use the new [MIPS softfloat support](https://github.com/golang/go/issues/18162) in Go 1.10. ### Changed - Re-write git history with new email address. - Re-licensed to GPLv2. ### Removed - Stop disabling GC on client and remove `--gc` flag from server as disabling GC no longer offers a demonstrable performance benefit. See [The Journey of Go's Garbage Collector](https://blog.golang.org/ismmkeynote). ### Fixed - Fix potential client race at startup. - Fix issue on platforms with timer resolution not accurate enough to measure server processing time (closes #13). - Change handled interrupt signals to only `os.Interrupt` and `syscall.SIGTERM` to fix Plan 9 build. - Various documentation fixes. ## 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. heistp-irtt-d858e7f/LICENSE000066400000000000000000000432541405065231400154370ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) 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 this service 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 make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. 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. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), 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 distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the 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 a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 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. 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 convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision 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, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This 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. heistp-irtt-d858e7f/README.md000066400000000000000000000601071405065231400157050ustar00rootroot00000000000000# 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.1. 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 - Keep the executable size small enough for use on embedded devices - 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 - One nanosecond time precision on Linux and OS/X, and 100ns on Windows - Robustness in the face of clock drift and NTP corrections through the use of both 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 - An available [SmokePing](https://oss.oetiker.ch/smokeping/) probe ([code](https://github.com/oetiker/SmokePing/blob/master/lib/Smokeping/probes/IRTT.pm)) ## Limitations See the [LIMITATIONS](http://htmlpreview.github.io/?https://github.com/heistp/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/heistp/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/heistp/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 Building for iOS: I have no way to verify this, but I received a report that the following is "close to but not quite the right command" to cross-compile for iOS: ``GOOS=ios GOARCH=arm64 IPHONEOS_DEPLOYMENT_TARGET=14.0 CGO_ENABLED=1 CGO_CFLAGS="-arch arm64 -isysroot `xcrun --sdk iphoneos --show-sdk-path` -mios-version-min=10.0" CGO_LDFLAGS="-arch arm64 -isysroot `xcrun --sdk iphoneos --show-sdk-path`" go build -o irtt cmd/irtt/main.go`` Please file an issue if you get this working so I can update the doc. ## 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/heistp/irtt/blob/master/doc/irtt.html) | [EXAMPLES](http://htmlpreview.github.io/?https://github.com/heistp/irtt/blob/master/doc/irtt.html#examples) - [irtt-client(1)](http://htmlpreview.github.io/?https://github.com/heistp/irtt/blob/master/doc/irtt-client.html) | [EXAMPLES](http://htmlpreview.github.io/?https://github.com/heistp/irtt/blob/master/doc/irtt-client.html#examples) - [irtt-server(1)](http://htmlpreview.github.io/?https://github.com/heistp/irtt/blob/master/doc/irtt-server.html) | [EXAMPLES](http://htmlpreview.github.io/?https://github.com/heistp/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 a couple hundred 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. - On Windows, ping has a precision of 0.5ms, while IRTT uses high resolution timer functions for a precision of 100ns (high resolution wall clock only available on Windows 8 or Windows 2012 Server and later). Also note the following behavioral differences between ping and IRTT: - IRTT makes a stateful connection to the server, whereas ping is stateless. - By default, ping waits for a reply before sending its next request, while IRTT keeps sending requests on the specified interval regardless of whether or not replies are received. The effect of this, for example, is that a fixed-length pause in server packet processing (with packets buffered during the pause) will look like a single high RTT in ping, and multiple high then descending RTTs in IRTT for the duration of the maximum RTT. 2) Is there a public server I can use? There is a test server running at `irtt.heistp.net` with an HMAC key of `irttuser`. Please do not abuse it. To restrict bandwidth, the minimum interval is set to 100ms, the max length to 256 bytes, and the max duration to 60 seconds. Example usage: `irtt client --hmac=irttuser irtt.heistp.net` 3) How do I run the IRTT server at startup? This depends on your OS and init system, but see: - the `irtt.service` file for [systemd](https://www.freedesktop.org/wiki/Software/systemd/), used in Debian and Ubuntu - the `irtt.openrc` file for [OpenRC](https://wiki.gentoo.org/wiki/OpenRC), used in Gentoo and Alpine 4) 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, but reply packets are coming back on a different route from the requests, or not coming back at all. This can happen in network environments with [asymmetric routing and a firewall or NAT] (https://www.cisco.com/web/services/news/ts_newsletter/tech/chalktalk/archives/200903.html). There are several possible solutions to this: - Change your network configuration to avoid the problem. - Have the IRTT server listen on specific addresses with the `-b` flag. - Use the `--set-src-ip` flag on the server, which explicitly sets the source address on all reply packets from listeners on unspecified IP addresses to the destination address that the request was received on. The only reason this is not done by default is to avoid the extra per-packet heap allocations required by the `golang.org/x/net` packege to do so. 5) 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. 6) 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. 7) 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. 8) 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. 9) 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). 10) 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. 11) 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/heistp/irtt/issues). 12) 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. 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 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. 17) Why is the virt size (vsz) memory usage for the server so high in Linux? This has to do with the way Go allocates memory, but should not cause a problem. 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. 18) Why doesn't the server start on Linux when the kernel parameter `ipv6.disable=1` is set? By default, IRTT tries to listen on both IPv4 and IPv6 addresses, and for safety, the server shuts down if there are failures on any of the listeners for any of the addresses. In this case, the server may be started with the `-4` flag. 19) Why don't you make use of `x` library? We need to keep the executable size as small as possible for embedded devices, and most external libaries are not compatible with this. ## Changes See [CHANGES.md](CHANGES.md). ## Roadmap ### v0.9.2 _Planned for v0.9.2..._ - Solidify TimeSource, Time and new Windows timer support: - Add --timesrc to client and server - Fall back to Go functions as necessary for older Windows versions - Make sure all calls to TimeSource.Now pass in only needed clocks - Find a better way to log warnings than fmt.Fprintf(os.Stderr) in timesrc_win.go - Rename Time.Mono to Monotonic, or others from Monotonic to Mono for consistency - Document 100ns resolution for Windows - Improve diagnostic commands: - Change bench command to output in columns - Rename sleep command to timer and add --timesrc, --sleep, --timer and --tcomp - Rename timer command to resolution and add --timesrc - Rename clock command to drift and add --timesrc - Add a `late` flag to RoundTrip - Measure and document local differences between ping and irtt response times - Sync Debian package to history re-write and create backports version for Debian stable - Add `report` command, or similar, to print results from an existing JSON file ### v1.0.0 _Planned for v1.0.0..._ - Refactor handshake params to use signed values and straight bytes as appropriate. - Improve client output flexibility: - Allow specifying a format string for text output with optional units for times - Add format abbreviations for CSV, space delimited, etc. - Add a subcommand to the CLI to convert JSON to CSV - Add a way to disable per-packet results in JSON - 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 - Add more info on outliers and possibly a textual histogram - Refactor packet manipulation to improve readability, prevent multiple validations and support unit tests - Add DSCP text values and return an error when ECN bits are passed to --dscp - Improve open/close process: - Do Happy Eyeballs (RFC 8305) to better handle multiple address families and addresses - 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 - Add per-IP limiting - Add a more secure way than cmdline flag to specify --hmac - Stabilize API: - Minimize exposed functions (remove timer, timer comp, etc) - Always return instance of irtt.Error? If so, look at exitOnError. - Use error code (if available) as exit code - 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 - Show actual size of header in text and json, and add calculation to doc ### Inbox _Collection area..._ - Add [ping-pair](https://www.microsoft.com/en-us/research/wp-content/uploads/2017/09/PingPair-CoNEXT2017.pdf)-like functionality - Add UDP-lite support to allow partially damaged packets to be received - 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 - Add seqno to the Max and maybe Min columns in the text output - Prototype TCP throughput test and compare straight Go vs iperf/netperf - Support a range of server ports to improve concurrency and maybe defeat latency "slotting" on multi-queue interfaces - Add more 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 - Use a larger, internal received window on the server to increase up/down loss accuracy - Allow specifying two out of three of interval, bitrate and packet size - Calculate per-packet arrival order during results generation using timestamps - Make it possible to add custom per-round-trip statistics programmatically - Allow Server to listen on multiple IPs for a hostname - Prompt to write JSON file on cancellation - Open questions: - What do I do for IPDV when there are out of order packets? - Does exposing both monotonic and wall clock values, as well as dual timestamps, open the server to any timing attacks? - Is there any way to make the server concurrent without inducing latency? - 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. heistp-irtt-d858e7f/averager.go000066400000000000000000000123121405065231400165440ustar00rootroot00000000000000package 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), ) } heistp-irtt-d858e7f/bitrate.go000066400000000000000000000025221405065231400164040ustar00rootroot00000000000000package 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) } heistp-irtt-d858e7f/build.sh000077500000000000000000000034231405065231400160620ustar00rootroot00000000000000#!/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/heistp/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"|"linux32") env="GOOS=linux GOARCH=386" ;; "linux-387"|"linux-alix") env="GOOS=linux GOARCH=386 GO386=387" ;; "linux-amd64"|"linux") env="GOOS=linux GOARCH=amd64" ;; "linux-arm"|"rpi") env="GOOS=linux GOARCH=arm" ;; "linux-mips64"|"erl") env="GOOS=linux GOARCH=mips" ;; "linux-mipsle"|"erx") env="GOOS=linux GOARCH=mipsle" ;; "linux-mips-softfloat"|"om2p") env="GOOS=linux GOARCH=mips GOMIPS=softfloat" ;; "darwin-amd64"|"osx") env="GOOS=darwin GOARCH=amd64" ;; "win32"|"windows32") env="GOOS=windows GOARCH=386" ;; "win"|"windows") 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 heistp-irtt-d858e7f/bytes.go000066400000000000000000000012531405065231400161000ustar00rootroot00000000000000package 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) } heistp-irtt-d858e7f/cconfig.go000066400000000000000000000062261405065231400163670ustar00rootroot00000000000000package 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 TimeSource TimeSource 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, TimeSource: DefaultTimeSource, 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"` TimeSource string `json:"time_source"` 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(), TimeSource: c.TimeSource.String(), Waiter: c.Waiter.String(), Filler: fstr, FillOne: c.FillOne, ServerFill: c.ServerFill, ThreadLock: c.ThreadLock, Supplied: c.Supplied, } return json.Marshal(j) } heistp-irtt-d858e7f/client.go000066400000000000000000000256621405065231400162420ustar00rootroot00000000000000package irtt import ( "context" "fmt" "math/rand" "net" "runtime" "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 initCh chan (bool) } // NewClient returns a new client. func NewClient(cfg *ClientConfig) *Client { // create client c := *cfg c.Supplied = cfg return &Client{ ClientConfig: &c, initCh: make(chan (bool)), } } // 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.TimeSource, c.Handler); err != nil { return } // wait group for goroutine completion wg := sync.WaitGroup{} // 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() 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 { defer func() { close(c.initCh) }() 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) // set packet len and notify receive c.Length = p.setLen(c.Length) c.initCh <- true // 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 := c.TimeSource.Now(BothClocks) 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 * (c.TimeSource.Now(Monotonic).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 is after the end of the test if !tnext.Before(end) { break } // calculate sleep duration tsleep := c.TimeSource.Now(Monotonic) dsleep := tnext.Sub(tsleep) // sleep t, err = c.Timer.Sleep(ctx, c.TimeSource, 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() } if _, ok := <-c.initCh; !ok { return Errorf(UnexpectedInitChannelClose, "init channel closed unexpectedly") } 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 } heistp-irtt-d858e7f/cmd/000077500000000000000000000000001405065231400151655ustar00rootroot00000000000000heistp-irtt-d858e7f/cmd/irtt/000077500000000000000000000000001405065231400161475ustar00rootroot00000000000000heistp-irtt-d858e7f/cmd/irtt/main.go000066400000000000000000000001411405065231400174160ustar00rootroot00000000000000package main import ( "os" "github.com/heistp/irtt" ) func main() { irtt.RunCLI(os.Args) } heistp-irtt-d858e7f/code_string.go000066400000000000000000000125771405065231400172650ustar00rootroot00000000000000// Code generated by "stringer -type=Code"; DO NOT EDIT. package irtt import "strconv" func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[ShortWrite - -1] _ = x[InvalidDFString - -2] _ = x[FieldsLengthTooLarge - -3] _ = x[FieldsCapacityTooLarge - -4] _ = x[InvalidStampAtString - -5] _ = x[InvalidStampAtInt - -6] _ = x[InvalidAllowStampString - -7] _ = x[InvalidClockString - -8] _ = x[InvalidClockInt - -9] _ = x[BadMagic - -10] _ = x[NoHMAC - -11] _ = x[BadHMAC - -12] _ = x[UnexpectedHMAC - -13] _ = x[NonexclusiveMidpointTStamp - -14] _ = x[InconsistentClocks - -15] _ = x[DFNotSupported - -16] _ = x[InvalidFlagBitsSet - -17] _ = x[ShortParamBuffer - -18] _ = x[ParamOverflow - -19] _ = x[InvalidParamValue - -20] _ = x[ProtocolVersionMismatch - -21] _ = x[NoMatchingInterfaces - -1024] _ = x[NoMatchingInterfacesUp - -1025] _ = x[UnspecifiedWithSpecifiedAddresses - -1026] _ = x[UnexpectedReplyFlag - -1027] _ = x[NoSuitableAddressFound - -1028] _ = x[InvalidConnToken - -1029] _ = x[ShortInterval - -1030] _ = x[LargeRequest - -1031] _ = x[AddressMismatch - -1032] _ = x[SyslogNotSupported - -1033] _ = x[InvalidSyslogURI - -1034] _ = x[InvalidWinAvgWindow - -2048] _ = x[InvalidExpAvgAlpha - -2049] _ = x[AllocateResultsPanic - -2050] _ = x[UnexpectedOpenFlag - -2051] _ = x[DFError - -2052] _ = x[TTLError - -2053] _ = x[ExpectedReplyFlag - -2054] _ = x[ShortReply - -2055] _ = x[StampAtMismatch - -2056] _ = x[ClockMismatch - -2057] _ = x[UnexpectedSequenceNumber - -2058] _ = x[InvalidSleepFactor - -2059] _ = x[InvalidWaitString - -2060] _ = x[InvalidWaitFactor - -2061] _ = x[InvalidWaitDuration - -2062] _ = x[NoSuchAverager - -2063] _ = x[NoSuchFiller - -2064] _ = x[NoSuchTimer - -2065] _ = x[NoSuchTimeSource - -2066] _ = x[NoSuchWaiter - -2067] _ = x[IntervalNonPositive - -2068] _ = x[DurationNonPositive - -2069] _ = x[ConnTokenZero - -2070] _ = x[ServerClosed - -2071] _ = x[OpenTimeout - -2072] _ = x[InvalidServerRestriction - -2073] _ = x[InvalidReceivedStatsInt - -2074] _ = x[InvalidReceivedStatsString - -2075] _ = x[OpenTimeoutTooShort - -2076] _ = x[ServerFillTooLong - -2077] _ = x[UnexpectedInitChannelClose - -2078] _ = x[MultipleAddresses-1024] _ = x[ServerStart-1025] _ = x[ServerStop-1026] _ = x[ListenerStart-1027] _ = x[ListenerStop-1028] _ = x[ListenerError-1029] _ = x[Drop-1030] _ = x[NewConn-1031] _ = x[OpenClose-1032] _ = x[CloseConn-1033] _ = x[NoDSCPSupport-1034] _ = x[ExceededDuration-1035] _ = x[NoReceiveDstAddrSupport-1036] _ = x[RemoveNoConn-1037] _ = x[InvalidServerFill-1038] _ = x[Connecting-2048] _ = x[Connected-2049] _ = x[WaitForPackets-2050] _ = x[ServerRestriction-2051] _ = x[NoTest-2052] _ = x[ConnectedClosed-2053] } const ( _Code_name_0 = "UnexpectedInitChannelCloseServerFillTooLongOpenTimeoutTooShortInvalidReceivedStatsStringInvalidReceivedStatsIntInvalidServerRestrictionOpenTimeoutServerClosedConnTokenZeroDurationNonPositiveIntervalNonPositiveNoSuchWaiterNoSuchTimeSourceNoSuchTimerNoSuchFillerNoSuchAveragerInvalidWaitDurationInvalidWaitFactorInvalidWaitStringInvalidSleepFactorUnexpectedSequenceNumberClockMismatchStampAtMismatchShortReplyExpectedReplyFlagTTLErrorDFErrorUnexpectedOpenFlagAllocateResultsPanicInvalidExpAvgAlphaInvalidWinAvgWindow" _Code_name_1 = "InvalidSyslogURISyslogNotSupportedAddressMismatchLargeRequestShortIntervalInvalidConnTokenNoSuitableAddressFoundUnexpectedReplyFlagUnspecifiedWithSpecifiedAddressesNoMatchingInterfacesUpNoMatchingInterfaces" _Code_name_2 = "ProtocolVersionMismatchInvalidParamValueParamOverflowShortParamBufferInvalidFlagBitsSetDFNotSupportedInconsistentClocksNonexclusiveMidpointTStampUnexpectedHMACBadHMACNoHMACBadMagicInvalidClockIntInvalidClockStringInvalidAllowStampStringInvalidStampAtIntInvalidStampAtStringFieldsCapacityTooLargeFieldsLengthTooLargeInvalidDFStringShortWrite" _Code_name_3 = "MultipleAddressesServerStartServerStopListenerStartListenerStopListenerErrorDropNewConnOpenCloseCloseConnNoDSCPSupportExceededDurationNoReceiveDstAddrSupportRemoveNoConnInvalidServerFill" _Code_name_4 = "ConnectingConnectedWaitForPacketsServerRestrictionNoTestConnectedClosed" ) var ( _Code_index_0 = [...]uint16{0, 26, 43, 62, 88, 111, 135, 146, 158, 171, 190, 209, 221, 237, 248, 260, 274, 293, 310, 327, 345, 369, 382, 397, 407, 424, 432, 439, 457, 477, 495, 514} _Code_index_1 = [...]uint8{0, 16, 34, 49, 61, 74, 90, 112, 131, 164, 186, 206} _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 -2078 <= i && i <= -2048: i -= -2078 return _Code_name_0[_Code_index_0[i]:_Code_index_0[i+1]] case -1034 <= i && i <= -1024: i -= -1034 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) + ")" } } heistp-irtt-d858e7f/conn.go000066400000000000000000000327211405065231400157130ustar00rootroot00000000000000package 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 timeSource TimeSource } func (n *nconn) init(conn *net.UDPConn, ipVer IPVersion, ts TimeSource) { n.conn = conn n.ipVer = ipVer n.df = DFDefault n.timeSource = ts // 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, cfg.TimeSource) // 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 = c.timeSource.Now(BothClocks) p.trcvd = 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 = c.timeSource.Now(BothClocks) p.tsent = 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, ts TimeSource) (l *lconn, err error) { ipVer := IPVersionFromUDPAddr(laddr) var conn *net.UDPConn if conn, err = net.ListenUDP(ipVer.udpNetwork(), laddr); err != nil { return } l = &lconn{nconn: &nconn{}, setSrcIP: setSrcIP && laddr.IP.IsUnspecified()} l.init(conn, ipVer, ts) 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, ts TimeSource) (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, ts) 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 = l.timeSource.Now(BothClocks) p.trcvd = 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 = l.timeSource.Now(BothClocks) p.tsent = 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 } heistp-irtt-d858e7f/context.go000066400000000000000000000002131405065231400164310ustar00rootroot00000000000000package irtt import "context" func isContextError(err error) bool { return err == context.Canceled || err == context.DeadlineExceeded } heistp-irtt-d858e7f/defaults.go000066400000000000000000000076251405065231400165720ustar00rootroot00000000000000package 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) // DefaultTimeSource is the default TimeSource implementation (WindowsTimeSource // for Windows and GoTimeSource for everything else). var DefaultTimeSource = NewDefaultTimeSource() // 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 ) // 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 heistp-irtt-d858e7f/df.go000066400000000000000000000010621405065231400153410ustar00rootroot00000000000000package 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) } heistp-irtt-d858e7f/doc/000077500000000000000000000000001405065231400151675ustar00rootroot00000000000000heistp-irtt-d858e7f/doc/head.html000066400000000000000000000011511405065231400167540ustar00rootroot00000000000000 heistp-irtt-d858e7f/doc/irtt-client.1000066400000000000000000000722111405065231400175120ustar00rootroot00000000000000'\" t .\" Automatically generated by Pandoc 2.13 .\" .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[R]] .SH DESCRIPTION .PP \f[I]irtt client\f[R] is the client for irtt(1). .SH OPTIONS .TP -d \f[I]duration\f[R] Total time to send (default 1m0s, see Duration units below) .TP -i \f[I]interval\f[R] Send interval (default 1s, see Duration units below) .TP -l \f[I]length\f[R] 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 -o \f[I]file\f[R] Write JSON output to file (use `-' for stdout). The extension used for \f[I]file\f[R] 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 -q Quiet, suppress per-packet output .TP -Q Really quiet, suppress all output except errors to stderr .TP -n No test, connect to the server and validate test parameters but don\[cq]t run the test .TP --stats=\f[I]stats\f[R] Server stats on received packets (default \f[I]both\f[R]). Possible values: .RS .PP .TS tab(@); l l. T{ Value T}@T{ Meaning T} _ T{ \f[I]none\f[R] T}@T{ no server stats on received packets T} T{ \f[I]count\f[R] T}@T{ total count of received packets T} T{ \f[I]window\f[R] T}@T{ receipt status of last 64 packets with each reply T} T{ \f[I]both\f[R] T}@T{ both count and window T} .TE .RE .TP --tstamp=\f[I]mode\f[R] Server timestamp mode (default \f[I]both\f[R]). Possible values: .RS .PP .TS tab(@); l l. T{ Value T}@T{ Meaning T} _ T{ \f[I]none\f[R] T}@T{ request no timestamps T} T{ \f[I]send\f[R] T}@T{ request timestamp at server send T} T{ \f[I]receive\f[R] T}@T{ request timestamp at server receive T} T{ \f[I]both\f[R] T}@T{ request both send and receive timestamps T} T{ \f[I]midpoint\f[R] T}@T{ request midpoint timestamp (send/receive avg) T} .TE .RE .TP --clock=\f[I]clock\f[R] Clock/s used for server timestamps (default \f[I]both\f[R]). Possible values: .RS .PP .TS tab(@); l l. T{ Value T}@T{ Meaning T} _ T{ \f[I]wall\f[R] T}@T{ wall clock only T} T{ \f[I]monotonic\f[R] T}@T{ monotonic clock only T} T{ \f[I]both\f[R] T}@T{ both clocks T} .TE .RE .TP --dscp=\f[I]dscp\f[R] 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 --df=\f[I]DF\f[R] 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[R] T}@T{ OS default T} T{ \f[I]false\f[R] T}@T{ DF bit not set T} T{ \f[I]true\f[R] T}@T{ DF bit set T} .TE .RE .TP --wait=\f[I]wait\f[R] 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[R]duration T}@T{ # times max RTT, or duration if no response T} T{ #\f[I]r\f[R]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 --timer=\f[I]timer\f[R] 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[R] T}@T{ Go\[cq]s standard time.Timer T} T{ \f[I]comp\f[R] T}@T{ Simple timer with error compensation (see -tcomp) T} T{ \f[I]hybrid:\f[R]# T}@T{ Hybrid comp/busy timer with sleep factor (default 0.95) T} T{ \f[I]busy\f[R] T}@T{ busy wait loop (high precision and CPU, blasphemy) T} .TE .RE .TP --tcomp=\f[I]alg\f[R] 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[R] T}@T{ Cumulative average error T} T{ \f[I]win:\f[R]# T}@T{ Moving average error with window # (default 5) T} T{ \f[I]exp:\f[R]# T}@T{ Exponential average with alpha # (default 0.10) T} .TE .RE .TP --fill=\f[I]fill\f[R] 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[R] T}@T{ Leave payload as all zeroes T} T{ \f[I]rand\f[R] T}@T{ Use random bytes from Go\[cq]s math.rand T} T{ \f[I]pattern:\f[R]XX T}@T{ Use repeating pattern of hex (default 69727474) T} .TE .RE .TP --fill-one Fill only once and repeat for all packets .TP --sfill=fill Request server fill (default not specified). See values for \[en]fill. Server must support and allow this fill with \[en]allow-fills. .TP --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[R] T}@T{ Unspecified address (all IPv4/IPv6 addresses) with port T} T{ \f[I]host\f[R] T}@T{ Host with dynamic port, see Host formats below T} T{ \f[I]host:port\f[R] T}@T{ Host with specified port, see Host formats below T} .TE .RE .TP --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 -4 IPv4 only .TP -6 IPv6 only .TP --timeouts=\f[I]durations\f[R] 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. .TP --ttl=\f[I]ttl\f[R] Time to live (default 0, meaning use OS default) .TP --loose Accept and use any server restricted test parameters instead of exiting with nonzero status. .TP --thread Lock sending and receiving goroutines to OS threads .TP -h Show help .TP -v Show version .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[R] 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\[cq]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[R]. .SS version .PP version information .IP .nf \f[C] \[dq]version\[dq]: { \[dq]irtt\[dq]: \[dq]0.9.0\[dq], \[dq]protocol\[dq]: 1, \[dq]json_format\[dq]: 1 }, \f[R] .fi .IP \[bu] 2 \f[I]irtt\f[R] the IRTT version number .IP \[bu] 2 \f[I]protocol\f[R] the protocol version number (increments mean incompatible changes) .IP \[bu] 2 \f[I]json_format\f[R] the JSON format number (increments mean incompatible changes) .SS system_info .PP a few basic pieces of system information .IP .nf \f[C] \[dq]system_info\[dq]: { \[dq]os\[dq]: \[dq]darwin\[dq], \[dq]cpus\[dq]: 8, \[dq]go_version\[dq]: \[dq]go1.9.2\[dq], \[dq]hostname\[dq]: \[dq]tron.local\[dq] }, \f[R] .fi .IP \[bu] 2 \f[I]os\f[R] the Operating System from Go\[cq]s \f[I]runtime.GOOS\f[R] .IP \[bu] 2 \f[I]cpus\f[R] the number of CPUs reported by Go\[cq]s \f[I]runtime.NumCPU()\f[R], 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[R] the version of Go the executable was built with .IP \[bu] 2 \f[I]hostname\f[R] the local hostname .SS config .PP the configuration used for the test .IP .nf \f[C] \[dq]config\[dq]: { \[dq]local_address\[dq]: \[dq]127.0.0.1:51203\[dq], \[dq]remote_address\[dq]: \[dq]127.0.0.1:2112\[dq], \[dq]open_timeouts\[dq]: \[dq]1s,2s,4s,8s\[dq], \[dq]params\[dq]: { \[dq]proto_version\[dq]: 1, \[dq]duration\[dq]: 600000000, \[dq]interval\[dq]: 200000000, \[dq]length\[dq]: 48, \[dq]received_stats\[dq]: \[dq]both\[dq], \[dq]stamp_at\[dq]: \[dq]both\[dq], \[dq]clock\[dq]: \[dq]both\[dq], \[dq]dscp\[dq]: 0, \[dq]server_fill\[dq]: \[dq]\[dq] }, \[dq]loose\[dq]: false, \[dq]ip_version\[dq]: \[dq]IPv4\[dq], \[dq]df\[dq]: 0, \[dq]ttl\[dq]: 0, \[dq]timer\[dq]: \[dq]comp\[dq], \[dq]waiter\[dq]: \[dq]3x4s\[dq], \[dq]filler\[dq]: \[dq]none\[dq], \[dq]fill_one\[dq]: false, \[dq]thread_lock\[dq]: false, \[dq]supplied\[dq]: { \[dq]local_address\[dq]: \[dq]:0\[dq], \[dq]remote_address\[dq]: \[dq]localhost\[dq], \[dq]open_timeouts\[dq]: \[dq]1s,2s,4s,8s\[dq], \[dq]params\[dq]: { \[dq]proto_version\[dq]: 1, \[dq]duration\[dq]: 600000000, \[dq]interval\[dq]: 200000000, \[dq]length\[dq]: 0, \[dq]received_stats\[dq]: \[dq]both\[dq], \[dq]stamp_at\[dq]: \[dq]both\[dq], \[dq]clock\[dq]: \[dq]both\[dq], \[dq]dscp\[dq]: 0, \[dq]server_fill\[dq]: \[dq]\[dq] }, \[dq]loose\[dq]: false, \[dq]ip_version\[dq]: \[dq]IPv4+6\[dq], \[dq]df\[dq]: 0, \[dq]ttl\[dq]: 0, \[dq]timer\[dq]: \[dq]comp\[dq], \[dq]waiter\[dq]: \[dq]3x4s\[dq], \[dq]filler\[dq]: \[dq]none\[dq], \[dq]fill_one\[dq]: false, \[dq]thread_lock\[dq]: false } }, \f[R] .fi .IP \[bu] 2 \f[I]local_address\f[R] the local address (IP:port) for the client .IP \[bu] 2 \f[I]remote_address\f[R] the remote address (IP:port) for the server .IP \[bu] 2 \f[I]open_timeouts\f[R] a list of timeout durations used after an open packet is sent .IP \[bu] 2 \f[I]params\f[R] are the parameters that were negotiated with the server, including: .RS 2 .IP \[bu] 2 \f[I]proto_version\f[R] protocol version number .IP \[bu] 2 \f[I]duration\f[R] duration of the test, in nanoseconds .IP \[bu] 2 \f[I]interval\f[R] send interval, in nanoseconds .IP \[bu] 2 \f[I]length\f[R] packet length .IP \[bu] 2 \f[I]received_stats\f[R] statistics for packets received by server (none, count, window or both, \f[I]--stats\f[R] flag for irtt client) .IP \[bu] 2 \f[I]stamp_at\f[R] timestamp selection parameter (none, send, receive, both or midpoint, \f[I]--tstamp\f[R] flag for irtt client) .IP \[bu] 2 \f[I]clock\f[R] clock selection parameter (wall or monotonic, \f[I]--clock\f[R] flag for irtt client) .IP \[bu] 2 \f[I]dscp\f[R] the DSCP (https://en.wikipedia.org/wiki/Differentiated_services) value .IP \[bu] 2 \f[I]server_fill\f[R] the requested server fill (\f[I]--sfill\f[R] flag for irtt client) .RE .IP \[bu] 2 \f[I]loose\f[R] if true, client accepts and uses restricted server parameters, with a warning .IP \[bu] 2 \f[I]ip_version\f[R] the IP version used (IPv4 or IPv6) .IP \[bu] 2 \f[I]df\f[R] the do-not-fragment setting (0 == OS default, 1 == false, 2 == true) .IP \[bu] 2 \f[I]ttl\f[R] the IP time-to-live (https://en.wikipedia.org/wiki/Time_to_live) value .IP \[bu] 2 \f[I]timer\f[R] the timer used: simple, comp, hybrid or busy (irtt client --timer flag) .IP \[bu] 2 \f[I]time_source\f[R] the time source used: go or windows .IP \[bu] 2 \f[I]waiter\f[R] the waiter used: fixed duration, multiple of RTT or multiple of max RTT (irtt client \f[I]--wait\f[R] flag) .IP \[bu] 2 \f[I]filler\f[R] the packet filler used: none, rand or pattern (irtt client \f[I]--fill\f[R] flag) .IP \[bu] 2 \f[I]fill_one\f[R] whether to fill only once and repeat for all packets (irtt client \f[I]--fill-one\f[R] flag) .IP \[bu] 2 \f[I]thread_lock\f[R] whether to lock packet handling goroutines to OS threads .IP \[bu] 2 \f[I]supplied\f[R] a nested \f[I]config\f[R] object with the configuration as originally supplied to the API or \f[I]irtt\f[R] command. The supplied configuration can differ from the final configuration in the following ways: .RS 2 .IP \[bu] 2 \f[I]local_address\f[R] and \f[I]remote_address\f[R] may have hostnames or named ports before being resolved to an IP and numbered port .IP \[bu] 2 \f[I]ip_version\f[R] may be IPv4+6 before it is determined after address resolution .IP \[bu] 2 \f[I]params\f[R] may be different before the server applies restrictions based on its configuration .RE .SS stats .PP statistics for the results .IP .nf \f[C] \[dq]stats\[dq]: { \[dq]start_time\[dq]: { \[dq]wall\[dq]: 1528621979787034330, \[dq]monotonic\[dq]: 5136247 }, \[dq]send_call\[dq]: { \[dq]total\[dq]: 79547, \[dq]n\[dq]: 3, \[dq]min\[dq]: 17790, \[dq]max\[dq]: 33926, \[dq]mean\[dq]: 26515, \[dq]stddev\[dq]: 8148, \[dq]variance\[dq]: 66390200 }, \[dq]timer_error\[dq]: { \[dq]total\[dq]: 227261, \[dq]n\[dq]: 2, \[dq]min\[dq]: 59003, \[dq]max\[dq]: 168258, \[dq]mean\[dq]: 113630, \[dq]stddev\[dq]: 77254, \[dq]variance\[dq]: 5968327512 }, \[dq]rtt\[dq]: { \[dq]total\[dq]: 233915, \[dq]n\[dq]: 2, \[dq]min\[dq]: 99455, \[dq]max\[dq]: 134460, \[dq]mean\[dq]: 116957, \[dq]median\[dq]: 116957, \[dq]stddev\[dq]: 24752, \[dq]variance\[dq]: 612675012 }, \[dq]send_delay\[dq]: { \[dq]total\[dq]: 143470, \[dq]n\[dq]: 2, \[dq]min\[dq]: 54187, \[dq]max\[dq]: 89283, \[dq]mean\[dq]: 71735, \[dq]median\[dq]: 71735, \[dq]stddev\[dq]: 24816, \[dq]variance\[dq]: 615864608 }, \[dq]receive_delay\[dq]: { \[dq]total\[dq]: 90445, \[dq]n\[dq]: 2, \[dq]min\[dq]: 45177, \[dq]max\[dq]: 45268, \[dq]mean\[dq]: 45222, \[dq]median\[dq]: 45222, \[dq]stddev\[dq]: 64, \[dq]variance\[dq]: 4140 }, \[dq]server_packets_received\[dq]: 2, \[dq]bytes_sent\[dq]: 144, \[dq]bytes_received\[dq]: 96, \[dq]duplicates\[dq]: 0, \[dq]late_packets\[dq]: 0, \[dq]wait\[dq]: 403380, \[dq]duration\[dq]: 400964028, \[dq]packets_sent\[dq]: 3, \[dq]packets_received\[dq]: 2, \[dq]packet_loss_percent\[dq]: 33.333333333333336, \[dq]upstream_loss_percent\[dq]: 33.333333333333336, \[dq]downstream_loss_percent\[dq]: 0, \[dq]duplicate_percent\[dq]: 0, \[dq]late_packets_percent\[dq]: 0, \[dq]ipdv_send\[dq]: { \[dq]total\[dq]: 35096, \[dq]n\[dq]: 1, \[dq]min\[dq]: 35096, \[dq]max\[dq]: 35096, \[dq]mean\[dq]: 35096, \[dq]median\[dq]: 35096, \[dq]stddev\[dq]: 0, \[dq]variance\[dq]: 0 }, \[dq]ipdv_receive\[dq]: { \[dq]total\[dq]: 91, \[dq]n\[dq]: 1, \[dq]min\[dq]: 91, \[dq]max\[dq]: 91, \[dq]mean\[dq]: 91, \[dq]median\[dq]: 91, \[dq]stddev\[dq]: 0, \[dq]variance\[dq]: 0 }, \[dq]ipdv_round_trip\[dq]: { \[dq]total\[dq]: 35005, \[dq]n\[dq]: 1, \[dq]min\[dq]: 35005, \[dq]max\[dq]: 35005, \[dq]mean\[dq]: 35005, \[dq]median\[dq]: 35005, \[dq]stddev\[dq]: 0, \[dq]variance\[dq]: 0 }, \[dq]server_processing_time\[dq]: { \[dq]total\[dq]: 20931, \[dq]n\[dq]: 2, \[dq]min\[dq]: 9979, \[dq]max\[dq]: 10952, \[dq]mean\[dq]: 10465, \[dq]stddev\[dq]: 688, \[dq]variance\[dq]: 473364 }, \[dq]timer_err_percent\[dq]: 0.056815, \[dq]timer_misses\[dq]: 0, \[dq]timer_miss_percent\[dq]: 0, \[dq]send_rate\[dq]: { \[dq]bps\[dq]: 2878, \[dq]string\[dq]: \[dq]2.9 Kbps\[dq] }, \[dq]receive_rate\[dq]: { \[dq]bps\[dq]: 3839, \[dq]string\[dq]: \[dq]3.8 Kbps\[dq] } }, \f[R] .fi .PP \f[B]Note:\f[R] In the \f[I]stats\f[R] object, a \f[I]duration stats\f[R] 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[R] the total of the duration values .IP \[bu] 2 \f[I]n\f[R] the number of duration values .IP \[bu] 2 \f[I]min\f[R] the minimum duration value .IP \[bu] 2 \f[I]max\f[R] the maximum duration value .IP \[bu] 2 \f[I]mean\f[R] the mean duration value .IP \[bu] 2 \f[I]stddev\f[R] the standard deviation .IP \[bu] 2 \f[I]variance\f[R] the variance .PP The regular attributes in \f[I]stats\f[R] are as follows: .IP \[bu] 2 \f[I]start_time\f[R] the start time of the test (see \f[I]round_trips\f[R] Notes for descriptions of \f[I]wall\f[R] and \f[I]monotonic\f[R] values) .IP \[bu] 2 \f[I]send_call\f[R] a duration stats object for the call time when sending packets .IP \[bu] 2 \f[I]timer_error\f[R] a duration stats object for the observed sleep time error .IP \[bu] 2 \f[I]rtt\f[R] a duration stats object for the round-trip time .IP \[bu] 2 \f[I]send_delay\f[R] a duration stats object for the one-way send delay \f[B](only available if server timestamps are enabled)\f[R] .IP \[bu] 2 \f[I]receive_delay\f[R] a duration stats object for the one-way receive delay \f[B](only available if server timestamps are enabled)\f[R] .IP \[bu] 2 \f[I]server_packets_received\f[R] the number of packets received by the server, including duplicates (always present, but only valid if the \f[I]ReceivedStats\f[R] parameter includes \f[I]ReceivedStatsCount\f[R], or the \f[I]--stats\f[R] flag to the irtt client is \f[I]count\f[R] or \f[I]both\f[R]) .IP \[bu] 2 \f[I]bytes_sent\f[R] the number of UDP payload bytes sent during the test .IP \[bu] 2 \f[I]bytes_received\f[R] the number of UDP payload bytes received during the test .IP \[bu] 2 \f[I]duplicates\f[R] the number of packets received with the same sequence number .IP \[bu] 2 \f[I]late_packets\f[R] 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[R] the actual time spent waiting for final packets, in nanoseconds .IP \[bu] 2 \f[I]duration\f[R] 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[R] the number of packets sent to the server .IP \[bu] 2 \f[I]packets_received\f[R] the number of packets received from the server .IP \[bu] 2 \f[I]packet_loss_percent\f[R] 100 * (\f[I]packets_sent\f[R] - \f[I]packets_received\f[R]) / \f[I]packets_sent\f[R] .IP \[bu] 2 \f[I]upstream_loss_percent\f[R] 100 * (\f[I]packets_sent\f[R] - \f[I]server_packets_received\f[R] / \f[I]packets_sent\f[R]) (always present, but only valid if \f[I]server_packets_received\f[R] is valid) .IP \[bu] 2 \f[I]downstream_loss_percent\f[R] 100 * (\f[I]server_packets_received\f[R] - \f[I]packets_received\f[R] / \f[I]server_packets_received\f[R]) (always present, but only valid if \f[I]server_packets_received\f[R] is valid) .IP \[bu] 2 \f[I]duplicate_percent\f[R] 100 * \f[I]duplicates\f[R] / \f[I]packets_received\f[R] .IP \[bu] 2 \f[I]late_packets_percent\f[R] 100 * \f[I]late_packets\f[R] / \f[I]packets_received\f[R] .IP \[bu] 2 \f[I]ipdv_send\f[R] 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[R] .IP \[bu] 2 \f[I]ipdv_receive\f[R] 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[R] .IP \[bu] 2 \f[I]ipdv_round_trip\f[R] 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[R] .IP \[bu] 2 \f[I]server_processing_time\f[R] 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[R] .IP \[bu] 2 \f[I]timer_err_percent\f[R] the mean of the absolute values of the timer error, as a percentage of the interval .IP \[bu] 2 \f[I]timer_misses\f[R] 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[R] 100 * \f[I]timer_misses\f[R] / expected packets sent .IP \[bu] 2 \f[I]send_rate\f[R] 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[R] 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] \[dq]round_trips\[dq]: [ { \[dq]seqno\[dq]: 0, \[dq]lost\[dq]: false, \[dq]timestamps\[dq]: { \[dq]client\[dq]: { \[dq]receive\[dq]: { \[dq]wall\[dq]: 1508180723502871779, \[dq]monotonic\[dq]: 2921143 }, \[dq]send\[dq]: { \[dq]wall\[dq]: 1508180723502727340, \[dq]monotonic\[dq]: 2776704 } }, \[dq]server\[dq]: { \[dq]receive\[dq]: { \[dq]wall\[dq]: 1508180723502816623, \[dq]monotonic\[dq]: 32644353327 }, \[dq]send\[dq]: { \[dq]wall\[dq]: 1508180723502826602, \[dq]monotonic\[dq]: 32644363306 } } }, \[dq]delay\[dq]: { \[dq]receive\[dq]: 45177, \[dq]rtt\[dq]: 134460, \[dq]send\[dq]: 89283 }, \[dq]ipdv\[dq]: {} }, { \[dq]seqno\[dq]: 1, \[dq]lost\[dq]: false, \[dq]timestamps\[dq]: { \[dq]client\[dq]: { \[dq]receive\[dq]: { \[dq]wall\[dq]: 1508180723702917735, \[dq]monotonic\[dq]: 202967099 }, \[dq]send\[dq]: { \[dq]wall\[dq]: 1508180723702807328, \[dq]monotonic\[dq]: 202856692 } }, \[dq]server\[dq]: { \[dq]receive\[dq]: { \[dq]wall\[dq]: 1508180723702861515, \[dq]monotonic\[dq]: 32844398219 }, \[dq]send\[dq]: { \[dq]wall\[dq]: 1508180723702872467, \[dq]monotonic\[dq]: 32844409171 } } }, \[dq]delay\[dq]: { \[dq]receive\[dq]: 45268, \[dq]rtt\[dq]: 99455, \[dq]send\[dq]: 54187 }, \[dq]ipdv\[dq]: { \[dq]receive\[dq]: 91, \[dq]rtt\[dq]: -35005, \[dq]send\[dq]: -35096 } }, { \[dq]seqno\[dq]: 2, \[dq]lost\[dq]: true, \[dq]timestamps\[dq]: { \[dq]client\[dq]: { \[dq]receive\[dq]: {}, \[dq]send\[dq]: { \[dq]wall\[dq]: 1508180723902925971, \[dq]monotonic\[dq]: 402975335 } }, \[dq]server\[dq]: { \[dq]receive\[dq]: {}, \[dq]send\[dq]: {} } }, \[dq]delay\[dq]: {}, \[dq]ipdv\[dq]: {} } ] \f[R] .fi .PP \f[B]Note:\f[R] \f[I]wall\f[R] values are from Go\[cq]s \f[I]time.Time.UnixNano()\f[R], the number of nanoseconds elapsed since January 1, 1970 UTC .PP \f[B]Note:\f[R] \f[I]monotonic\f[R] values are the number of nanoseconds since some arbitrary point in time, so can only be relied on to measure duration .IP \[bu] 2 \f[I]seqno\f[R] the sequence number .IP \[bu] 2 \f[I]lost\f[R] the lost status of the packet, which can be one of \f[I]false\f[R], \f[I]true\f[R], \f[I]true_down\f[R] or \f[I]true_up\f[R]. The \f[I]true_down\f[R] and \f[I]true_up\f[R] values are only possible if the \f[I]ReceivedStats\f[R] parameter includes \f[I]ReceivedStatsWindow\f[R] (irtt client \f[I]--stats\f[R] flag). Even then, if it could not be determined whether the packet was lost upstream or downstream, the value \f[I]true\f[R] is used. .IP \[bu] 2 \f[I]timestamps\f[R] the client and server timestamps .RS 2 .IP \[bu] 2 \f[I]client\f[R] 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[R] .IP \[bu] 2 \f[I]server\f[R] 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[R], and additionally: .RS 2 .IP \[bu] 2 \f[I]send\f[R] values are not present if the StampAt (irtt client \f[I]--tstamp\f[R] flag) does not include send timestamps .IP \[bu] 2 \f[I]receive\f[R] values are not present if the StampAt (irtt client \f[I]--tstamp\f[R] flag) does not include receive timestamps .IP \[bu] 2 \f[I]wall\f[R] values are not present if the Clock (irtt client \f[I]--clock\f[R] flag) does not include wall values or server timestamps are not enabled .IP \[bu] 2 \f[I]monotonic\f[R] values are not present if the Clock (irtt client \f[I]--clock\f[R] flag) does not include monotonic values or server timestamps are not enabled .RE .RE .IP \[bu] 2 \f[I]delay\f[R] an object containing the delay values .RS 2 .IP \[bu] 2 \f[I]receive\f[R] 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[R] .IP \[bu] 2 \f[I]rtt\f[R] the round-trip time, in nanoseconds, always present .IP \[bu] 2 \f[I]send\f[R] 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[R] .RE .IP \[bu] 2 \f[I]ipdv\f[R] 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[R] .RS 2 .IP \[bu] 2 \f[I]receive\f[R] the difference in receive delay relative to the previous packet \f[B](present only if at least one server timestamp is available)\f[R] .IP \[bu] 2 \f[I]rtt\f[R] the difference in round-trip time relative to the previous packet (always present for \f[I]seqno\f[R] > 0) .IP \[bu] 2 \f[I]send\f[R] the difference in send delay relative to the previous packet \f[B](present only if at least one server timestamp is available)\f[R] .RE .SH EXIT STATUS .PP \f[I]irtt client\f[R] 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 $ irtt client localhost Sends requests once per second for one minute to localhost. .TP $ irtt client -i 200ms -d 10s -o - localhost Sends requests every 0.2 sec for 10 seconds to localhost. Writes JSON output to stdout. .TP $ 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. .TP $ 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). .TP $ 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[R]. .SH SEE ALSO .PP irtt(1), irtt-server(1) .PP IRTT GitHub repository (https://github.com/heistp/irtt/) heistp-irtt-d858e7f/doc/irtt-client.html000066400000000000000000001132741405065231400203230ustar00rootroot00000000000000IRTT-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:

  • 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

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

  • Dropping of all packets without a correct HMAC
  • Protection for server against unauthorized discovery and use
-4
IPv4 only
-6
IPv6 only
--timeouts=durations
Timeouts used when connecting to server (default 1s,2s,4s,8s). Comma separated list of durations (see Duration units below). Total wait time will be up to the sum of these Durations. Max packets sent is up to the number of Durations. Minimum timeout duration is 200ms.
--ttl=ttl
Time to live (default 0, meaning use OS default)
--loose
Accept and use any server restricted test parameters instead of exiting with nonzero status.
--thread
Lock sending and receiving goroutines to OS threads
-h
Show help
-v
Show version

Host formats

Hosts may be either hostnames (for IPv4 or IPv6) or IP addresses. IPv6 addresses must be surrounded by brackets and may include a zone after the % character. Examples:

Type Example
IPv4 IP 192.168.1.10
IPv6 IP [2001:db8:8f::2/32]
IPv4/6 hostname localhost

Note: IPv6 addresses must be quoted in most shells.

Duration units

Durations are a sequence of decimal numbers, each with optional fraction, and unit suffix, such as: “300ms”, “1m30s” or “2.5m”. Sanity not enforced.

Suffix Unit
h hours
m minutes
s seconds
ms milliseconds
ns nanoseconds

OUTPUT

IRTT’s JSON output format consists of five top-level objects:

  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
},
  • 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 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 value
  • timer the timer used: simple, comp, hybrid or busy (irtt client --timer flag)
  • time_source the time source used: go or windows
  • 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": {
        "wall": 1528621979787034330,
        "monotonic": 5136247
    },
    "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 (see round_trips Notes for descriptions of wall and monotonic values)
  • 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 (only available if server timestamps are enabled)
  • ipdv_receive a duration stats object for the receive IPDV (only available if server timestamps are enabled)
  • ipdv_round_trip a duration stats object for the round-trip IPDV (available regardless of whether server timestamps are enabled or not)
  • server_processing_time a duration stats object for the time the server took after it received the packet to when it sent the response (only available when both send and receive timestamps are enabled)
  • timer_err_percent the mean of the absolute values of the timer error, as a percentage of the interval
  • timer_misses the number of times the timer missed the interval (was at least 50% over the scheduled time)
  • timer_miss_percent 100 * timer_misses / expected packets sent
  • send_rate the send bitrate (bits-per-second and corresponding string), calculated using the number of UDP payload bytes sent between the time right before the first send call and the time right after the last send call
  • receive_rate the receive bitrate (bits-per-second and corresponding string), calculated using the number of UDP payload bytes received between the time right after the first receive call and the time right after the last receive call

round_trips

each round-trip is a single request to / reply from the server

"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 some arbitrary point in time, so can only be relied on to measure duration

  • seqno the sequence number
  • lost the lost status of the packet, which can be one of false, true, true_down or true_up. The true_down and true_up values are only possible if the ReceivedStats parameter includes ReceivedStatsWindow (irtt client --stats flag). Even then, if it could not be determined whether the packet was lost upstream or downstream, the value true is used.
  • timestamps the client and server timestamps
    • client the client send and receive wall and monotonic timestamps (receive values only present if lost is false)
    • server the server send and receive wall and monotonic timestamps (both send and receive values not present if lost is true), and additionally:
      • send values are not present if the StampAt (irtt client --tstamp flag) does not include send timestamps
      • receive values are not present if the StampAt (irtt client --tstamp flag) does not include receive timestamps
      • wall values are not present if the Clock (irtt client --clock flag) does not include wall values or server timestamps are not enabled
      • monotonic values are not present if the Clock (irtt client --clock flag) does not include monotonic values or server timestamps are not enabled
  • delay an object containing the delay values
    • receive the one-way receive delay, in nanoseconds (present only if server timestamps are enabled and at least one wall clock value is available)
    • rtt the round-trip time, in nanoseconds, always present
    • send the one-way send delay, in nanoseconds (present only if server timestamps are enabled and at least one wall clock value is available)
  • ipdv an object containing the IPDV values (attributes present only for seqno > 0, and if lost is false for both the current and previous round_trip)
    • receive the difference in receive delay relative to the previous packet (present only if at least one server timestamp is available)
    • rtt the difference in round-trip time relative to the previous packet (always present for seqno > 0)
    • send the difference in send delay relative to the previous packet (present only if at least one server timestamp is available)

EXIT STATUS

irtt client exits with one of the following status codes:

Code Meaning
0 Success
1 Runtime error
2 Command line error
3 Two interrupt signals received

WARNINGS

It is possible with the irtt client to dramatically harm network performance by using intervals that are too low, particularly in combination with large packet lengths. Careful consideration should be given before using sub-millisecond intervals, not only because of the impact on the network, but also because:

  • Timer accuracy at sub-millisecond intervals may begin to suffer without the use of a custom kernel or the busy timer (which pins the CPU)
  • Memory consumption for results storage and system CPU time both rise rapidly
  • The granularity of the results reported may very well not be required

EXAMPLES

$ irtt client localhost
Sends requests once per second for one minute to localhost.
$ irtt client -i 200ms -d 10s -o - localhost
Sends requests every 0.2 sec for 10 seconds to localhost. Writes JSON output to stdout.
$ 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

heistp-irtt-d858e7f/doc/irtt-client.md000066400000000000000000000606111405065231400177530ustar00rootroot00000000000000% 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) - *time_source* the time source used: go or windows - *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": { "wall": 1528621979787034330, "monotonic": 5136247 }, "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 (see *round_trips* Notes for descriptions of *wall* and *monotonic* values) - *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 some arbitrary point in time, so can only be relied on to measure duration - *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/heistp/irtt/) heistp-irtt-d858e7f/doc/irtt-server.1000066400000000000000000000155371405065231400175520ustar00rootroot00000000000000'\" t .\" Automatically generated by Pandoc 2.13 .\" .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[R]] .SH DESCRIPTION .PP \f[I]irtt server\f[R] is the server for irtt(1). .SH OPTIONS .TP -b \f[I]addresses\f[R] 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[R] iface strings may contain * to match multiple interfaces .RE .TP -d \f[I]duration\f[R] Max test duration, or 0 for no maximum (default 0s, see Duration units below) .TP -i \f[I]interval\f[R] Min send interval, or 0 for no minimum (default 10ms, see Duration units below) .TP -l \f[I]length\f[R] Max packet length (default 0), or 0 for no maximum. Numbers less than size of required headers will cause test packets to be dropped. .TP --hmac=\f[I]key\f[R] Add HMAC with \f[I]key\f[R] (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 --syslog=\f[I]uri\f[R] Log events to syslog (default don\[cq]t use syslog). URI format: protocol://host:port/tag. Examples: .RS .PP .TS tab(@); l l. T{ URI T}@T{ Result T} _ T{ local: T}@T{ Log to local syslog, default tag irtt T} T{ local:/irttsrv T}@T{ Log to local syslog, tag irttsrv T} T{ udp://logsrv:514/irttsrv T}@T{ UDP to logsrv:514, tag irttsrv T} T{ tcp://logsrv:8514/ T}@T{ TCP to logsrv:8514, default tag irtt T} .TE .PP \f[B]Note:\f[R] not available on Windows, Plan 9 or Google Native Client .RE .TP --timeout=\f[I]duration\f[R] 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. .TP --pburst=\f[I]#\f[R] Packet burst allowed before enforcing minimum interval (default 5) .TP --fill=\f[I]fill\f[R] 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[R] T}@T{ Echo client payload (insecure on public servers) T} T{ \f[I]rand\f[R] T}@T{ Use random bytes from Go\[cq]s math.rand T} T{ \f[I]pattern:\f[R]XX T}@T{ Use repeating pattern of hex (default 69727474) T} .TE .RE .TP --allow-fills=\f[I]fills\f[R] Comma separated patterns of fill requests to allow (default rand). See options for \f[I]\[en]fill\f[R]. 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=\[dq]\[dq]\f[R] to disallow all fill requests .RE .TP --tstamp=\f[I]modes\f[R] 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[R] T}@T{ Don\[cq]t allow any timestamps T} T{ \f[I]single\f[R] T}@T{ Allow a single timestamp (send, receive or midpoint) T} T{ \f[I]dual\f[R] T}@T{ Allow dual timestamps T} .TE .RE .TP --no-dscp Don\[cq]t allow setting dscp (default false) .TP --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) .TP --thread Lock request handling goroutines to OS threads .TP -h Show help .TP -v Show version .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[R] 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\[cq]s open to the outside world requires some additional attention. For starters, the command line flags should be used to, at a minimum: .IP \[bu] 2 Restrict the duration (\f[I]-d\f[R]), interval (\f[I]-i\f[R]) and length (\f[I]-l\f[R]) of tests, particularly for public servers .IP \[bu] 2 Set an HMAC key (\f[I]--hmac\f[R]) 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 some 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 Installing irtt in an unprivileged container .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\[cq]s high-level language constructs for memory management, and at this time IRTT makes no use of Go\[cq]s unsafe (https://golang.org/pkg/unsafe/) package. .SH EXIT STATUS .PP \f[I]irtt server\f[R] 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 $ irtt server Starts the server and listens on all addresses (unspecified address) .TP $ irtt server -d 30s -i 20ms -l 256 --fill=rand --allow-fills=\[dq]\[dq] 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. .TP $ 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[R], otherwise packets are dropped. .SH SEE ALSO .PP irtt(1), irtt-client(1) .PP IRTT GitHub repository (https://github.com/heistp/irtt/) heistp-irtt-d858e7f/doc/irtt-server.html000066400000000000000000000321631405065231400203500ustar00rootroot00000000000000 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:

  • Dropping of all packets without a correct HMAC
  • Protection for server against unauthorized discovery and use
--syslog=uri

Log events to syslog (default don’t use syslog). URI format: protocol://host:port/tag. Examples:

URI Result
local: Log to local syslog, default tag irtt
local:/irttsrv Log to local syslog, tag irttsrv
udp://logsrv:514/irttsrv UDP to logsrv:514, tag irttsrv
tcp://logsrv:8514/ TCP to logsrv:8514, default tag irtt

Note: not available on Windows, Plan 9 or Google Native Client

--timeout=duration
Timeout for closing connections if no requests received on a connection (default 1m0s, see Duration units below). 0 means no timeout (not recommended, especially on public servers). Max client interval will be restricted to timeout/4.
--pburst=#
Packet burst allowed before enforcing minimum interval (default 5)
--fill=fill

Payload fill if not requested (default pattern:69727474). Possible values include:

Value Fill
none Echo client payload (insecure on public servers)
rand Use random bytes from Go’s math.rand
pattern:XX Use repeating pattern of hex (default 69727474)
--allow-fills=fills

Comma separated patterns of fill requests to allow (default rand). See options for –fill. Notes:

  • Patterns may contain * for matching
  • Allowing non-random fills insecure on public servers
  • Use --allow-fills="" to disallow all fill requests
--tstamp=modes

Timestamp modes to allow (default dual). Possible values:

Value Allowed Timestamps
none Don’t allow any timestamps
single Allow a single timestamp (send, receive or midpoint)
dual Allow dual timestamps
--no-dscp
Don’t allow setting dscp (default false)
--set-src-ip
Set source IP address on all outgoing packets from listeners on unspecified IP addresses (use for more reliable reply routing, but increases per-packet heap allocations)
--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, the 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 some 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:

  • Installing irtt in an unprivileged container
  • Setting up an iptables firewall (only UDP port 2112 must be open)
  • Setting up a chroot jail

It should be noted that there are no known security vulnerabilities in the Go language at this time, and the steps above, in particular the chroot jail, may or may not serve to enhance security in any way. Go-based servers are generally regarded as safe because of Go’s high-level language constructs for memory management, and at this time IRTT makes no use of Go’s unsafe package.

EXIT STATUS

irtt server exits with one of the following status codes:

Code Meaning
0 Success
1 Runtime error
2 Command line error
3 Two interrupt signals received

EXAMPLES

$ irtt server
Starts the server and listens on all addresses (unspecified address)
$ 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

heistp-irtt-d858e7f/doc/irtt-server.md000066400000000000000000000147201405065231400200030ustar00rootroot00000000000000% 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 \--syslog=*uri* : Log events to syslog (default don't use syslog). URI format: protocol://host:port/tag. Examples: URI | Result ------------------------ | ------ local: | Log to local syslog, default tag irtt local:/irttsrv | Log to local syslog, tag irttsrv udp://logsrv:514/irttsrv | UDP to logsrv:514, tag irttsrv tcp://logsrv:8514/ | TCP to logsrv:8514, default tag irtt **Note:** not available on Windows, Plan 9 or Google Native Client \--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) \--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, the 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 some 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: - Installing irtt in an unprivileged container - 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/heistp/irtt/) heistp-irtt-d858e7f/doc/irtt.1000066400000000000000000000227751405065231400162500ustar00rootroot00000000000000.\" Automatically generated by Pandoc 2.13 .\" .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[R] [\f[I]args\f[R]] .PP irtt help \f[I]command\f[R] .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 \f[I]client\f[R] runs the client .TP \f[I]server\f[R] runs the server .TP \f[I]bench\f[R] runs HMAC and fill benchmarks .TP \f[I]clock\f[R] runs wall vs monotonic clock test .TP \f[I]sleep\f[R] runs sleep accuracy test .TP \f[I]version\f[R] shows the version .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[R] .fi .PP While that\[cq]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\[mc]s 62.28ms 2.86ms send call time 56.5\[mc]s 82.8\[mc]s 18.99ms 348\[mc]s timer error 0s 21.7\[mc]s 19.05ms 356\[mc]s server proc. time 23.9\[mc]s 26.9\[mc]s 141\[mc]s 11.2\[mc]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[R] .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[R] may not be used on the server. .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[R] .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\[cq]s easy to implement .IP \[bu] 2 It\[cq]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\[cq]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\[cq]s easier to perform statistical analysis (like calculation of the median) on fixed arrays than on running data values .IP \[bu] 2 We don\[cq]t need to either send client timestamps to the server, or maintain a local running window of sent packet info, because they\[cq]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\[ha]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[R] 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[R]. .IP \[bu] 2 While any packet marked \f[C]LostDown\f[R] 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[R], 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\[cq]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\[cq]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[R] number of packets received by the server but not returned to the client (which can be obtained using \f[C]--stats=count\f[R]) will still be correct, which will still provide an accurate \f[B]average\f[R] 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\[cq]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\[cq]s easy to support a broad array of hardware and OS combinations .SH SEE ALSO .PP irtt-client(1), irtt-server(1) .PP IRTT GitHub repository (https://github.com/heistp/irtt/) .SH AUTHOR .PP Pete Heist .PP Many thanks to both Toke H\[/o]iland-J\[/o]rgensen and Dave T\[:a]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. heistp-irtt-d858e7f/doc/irtt.html000066400000000000000000000345111405065231400170430ustar00rootroot00000000000000 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

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

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)

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

IRTT GitHub repository

AUTHOR

Pete Heist

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.

heistp-irtt-d858e7f/doc/irtt.md000066400000000000000000000216701405065231400165010ustar00rootroot00000000000000% 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. # 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/heistp/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. heistp-irtt-d858e7f/error.go000066400000000000000000000034001405065231400160770ustar00rootroot00000000000000package 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 UnexpectedReplyFlag NoSuitableAddressFound InvalidConnToken ShortInterval LargeRequest AddressMismatch SyslogNotSupported InvalidSyslogURI ) // 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 NoSuchTimeSource NoSuchWaiter IntervalNonPositive DurationNonPositive ConnTokenZero ServerClosed OpenTimeout InvalidServerRestriction InvalidReceivedStatsInt InvalidReceivedStatsString OpenTimeoutTooShort ServerFillTooLong UnexpectedInitChannelClose ) // 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 } heistp-irtt-d858e7f/event.go000066400000000000000000000035341405065231400160770ustar00rootroot00000000000000package irtt import ( "fmt" "net" ) // Code uniquely identifies events and errors to improve context. type Code int // IsError returns true if the code for an error (negative). func (c Code) IsError() bool { return c < 0 } //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} } // IsError returns true if the event is an error (its code is negative). func (e *Event) IsError() bool { return e.Code.IsError() } 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) } // MultiHandler calls multiple event handlers. type MultiHandler struct { Handlers []Handler } // AddHandler adds a handler. func (m *MultiHandler) AddHandler(h Handler) { m.Handlers = append(m.Handlers, h) } // OnEvent calls all event handlers. func (m *MultiHandler) OnEvent(e *Event) { for _, h := range m.Handlers { h.OnEvent(e) } } heistp-irtt-d858e7f/fbuf.go000066400000000000000000000120221405065231400156700ustar00rootroot00000000000000package 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 } heistp-irtt-d858e7f/filler.go000066400000000000000000000055441405065231400162360ustar00rootroot00000000000000package 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), ) } heistp-irtt-d858e7f/glob.go000066400000000000000000000015331405065231400156760ustar00rootroot00000000000000package 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]) } heistp-irtt-d858e7f/go.mod000066400000000000000000000003721405065231400155320ustar00rootroot00000000000000module github.com/heistp/irtt go 1.12 require ( github.com/ogier/pflag v0.0.2-0.20160129220114-45c278ab3607 github.com/pkg/profile v1.3.0 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 ) heistp-irtt-d858e7f/go.sum000066400000000000000000000024371405065231400155630ustar00rootroot00000000000000github.com/ogier/pflag v0.0.2-0.20160129220114-45c278ab3607 h1:xZoOomu8/sOa+6Q469LrXeyq2YsmkhZo8wU6EzNWMDg= github.com/ogier/pflag v0.0.2-0.20160129220114-45c278ab3607/go.mod h1:zkFki7tvTa0tafRvTBIZTvzYyAu6kQhPZFnshFFPE+g= github.com/pkg/profile v1.3.0 h1:OQIvuDgm00gWVWGTf4m4mCt6W1/0YqU7Ntg0mySWgaI= github.com/pkg/profile v1.3.0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= heistp-irtt-d858e7f/irtt.go000066400000000000000000000045301405065231400157350ustar00rootroot00000000000000package 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("timer", "runs timer resolution test", runTimer, 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) } heistp-irtt-d858e7f/irtt.initd000077500000000000000000000040141405065231400164370ustar00rootroot00000000000000#!/bin/sh ### BEGIN INIT INFO # Provides: irtt # Required-Start: $remote_fs $syslog # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Start daemon at boot time # Description: Enable service provided by daemon. ### END INIT INFO dir="/tmp" cmd="/usr/local/bin/irtt server" user="nobody" #open_firewall_port="2112" iptables="/sbin/iptables" name=`basename $0` pid_file="/var/run/$name.pid" stdout_log="/var/log/$name.log" stderr_log="/var/log/$name.err" get_pid() { cat "$pid_file" } is_running() { [ -f "$pid_file" ] && ps -p `get_pid` > /dev/null 2>&1 } pre_start() { if [ -n "${open_firewall_port}" ]; then $iptables -A INPUT -p udp -m udp --dport $open_firewall_port -j ACCEPT fi } post_stop() { if [ -n "${open_firewall_port}" ]; then $iptables -D INPUT -p udp -m udp --dport $open_firewall_port -j ACCEPT fi } case "$1" in start) if is_running; then echo "Already started" else echo "Starting $name" pre_start cd "$dir" if [ -z "$user" ]; then sudo $cmd >> "$stdout_log" 2>> "$stderr_log" & else sudo -u "$user" $cmd >> "$stdout_log" 2>> "$stderr_log" & fi echo $! > "$pid_file" if ! is_running; then echo "Unable to start, see $stdout_log and $stderr_log" exit 1 fi fi ;; stop) if is_running; then echo -n "Stopping $name..." kill `get_pid` for i in 1 2 3 4 5 6 7 8 9 10 # for i in `seq 10` do if ! is_running; then break fi echo -n "." sleep 1 done echo if is_running; then echo "Not stopped; may still be shutting down or shutdown may have failed" exit 1 else echo "Stopped" if [ -f "$pid_file" ]; then rm "$pid_file" fi post_stop fi else echo "Not running" fi ;; restart) $0 stop if is_running; then echo "Unable to stop, will not attempt to start" exit 1 fi $0 start ;; status) if is_running; then echo "Running" else echo "Stopped" exit 1 fi ;; *) echo "Usage: $0 {start|stop|restart|status}" exit 1 ;; esac exit 0 heistp-irtt-d858e7f/irtt.openrc000066400000000000000000000011201405065231400166060ustar00rootroot00000000000000#!/sbin/openrc-run command="/usr/bin/irtt" command_args="server --syslog=local: $IRTT_OPTS" pidfile="/run/${RC_SVCNAME}.pid" command_background=true command_user="nobody" # This may be used on OpenRC 0.37 and later to take advantage of the # flexibility that the logger command provides, in which case # --syslog=local should be removed from command_args. # output_logger="logger -t irtt" # Note: If you have specified a bind address or interface with the -b flag, # you may need to specify rc_need so irtt is started after the interface # is available, for example: # rc_need="net.eth0" heistp-irtt-d858e7f/irtt.service000066400000000000000000000011521405065231400167650ustar00rootroot00000000000000[Unit] Description=irtt server After=network.target Documentation=man:irtt(1) Documentation=man:irtt-server(1) [Service] #ExecStartPre=/sbin/iptables -A INPUT -p udp -m udp --dport 2112 -j ACCEPT ExecStart=/usr/bin/irtt server #ExecStopPost=/sbin/iptables -D INPUT -p udp -m udp --dport 2112 -j ACCEPT 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 heistp-irtt-d858e7f/irtt@.service000066400000000000000000000011101405065231400170570ustar00rootroot00000000000000[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 heistp-irtt-d858e7f/irtt_bench.go000066400000000000000000000027311405065231400170750ustar00rootroot00000000000000package 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() } heistp-irtt-d858e7f/irtt_client.go000066400000000000000000000404041405065231400172730ustar00rootroot00000000000000package 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(" 0x10 (CS1- Bulk)") printf(" 0xa0 (CS5- Video)") printf(" 0xb8 (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, os.Interrupt, syscall.SIGTERM) 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(e *Event) { if !c.reallyQuiet { printf("%s", e) } } heistp-irtt-d858e7f/irtt_clock.go000066400000000000000000000012511405065231400171050ustar00rootroot00000000000000package 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++ } } heistp-irtt-d858e7f/irtt_print.go000066400000000000000000000015061405065231400171510ustar00rootroot00000000000000package 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) } } type consoleHandler struct { } func (h *consoleHandler) OnEvent(e *Event) { println(e.String()) } heistp-irtt-d858e7f/irtt_server.go000066400000000000000000000164021405065231400173240ustar00rootroot00000000000000package 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") if syslogSupport { printf("--syslog=uri log events to syslog (default don't use syslog)") printf(" URI format: scheme://host:port/tag, examples:") printf(" local: log to local syslog, default tag irtt") printf(" local:/irttsrv: log to local syslog, tag irttsrv") printf(" udp://logsrv:514/irttsrv: UDP to logsrv:514, tag irttsrv") printf(" tcp://logsrv:8514/: TCP to logsrv:8514, default tag irtt") } 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("-4 IPv4 only") printf("-6 IPv6 only") 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("--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 syslogStr *string if syslogSupport { syslogStr = fs.String("syslog", "", "syslog uri") } 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 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) } // create event handler with console handler as default handler := &MultiHandler{[]Handler{&consoleHandler{}}} // add syslog event handler if syslogStr != nil && *syslogStr != "" { sh, err := newSyslogHandler(*syslogStr) exitOnError(err, exitCodeRuntimeError) handler.AddHandler(sh) } // 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 = handler cfg.IPVersion = ipVer cfg.SetSrcIP = *setSrcIP cfg.ThreadLock = *lockOSThread // create server s := NewServer(cfg) // install signal handler to stop server sigs := make(chan os.Signal, 1) signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) go func() { sig := <-sigs printf("%s", sig) s.Shutdown() sig = <-sigs os.Exit(exitCodeDoubleSignal) }() err = s.ListenAndServe() exitOnError(err, exitCodeRuntimeError) } heistp-irtt-d858e7f/irtt_sleep.go000066400000000000000000000017411405065231400171260ustar00rootroot00000000000000package 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) } } heistp-irtt-d858e7f/irtt_timer.go000066400000000000000000000035601405065231400171370ustar00rootroot00000000000000package irtt import ( "math" "runtime" "runtime/debug" "time" ) const minHistBins = 25 const maxHistBins = 10 func runTimer(args []string) { debug.SetGCPercent(-1) runtime.GC() printf("Testing timer characteristics...") min := time.Duration(math.MaxInt64) max := time.Duration(math.MinInt64) start := time.Now() last := time.Now() for { d := time.Since(last) if d > 0 && d < min { min = d } if d > max { max = d } if time.Since(start) > 1*time.Second { break } last = time.Now() } zeroes := 0 hmin := make([]int, minHistBins) hmax := make([]int, maxHistBins) outliers := 0 start = time.Now() last = time.Now() n := 0 for { d := time.Since(last) if d == 0 { zeroes++ } if d > 0 && d < min*minHistBins { hmin[d/min]++ } if d > 0 && d < max { hmax[d/(max/maxHistBins)]++ } if d >= max { outliers++ } if time.Since(start) > 1*time.Second { break } n++ last = time.Now() } printf("") printf("Histogram relative to minimum nonzero duration (low high count):") setTabWriter(0) for i := int64(0); i < minHistBins; i++ { l := i * min.Nanoseconds() if l == 0 { l = 1 } h := (i+1)*min.Nanoseconds() - 1 printf("%s\t%s\t%d", time.Duration(l), time.Duration(h), hmin[i]) } printf("") printf("Histogram relative to maximum duration (low high count):") setTabWriter(0) for i := int64(0); i < maxHistBins; i++ { l := int64(float64(i) * (float64(max.Nanoseconds()) / float64(maxHistBins))) if l == 0 { l = 1 } h := int64((float64(i)+1)*(float64(max.Nanoseconds())/float64(maxHistBins))) - int64(1) printf("%s\t%s\t%d", time.Duration(l), time.Duration(h), hmax[i]) } printf("") setTabWriter(0) printf("Statistics:") printf("Minimum nonzero duration\t%s", min) printf("Maximum duration\t%s", max) printf("Zeroes\t%d / %d", zeroes, n) printf("Outliers\t%d / %d", outliers, n) flush() } heistp-irtt-d858e7f/irtt_version.go000066400000000000000000000004451405065231400175030ustar00rootroot00000000000000package 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) } heistp-irtt-d858e7f/mtu.go000066400000000000000000000040661405065231400155640ustar00rootroot00000000000000package 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 } heistp-irtt-d858e7f/net.go000066400000000000000000000053351405065231400155450ustar00rootroot00000000000000package 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 } heistp-irtt-d858e7f/packet.go000066400000000000000000000272661405065231400162350ustar00rootroot00000000000000package 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 trcvd 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(at StampAt, ts Timestamp) { if at == AtMidpoint { p.tset(&ts.Receive, fMWall, fMMono) return } if at&AtReceive != 0 { p.tset(&ts.Receive, fRWall, fRMono) } if at&AtSend != 0 { 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 } heistp-irtt-d858e7f/packet_test.go000066400000000000000000000067771405065231400173000ustar00rootroot00000000000000package 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(AtBoth, 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() } heistp-irtt-d858e7f/params.go000066400000000000000000000106571405065231400162450ustar00rootroot00000000000000package 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 } heistp-irtt-d858e7f/prof_off.go000066400000000000000000000002051405065231400165460ustar00rootroot00000000000000// +build !profile package irtt const profileEnabled = false func startProfile(path string) interface { Stop() } { return nil } heistp-irtt-d858e7f/prof_on.go000066400000000000000000000004261405065231400164150ustar00rootroot00000000000000// +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) } heistp-irtt-d858e7f/rand.go000066400000000000000000000001411405065231400156710ustar00rootroot00000000000000package irtt import ( "math/rand" "time" ) func init() { rand.Seed(time.Now().UnixNano()) } heistp-irtt-d858e7f/recorder.go000066400000000000000000000335621405065231400165670ustar00rootroot00000000000000package 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 `json:"start_time"` FirstSend Time `json:"-"` LastSent Time `json:"-"` FirstReceived Time `json:"-"` LastReceived 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 timeSource TimeSource 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, ts TimeSource, 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, timeSource: ts, } return } func (r *Recorder) recordPreSend() 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 := r.timeSource.Now(BothClocks) r.RoundTripData[len(r.RoundTripData)-1].Client.Send = tsend if r.FirstSend.IsZero() { r.FirstSend = 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, tsent 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 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 = 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 and the measured // server processing time does not exceed the round-trip time. func (ts *RoundTripData) RTT() (rtt time.Duration) { if !ts.ReplyReceived() { return InvalidDuration } rtt = ts.Client.Receive.Mono - ts.Client.Send.Mono if spt := ts.ServerProcessingTime(); 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.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) } heistp-irtt-d858e7f/result.go000066400000000000000000000201241405065231400162660ustar00rootroot00000000000000package 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 (monotonic time since start) r.Duration = cfg.TimeSource.Now(Monotonic).Sub(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]) } heistp-irtt-d858e7f/rstats.go000066400000000000000000000035151405065231400162750ustar00rootroot00000000000000package 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()) } heistp-irtt-d858e7f/sconfig.go000066400000000000000000000021341405065231400164010ustar00rootroot00000000000000package 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 TimeSource TimeSource 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, TimeSource: DefaultTimeSource, ThreadLock: DefaultThreadLock, } } heistp-irtt-d858e7f/sconn.go000066400000000000000000000153041405065231400160740ustar00rootroot00000000000000package 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 := p.trcvd.Midpoint(sc.TimeSource.Now(cl)) rt = mt st = mt } else { if at&AtReceive != 0 { rt = p.trcvd.KeepClocks(cl) } if at&AtSend != 0 { st = sc.TimeSource.Now(cl) } } p.setTimestamp(at, 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 } heistp-irtt-d858e7f/server.go000066400000000000000000000176721405065231400162740ustar00rootroot00000000000000package irtt import ( "encoding/binary" "math/rand" "net" "runtime" "sync" "time" ) // Server is the irtt server. type Server struct { *ServerConfig start time.Time 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) } // 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, s.TimeSource) if err != nil { return nil, err } ls := make([]*listener, 0, len(lconns)) for _, lconn := range lconns { ls = append(ls, newListener(s.ServerConfig, lconn)) } return ls, nil } // 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) *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), } } 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 sconns map[ctoken]*sconn } func newConnMgr(cfg *ServerConfig) *connmgr { return &connmgr{ ServerConfig: cfg, sconns: make(map[ctoken]*sconn, sconnsInitSize), } } func (cm *connmgr) put(sc *sconn) { cm.removeSomeExpired() ct := cm.newCtoken() sc.ctoken = ct cm.sconns[ct] = sc } 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) } heistp-irtt-d858e7f/sys_bsd.go000066400000000000000000000004751405065231400164250ustar00rootroot00000000000000// +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) } heistp-irtt-d858e7f/sys_linux.go000066400000000000000000000005671405065231400170160ustar00rootroot00000000000000// +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) } heistp-irtt-d858e7f/sys_nodf.go000066400000000000000000000002661405065231400166010ustar00rootroot00000000000000// +build !linux,!openbsd,!freebsd package irtt import ( "net" ) func setSockoptDF(conn *net.UDPConn, df DF) error { return Errorf(DFNotSupported, "DF sockopt not supported") } heistp-irtt-d858e7f/sys_posix.go000066400000000000000000000015441405065231400170150ustar00rootroot00000000000000// +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) } heistp-irtt-d858e7f/sysinfo.go000066400000000000000000000007471405065231400164530ustar00rootroot00000000000000package 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 } heistp-irtt-d858e7f/syslog.go000066400000000000000000000020421405065231400162670ustar00rootroot00000000000000// +build !windows,!nacl,!plan9 package irtt import ( "log/syslog" "net/url" "strings" ) const syslogSupport = true const defaultSyslogTag = "irtt" type syslogHandler struct { syslogWriter *syslog.Writer } func (s *syslogHandler) OnEvent(e *Event) { if e.IsError() { s.syslogWriter.Err(e.String()) } else { s.syslogWriter.Info(e.String()) } } func newSyslogHandler(uriStr string) (sh Handler, err error) { var suri *url.URL if suri, err = parseSyslogURI(uriStr); err != nil { return } prio := syslog.LOG_DAEMON | syslog.LOG_INFO var sw *syslog.Writer if suri.Scheme == "local" { sw, err = syslog.New(prio, suri.Path) } else { sw, err = syslog.Dial(suri.Scheme, suri.Host, prio, suri.Path) } sh = &syslogHandler{sw} return } func parseSyslogURI(suri string) (u *url.URL, err error) { if u, err = url.Parse(suri); err != nil { return } u.Path = strings.Trim(u.Path, "/") if u.Path == "" { u.Path = defaultSyslogTag } if u.Scheme == "" { err = Errorf(InvalidSyslogURI, "missing colon after scheme") } return } heistp-irtt-d858e7f/syslog_stub.go000066400000000000000000000003031405065231400173220ustar00rootroot00000000000000// +build windows nacl plan9 package irtt const syslogSupport = false func newSyslogHandler(uriStr string) (Handler, error) { return nil, Errorf(SyslogNotSupported, "syslog not supported") } heistp-irtt-d858e7f/time.go000066400000000000000000000207311405065231400157120ustar00rootroot00000000000000package irtt import ( "encoding/json" "fmt" "math" "strings" "time" ) // 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 is the wall clock value as the number of nanoseconds elapsed since // January 1, 1970 UTC. Wall int64 `json:"wall,omitempty"` // Monotonic is the monotonic clock value and may be relative to any start // point in time. Mono time.Duration `json:"monotonic,omitempty"` } // Sub returns the duration t-u. Monotonic times are used if both are non-zero. func (t Time) Sub(u Time) time.Duration { if t.Mono != 0 && u.Mono != 0 { return t.Mono - u.Mono } else if t.Wall != 0 && u.Wall != 0 { return time.Duration(t.Wall - u.Wall) } panic("Time.Sub() clock mismatch") } // Add returns the time t+d. func (t Time) Add(d time.Duration) Time { ok := false if t.Wall != 0 { t.Wall += int64(d) ok = true } if t.Mono != 0 { t.Mono += d ok = true } if !ok { panic("Time.Add() for zero time") } return t } // After returns whether the time instant t is after u. func (t Time) After(u Time) bool { if t.Mono != 0 && u.Mono != 0 { return t.Mono > u.Mono } else if t.Wall != 0 && u.Wall != 0 { return t.Wall > u.Wall } panic("Time.After() clock mismatch") } // Before returns whether the time instant t is before u. func (t Time) Before(u Time) bool { if t.Mono != 0 && u.Mono != 0 { return t.Mono < u.Mono } else if t.Wall != 0 && u.Wall != 0 { return t.Wall < u.Wall } panic("Time.Before() clock mismatch") } // Midpoint returns the point in time halfway between t and u. func (t Time) Midpoint(u Time) Time { return t.Add(u.Sub(t) / 2) } // KeepClocks keeps only the specified clocks. Passing in Wall sets Mono to 0, // Monotonic sets Wall to 0 and BothClocks does nothing. func (t Time) KeepClocks(c Clock) Time { switch c { case Wall: t.Mono = 0 case Monotonic: t.Wall = 0 case BothClocks: default: panic(fmt.Sprintf("unknown clock %s", c)) } return t } // IsWallZero returns true if Wall is zero. func (t Time) IsWallZero() bool { return t.Wall == 0 } // IsMonoZero returns true if Mono is zero. func (t Time) IsMonoZero() bool { return t.Mono == 0 } // IsZero returns true if both Wall and Mono are zero. func (t Time) IsZero() bool { return t.IsWallZero() && t.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"` } // 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) } // 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) } heistp-irtt-d858e7f/timer.go000066400000000000000000000167331405065231400161030ustar00rootroot00000000000000package 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, tsrc TimeSource, now Time, d time.Duration) (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, tsrc TimeSource, now Time, d time.Duration) (Time, error) { st.timer.Reset(d) select { case <-st.timer.C: return tsrc.Now(Monotonic), nil case <-ctx.Done(): // stop and drain timer for cleanliness if !st.timer.Stop() { <-st.timer.C } return tsrc.Now(Monotonic), 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, tsrc TimeSource, now Time, d time.Duration) (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, tsrc, now, d) erf := float64(t2.Sub(now)) / 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, tsrc TimeSource, now Time, d time.Duration) (Time, error) { e := now.Add(d) for now.Before(e) { select { case <-ctx.Done(): return now, ctx.Err() default: now = tsrc.Now(Monotonic) } } return now, 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, tsrc TimeSource, now Time, d time.Duration) (Time, error) { e := now.Add(d) d = time.Duration(float64(d) * ht.slfct) t2, err := ht.ctimer.Sleep(ctx, tsrc, now, d) if err != nil { return t2, err } for t2.Before(e) { select { case <-ctx.Done(): return t2, ctx.Err() default: t2 = tsrc.Now(Monotonic) } } 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)", ) } heistp-irtt-d858e7f/timesrc.go000066400000000000000000000036611405065231400164250ustar00rootroot00000000000000package irtt import ( "fmt" "time" ) // TimeSource provides wall and monotonic clock values. type TimeSource interface { // Now returns the current time, with wall or monotonic clock values set // according to the specified Clock. Now(c Clock) Time String() string } // GoTimeSource uses Go's default time functions. type GoTimeSource struct { monotonicStart time.Time } // Now returns a Time containing the current time. func (g *GoTimeSource) Now(clock Clock) Time { now := time.Now() switch clock { case Wall: return Time{now.UnixNano(), time.Duration(0)} case Monotonic: return Time{0, now.Sub(g.monotonicStart)} case BothClocks: return Time{now.UnixNano(), now.Sub(g.monotonicStart)} default: panic(fmt.Sprintf("unknown clock %s", clock)) } } func (g *GoTimeSource) String() string { return "go" } // NewGoTimeSource returns a new Go TimeSource. func NewGoTimeSource() *GoTimeSource { return &GoTimeSource{time.Now()} } // TimeSourceFactories are the registered TimeSource factories. var TimeSourceFactories = make([]TimeSourceFactory, 0) // TimeSourceFactory can create a TimeSource from a string. type TimeSourceFactory struct { FactoryFunc func(string) (TimeSource, error) Usage string } // RegisterTimeSource registers a new TimeSource. func RegisterTimeSource(fn func(string) (TimeSource, error), usage string) { TimeSourceFactories = append(TimeSourceFactories, TimeSourceFactory{fn, usage}) } // NewTimeSource returns a TimeSource from a string. func NewTimeSource(s string) (TimeSource, error) { for _, fac := range TimeSourceFactories { t, err := fac.FactoryFunc(s) if err != nil { return nil, err } if t != nil { return t, nil } } return nil, Errorf(NoSuchTimeSource, "no such TimeSource %s", s) } func init() { RegisterTimeSource( func(s string) (t TimeSource, err error) { if s == "go" { t = NewGoTimeSource() } return }, "go: Go's standard time.Time functions", ) } heistp-irtt-d858e7f/timesrc_notwin.go000066400000000000000000000003201405065231400200100ustar00rootroot00000000000000// +build !windows package irtt // NewDefaultTimeSource returns a WindowsTimeSource for Windows and GoTimeSource // for everything else. func NewDefaultTimeSource() TimeSource { return NewGoTimeSource() } heistp-irtt-d858e7f/timesrc_win.go000066400000000000000000000043751405065231400173050ustar00rootroot00000000000000// +build windows package irtt import ( "fmt" "os" "time" "unsafe" "golang.org/x/sys/windows" ) // WindowsTimeSource uses GetSystemTimePreciseAsFileTime for the wall clock and // QueryPerformanceFrequency/Counter for the monotonic clock. type WindowsTimeSource struct { period int64 qpc *windows.LazyProc } // Now returns a Time containing the current time. func (w *WindowsTimeSource) Now(clock Clock) Time { switch clock { case Wall: return Time{w.systemTimePreciseNs(), time.Duration(0)} case Monotonic: return Time{0, w.queryPerformanceCounterNs()} case BothClocks: return Time{w.systemTimePreciseNs(), w.queryPerformanceCounterNs()} default: panic(fmt.Sprintf("unknown clock %s", clock)) } } func (w *WindowsTimeSource) String() string { return "windows" } func (w *WindowsTimeSource) systemTimePreciseNs() int64 { var t windows.Filetime windows.GetSystemTimePreciseAsFileTime(&t) return t.Nanoseconds() } func (w *WindowsTimeSource) queryPerformanceCounterNs() time.Duration { var ctr int64 ret, _, err := w.qpc.Call(uintptr(unsafe.Pointer(&ctr))) if ret == 0 { panic(err) } return time.Duration(ctr * w.period) } // NewWindowsTimeSource returns a new WindowsTimeSource. func NewWindowsTimeSource() (ts *WindowsTimeSource, err error) { k := windows.NewLazySystemDLL("kernel32.dll") if err = k.Load(); err != nil { return } qpf := k.NewProc("QueryPerformanceFrequency") if err = qpf.Find(); err != nil { return } qpc := k.NewProc("QueryPerformanceCounter") if err = qpc.Find(); err != nil { return } var freq int64 var ret uintptr if ret, _, err = qpf.Call(uintptr(unsafe.Pointer(&freq))); ret == 0 { return } err = nil ts = &WindowsTimeSource{1000000000 / freq, qpc} return } // NewDefaultTimeSource returns a WindowsTimeSource for Windows and GoTimeSource // for everything else. func NewDefaultTimeSource() TimeSource { wts, err := NewWindowsTimeSource() if err != nil { fmt.Fprintf(os.Stderr, "falling back to Go time source: %s\n", err) return NewGoTimeSource() } return wts } func init() { RegisterTimeSource( func(s string) (t TimeSource, err error) { if s == "windows" { t, err = NewWindowsTimeSource() } return }, "windows: GetSystemTimePreciseAsFileTime/QueryPerformanceCounter", ) } heistp-irtt-d858e7f/user_dev.go000066400000000000000000000001531405065231400165640ustar00rootroot00000000000000// +build !prod package irtt import "time" func validateInterval(i time.Duration) error { return nil } heistp-irtt-d858e7f/user_prod.go000066400000000000000000000007761405065231400167650ustar00rootroot00000000000000// +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 } heistp-irtt-d858e7f/version.go000066400000000000000000000012501405065231400164340ustar00rootroot00000000000000package irtt // Version is the IRTT version number (replaced during build). var Version = "0.9.1" // 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, } } heistp-irtt-d858e7f/waiter.go000066400000000000000000000070431405065231400162500ustar00rootroot00000000000000package 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 }