├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── Makefile ├── README.md ├── arachned ├── config │ ├── arachne.yaml │ ├── test_target_config_darwin.json │ └── test_target_config_linux.json └── main.go ├── bootstrap.go ├── collector ├── collector.go └── collector_test.go ├── config ├── config.go └── config_test.go ├── debian ├── README.Debian ├── README.source ├── arachne.install ├── arachne.postinst ├── changelog ├── compat ├── control ├── copyright ├── docs ├── gbp.conf ├── rules └── source │ └── format ├── defines └── defines.go ├── doc.go ├── example_run_arachne_test.go ├── glide.lock ├── glide.yaml ├── internal ├── ip │ ├── ip.go │ ├── ip_bpf_darwin.go │ ├── ip_bpf_linux.go │ ├── ip_darwin.go │ └── ip_linux.go ├── log │ └── log.go ├── network │ ├── network.go │ └── network_test.go ├── tcp │ ├── tcp.go │ ├── tcp_darwin.go │ ├── tcp_linux.go │ └── tcp_test.go └── util │ └── util.go ├── metrics ├── metrics.go └── metrics_statsd.go └── options.go /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Before opening your pull request, please make sure that you have: 2 | 3 | - [ ] signed [Uber's Contributor License Agreement][]; 4 | - [ ] added tests to cover your changes; 5 | - [ ] run the test suite locally (`make test`); and finally, 6 | - [ ] run the linters locally (`make lint`). 7 | 8 | Thanks for your contribution! 9 | 10 | [Uber's Contributor License Agreement]: https://docs.google.com/a/uber.com/forms/d/1pAwS_-dA1KhPlfxzYLBqK6rsSWwRwH95OCCZrcsY5rk/viewform 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | build/ 12 | 13 | # Architecture specific extensions/prefixes 14 | *.[568vq] 15 | [568vq].out 16 | 17 | *.cgo1.go 18 | *.cgo2.c 19 | _cgo_defun.c 20 | _cgo_gotypes.go 21 | _cgo_export.* 22 | 23 | _testmain.go 24 | 25 | debian/arachne.debhelper.log 26 | debian/arachne.substvars 27 | debian/files 28 | 29 | *.exe 30 | *.test 31 | *.log 32 | *.prof 33 | *.pprof 34 | 35 | # Output of the go coverage tool, specifically when used with LiteIDE 36 | *.out 37 | 38 | # external packages folder 39 | vendor/ 40 | .idea/ 41 | *.iml 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: go 4 | 5 | go: 6 | - 1.12.x 7 | 8 | matrix: 9 | include: 10 | - os: linux 11 | dist: trusty 12 | - os: osx 13 | 14 | 15 | cache: 16 | directories: 17 | - vendor 18 | 19 | addons: 20 | apt: 21 | packages: 22 | - iproute2 23 | 24 | before_install: 25 | - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew update ; fi 26 | - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew install iproute2mac; fi 27 | 28 | install: make install_ci 29 | 30 | script: 31 | - if [ "$TRAVIS_OS_NAME" = "linux" ]; then ip a; fi 32 | - if [ "$TRAVIS_OS_NAME" = "osx" ]; then ip link; fi 33 | - make test 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Uber Technologies, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGES=$(shell glide novendor) 2 | 3 | builddir := build 4 | 5 | $(info builddir ${builddir}) 6 | 7 | ${builddir}: 8 | mkdir -p $(builddir) 9 | 10 | bins: install_ci 11 | go build -o ${builddir}/arachned github.com/uber/arachne/arachned/ 12 | 13 | all: bins 14 | 15 | clean: 16 | rm -f ${builddir}/* 17 | 18 | FILTER := grep -v -e '_string.go' -e '/gen-go/' -e '/mocks/' -e 'vendor/' 19 | lint: 20 | @echo "Running golint" 21 | -golint $(ALL_PKGS) | $(FILTER) | tee lint.log 22 | @echo "Running go vet" 23 | -go vet $(ALL_PKGS) 2>&1 | fgrep -v -e "possible formatting directiv" -e "exit status" | tee -a lint.log 24 | @echo "Verifying files are gofmt'd" 25 | -gofmt -l . | $(FILTER) | tee -a lint.log 26 | @echo "Checking for unresolved FIXMEs" 27 | -git grep -i -n fixme | $(FILTER) | grep -v -e Makefile | tee -a lint.log 28 | @[ ! -s lint.log ] 29 | 30 | test: lint install_ci 31 | find . -type f -name '*.go' | xargs golint 32 | go test $(PACKAGES) 33 | 34 | vendor: glide.lock 35 | glide install 36 | 37 | install_ci: 38 | glide --version || go get -u -f github.com/Masterminds/glide 39 | make vendor 40 | go get -u golang.org/x/lint/golint 41 | 42 | .PHONY: bins test vendor install_ci lint 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arachne [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Go Report Card][gorep-img]][gorep] 2 | 3 | Arachne is a packet loss detection system and an underperforming path detection 4 | system. It provides fast and easy active end-to-end functional testing 5 | of all the components in Data Center and Cloud infrastructures. 6 | Arachne is able to detect intra-DC, inter-DC, DC-to-Cloud, and 7 | DC-to-External-Services issues by generating minimal traffic: 8 | 9 | - Reachability 10 | - Round-trip and 1-way latency 11 | - Silent packet drops and black holes 12 | - Jitter (average of the deviation from the network mean latency) 13 | - PMTU or Firewall issues too related possibly to network config changes 14 | (accidental or not) 15 | - Whether network-level SLAs are met 16 | 17 | ## Usage 18 | 19 | There are two ways to use the Arachne package. 20 | 21 | ### As a standalone program 22 | Run Arachne as a standalone program (it's Debian packaged already too). 23 | 24 | ### As a library in your own program 25 | Import this package and call Arachne from your program/service with 26 | ```go 27 | arachne.Run(config, arachne.ReceiverOnlyMode(false)) 28 | ``` 29 | where the option provided above is among the few optional ones. 30 | 31 | 32 | Below is the list of all the CLI options available, when Arachne is 33 | used as a standalone program. The default options should be good 34 | enough for most users. 35 | 36 | ``` 37 | $ arachne --help 38 | 39 | ____________________________________________________________/\\\______________________________________ 40 | ___________________________________________________________\/\\\______________________________________ 41 | ___________________________________________________________\/\\\______________________________________ 42 | __/\\\\\\\\\_____/\\/\\\\\\\___/\\\\\\\\\________/\\\\\\\\_\/\\\__________/\\/\\\\\\_______/\\\\\\\\__ 43 | _\////////\\\___\/\\\/////\\\_\////////\\\_____/\\\//////__\/\\\\\\\\\\__\/\\\////\\\____/\\\/////\\\_ 44 | ___/\\\\\\\\\\__\/\\\___\///____/\\\\\\\\\\___/\\\_________\/\\\/////\\\_\/\\\__\//\\\__/\\\\\\\\\\\__ 45 | __/\\\/////\\\__\/\\\__________/\\\/////\\\__\//\\\________\/\\\___\/\\\_\/\\\___\/\\\_\//\\///////___ 46 | _\//\\\\\\\\/\\_\/\\\_________\//\\\\\\\\/\\__\///\\\\\\\\_\/\\\___\/\\\_\/\\\___\/\\\__\//\\\\\\\\\\_ 47 | __\////////\//__\///___________\////////\//_____\////////__\///____\///__\///____\///____\//////////__ 48 | 49 | 50 | Usage: arachne [--foreground] [-c=] [--receiver_only] [--sender_only] 51 | 52 | Arachne is a packet loss detection system and an underperforming path detection 53 | system for Data Center and Cloud infrastructures. 54 | 55 | Options: 56 | -v, --version Show the version and exit 57 | --foreground=false Force foreground mode 58 | -c, --config_file="/usr/local/etc/arachne/arachne.yaml" Config file path 59 | (default: /usr/local/etc/arachne/arachne.yaml) 60 | --receiver_only=false Force TCP receiver-only mode 61 | --sender_only=false Force TCP sender-only mode 62 | ``` 63 | 64 | 65 | ### Note on required privileges to run 66 | 67 | Arachne is granted access to raw sockets without the need to run with sudo or 68 | as root user, by being granted `CAP_NET_RAW` capability 69 | (see: [capabilities][]). 70 | 71 | 72 | ### Note on BPF filtering 73 | 74 | When receiving packets, Arachne attempts to apply a BPF filter to the raw socket 75 | so that processing of packets occurs on a much smaller set (ones destined 76 | specifically for Arachne agent testing). This is currently supported only on 77 | Linux and thus performance will be worse on BSD-based systems where a larger 78 | number of packets must be inspected. 79 | 80 |
81 | 82 | Released under the [MIT License](LICENSE.md). 83 | 84 | 85 | [doc-img]: https://godoc.org/github.com/uber/arachne?status.svg 86 | [doc]: https://godoc.org/github.com/uber/arachne 87 | [ci-img]: https://travis-ci.org/uber/arachne.svg?branch=master 88 | [ci]: https://travis-ci.org/uber/arachne 89 | [capabilities]: http://linux.die.net/man/7/capabilities 90 | [gorep-img]: https://goreportcard.com/badge/github.com/uber/arachne 91 | [gorep]: https://goreportcard.com/report/github.com/uber/arachne 92 | -------------------------------------------------------------------------------- /arachned/config/arachne.yaml: -------------------------------------------------------------------------------- 1 | arachne: 2 | pidPath: /var/run/arachne/arachne.pid 3 | orchestrator: 4 | enabled: false 5 | addrport: 127.0.0.1:12345 6 | restVersion: api/v1 7 | standaloneTargetConfig: /usr/local/etc/arachne/target_config.json 8 | 9 | logging: 10 | stdout: true 11 | level: info 12 | logSink: /var/log/arachne/arachne.log 13 | 14 | metrics: 15 | statsd: 16 | hostPort: 127.0.0.1:6789 17 | prefix: arachne 18 | -------------------------------------------------------------------------------- /arachned/config/test_target_config_darwin.json: -------------------------------------------------------------------------------- 1 | { 2 | "local": { 3 | "location": "us-west", 4 | "src_address": "", 5 | "interface_name": "en0", 6 | "target_tcp_port": 44111, 7 | "timeout": "500ms", 8 | "base_src_tcp_port": 31000, 9 | "num_src_tcp_ports": 16, 10 | "batch_interval": "10s", 11 | "qos": "disabled", 12 | "resolve_dns": "enabled", 13 | "dns_servers_alternate": "8.8.8.8, 8.8.4.4", 14 | "poll_orchestrator_interval_success": "2h55m", 15 | "poll_orchestrator_interval_failure": "60s" 16 | }, 17 | 18 | "internal": [ 19 | {"ip": "10.10.39.31", "location": "us-west"}, 20 | {"ip": "10.10.15.27"}, 21 | {"host_name": "hadoop375.myservers.com", "location": "apac-north"} 22 | ], 23 | 24 | "external": [ 25 | {"host_name": "payments.service.org"}, 26 | {"host_name": "maps.service.com"} 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /arachned/config/test_target_config_linux.json: -------------------------------------------------------------------------------- 1 | { 2 | "local": { 3 | "location": "us-west", 4 | "src_address": "", 5 | "interface_name": "eth0", 6 | "target_tcp_port": 44111, 7 | "timeout": "500ms", 8 | "base_src_tcp_port": 31000, 9 | "num_src_tcp_ports": 16, 10 | "batch_interval": "10s", 11 | "qos": "disabled", 12 | "resolve_dns": "enabled", 13 | "dns_servers_alternate": "8.8.8.8, 8.8.4.4", 14 | "poll_orchestrator_interval_success": "2h55m", 15 | "poll_orchestrator_interval_failure": "60s" 16 | }, 17 | 18 | "internal": [ 19 | {"ip": "10.10.39.31", "location": "us-west"}, 20 | {"ip": "10.10.15.27"}, 21 | {"host_name": "hadoop375.myservers.com", "location": "apac-north"} 22 | ], 23 | 24 | "external": [ 25 | {"host_name": "payments.service.org"}, 26 | {"host_name": "maps.service.com"} 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /arachned/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import ( 24 | "github.com/uber/arachne" 25 | "github.com/uber/arachne/config" 26 | "github.com/uber/arachne/metrics" 27 | ) 28 | 29 | func main() { 30 | 31 | mc := new(metrics.StatsdConfiger) 32 | 33 | ec := &config.Extended{ 34 | Metrics: metrics.Opt(*mc), 35 | } 36 | 37 | arachne.Run( 38 | ec, 39 | arachne.ReceiverOnlyMode(false), 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /bootstrap.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package arachne 22 | 23 | import ( 24 | coreLog "log" 25 | "os" 26 | "sync" 27 | "time" 28 | 29 | "github.com/uber/arachne/collector" 30 | "github.com/uber/arachne/config" 31 | d "github.com/uber/arachne/defines" 32 | "github.com/uber/arachne/internal/ip" 33 | "github.com/uber/arachne/internal/log" 34 | "github.com/uber/arachne/internal/tcp" 35 | "github.com/uber/arachne/internal/util" 36 | 37 | "go.uber.org/zap" 38 | ) 39 | 40 | // Run is the entry point for initiating any Arachne service. 41 | func Run(ec *config.Extended, opts ...Option) { 42 | var ( 43 | gl config.Global 44 | err error 45 | ) 46 | 47 | bl, err := zap.NewProduction() 48 | if err != nil { 49 | coreLog.Fatal(err) 50 | } 51 | bootstrapLogger := &log.Logger{ 52 | Logger: bl, 53 | PIDPath: "", 54 | RemovePID: util.RemovePID, 55 | } 56 | 57 | util.PrintBanner() 58 | 59 | gl.CLI = config.ParseCliArgs(bootstrapLogger, d.ArachneService, d.ArachneVersion) 60 | apply(&gl, opts...) 61 | gl.App, err = config.Get(gl.CLI, ec, bootstrapLogger) 62 | if err != nil { 63 | bootstrapLogger.Error("error reading the configuration file", 64 | zap.String("file", *gl.CLI.ConfigFile), 65 | zap.Error(err)) 66 | os.Exit(1) 67 | } 68 | 69 | logger, err := log.CreateLogger(gl.App.Logging, gl.App.PIDPath, util.RemovePID) 70 | if err != nil { 71 | bootstrapLogger.Fatal("unable to initialize Arachne Logger", zap.Error(err)) 72 | os.Exit(1) 73 | } 74 | 75 | // Channel to be informed if Unix signal has been received. 76 | sigC := make(chan struct{}, 1) 77 | util.UnixSignals(sigC, logger) 78 | 79 | // Check if another Arachne process is already running. 80 | // Pass bootstrapLogger so that the arachne PID file is not removed. 81 | if err := util.CheckPID(gl.App.PIDPath, bootstrapLogger); err != nil { 82 | os.Exit(1) 83 | } 84 | 85 | sr, err := gl.App.Metrics.NewReporter(logger.Logger) 86 | if err != nil { 87 | logger.Error("error initializing stats", zap.Error(err)) 88 | } 89 | 90 | // Hold raw socket connection for IPv4 packets 91 | var connIPv4 *ip.Conn 92 | 93 | logger.Info("Starting up arachne") 94 | 95 | for { 96 | var ( 97 | err error 98 | currentDSCP ip.DSCPValue 99 | dnsWg sync.WaitGroup 100 | finishedCycleUpload sync.WaitGroup 101 | ) 102 | 103 | // Channels to tell goroutines to terminate 104 | killC := new(util.KillChannels) 105 | 106 | // If Orchestrator mode enabled, fetch JSON configuration file, otherwise try 107 | // to retrieve default local file 108 | err = config.FetchRemoteList(&gl, d.MaxNumRemoteTargets, d.MaxNumSrcTCPPorts, 109 | d.MinBatchInterval, d.HTTPResponseHeaderTimeout, d.OrchestratorRESTConf, sigC, logger) 110 | if err != nil { 111 | break 112 | } 113 | logger.Debug("Global JSON configuration", zap.Any("configuration", gl.RemoteConfig)) 114 | 115 | if len(gl.Remotes) == 0 { 116 | logger.Debug("No targets to be echoed have been specified") 117 | apply(&gl, ReceiverOnlyMode(true)) 118 | } 119 | 120 | configRefresh := time.NewTicker(gl.RemoteConfig.PollOrchestratorInterval.Success) 121 | 122 | if gl.RemoteConfig.ResolveDNS && !*gl.CLI.ReceiverOnlyMode { 123 | // Refresh DNS resolutions 124 | dnsRefresh := time.NewTicker(d.DNSRefreshInterval) 125 | dnsWg.Add(1) 126 | killC.DNSRefresh = make(chan struct{}) 127 | config.ResolveDNSTargets(gl.Remotes, gl.RemoteConfig, dnsRefresh, &dnsWg, 128 | killC.DNSRefresh, logger) 129 | dnsWg.Wait() 130 | logger.Debug("Remotes after DNS resolution include", 131 | zap.Int("count", len(gl.Remotes)), 132 | zap.Any("remotes", gl.Remotes)) 133 | } 134 | 135 | // Channels for Collector to receive Probes and Responses from. 136 | sentC := make(chan tcp.Message, d.ChannelOutBufferSize) 137 | rcvdC := make(chan tcp.Message, d.ChannelInBufferSize) 138 | 139 | // Connection for IPv4 packets 140 | if connIPv4 == nil { 141 | connIPv4 = ip.NewConn( 142 | d.AfInet, 143 | gl.RemoteConfig.TargetTCPPort, 144 | gl.RemoteConfig.InterfaceName, 145 | gl.RemoteConfig.SrcAddress, 146 | logger) 147 | } 148 | 149 | // Actual echoing is a percentage of the total configured batch cycle duration. 150 | realBatchInterval := time.Duration(float32(gl.RemoteConfig.BatchInterval) * 151 | d.BatchIntervalEchoingPerc) 152 | uploadBatchInterval := time.Duration(float32(gl.RemoteConfig.BatchInterval) * 153 | d.BatchIntervalUploadStats) 154 | batchEndCycle := time.NewTicker(uploadBatchInterval) 155 | completeCycleUpload := make(chan bool, 1) 156 | 157 | if !*gl.CLI.SenderOnlyMode && !*gl.CLI.ReceiverOnlyMode { 158 | // Start gathering and reporting results. 159 | killC.Collector = make(chan struct{}) 160 | collector.Run(&gl, sentC, rcvdC, gl.Remotes, ¤tDSCP, sr, completeCycleUpload, 161 | &finishedCycleUpload, killC.Collector, logger) 162 | } 163 | 164 | if !*gl.CLI.SenderOnlyMode { 165 | // Listen for responses or probes from other IPv4 arachne agents. 166 | killC.Receiver = make(chan struct{}) 167 | err = tcp.Receiver(connIPv4, sentC, rcvdC, killC.Receiver, logger) 168 | if err != nil { 169 | logger.Fatal("IPv4 receiver failed to start", zap.Error(err)) 170 | } 171 | logger.Debug("IPv4 receiver now ready...") 172 | //TODO IPv6 receiver 173 | } 174 | 175 | if !*gl.CLI.ReceiverOnlyMode { 176 | logger.Debug("Echoing...") 177 | // Start echoing all targets. 178 | killC.Echo = make(chan struct{}) 179 | tcp.EchoTargets(gl.Remotes, connIPv4, gl.RemoteConfig.TargetTCPPort, 180 | gl.RemoteConfig.SrcTCPPortRange, gl.RemoteConfig.QoSEnabled, ¤tDSCP, 181 | realBatchInterval, batchEndCycle, sentC, *gl.CLI.SenderOnlyMode, 182 | completeCycleUpload, &finishedCycleUpload, killC.Echo, logger) 183 | } 184 | 185 | select { 186 | case <-configRefresh.C: 187 | util.CleanUpRefresh(killC, *gl.CLI.ReceiverOnlyMode, 188 | *gl.CLI.SenderOnlyMode, gl.RemoteConfig.ResolveDNS) 189 | log.ResetLogFiles(gl.App.Logging.OutputPaths, d.LogFileSizeMaxMB, d.LogFileSizeKeepKB, logger) 190 | logger.Info("Refreshing target list file, if needed") 191 | configRefresh.Stop() 192 | case <-sigC: 193 | logger.Debug("Received SIG") 194 | configRefresh.Stop() 195 | util.CleanUpAll(killC, *gl.CLI.ReceiverOnlyMode, *gl.CLI.SenderOnlyMode, 196 | gl.RemoteConfig.ResolveDNS, connIPv4, gl.App.PIDPath, sr, logger) 197 | logger.Info("Exiting") 198 | os.Exit(0) 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /collector/collector.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package collector 22 | 23 | import ( 24 | "fmt" 25 | "strconv" 26 | "strings" 27 | "sync" 28 | "time" 29 | 30 | "github.com/uber/arachne/config" 31 | "github.com/uber/arachne/defines" 32 | "github.com/uber/arachne/internal/ip" 33 | "github.com/uber/arachne/internal/log" 34 | "github.com/uber/arachne/internal/tcp" 35 | "github.com/uber/arachne/metrics" 36 | 37 | "github.com/fatih/color" 38 | "github.com/google/gopacket/layers" 39 | "go.uber.org/zap" 40 | "go.uber.org/zap/zapcore" 41 | ) 42 | 43 | const hostWidth = 51 44 | const tableWidth = 119 45 | 46 | // report of metrics measured 47 | type report struct { 48 | latency2Way time.Duration 49 | latency1Way time.Duration 50 | timedOut bool 51 | } 52 | 53 | // map[target address string] => *[QOS_DCSP_VALUE] =>map[source port] 54 | type resultStore map[string]*[defines.NumQOSDCSPValues]map[layers.TCPPort]report 55 | type messageStore map[string]*[defines.NumQOSDCSPValues]srcPortScopedMessageStore 56 | 57 | type srcPortScopedMessageStore struct { 58 | sent srcPortScopedMessages 59 | rcvd srcPortScopedMessages 60 | } 61 | type srcPortScopedMessages map[layers.TCPPort]tcp.Message 62 | 63 | func (ms messageStore) target(target string, QosDSCPIndex uint8) *srcPortScopedMessageStore { 64 | // TODO: validate dscp is in range or create a dscp type alias 65 | if _, exists := ms[target]; !exists { 66 | ms[target] = new([defines.NumQOSDCSPValues]srcPortScopedMessageStore) 67 | } 68 | if ms[target][QosDSCPIndex].sent == nil { 69 | ms[target][QosDSCPIndex].sent = make(srcPortScopedMessages) 70 | } 71 | if ms[target][QosDSCPIndex].rcvd == nil { 72 | ms[target][QosDSCPIndex].rcvd = make(srcPortScopedMessages) 73 | } 74 | return &ms[target][QosDSCPIndex] 75 | } 76 | 77 | func (spsm srcPortScopedMessages) add(srcPort layers.TCPPort, message tcp.Message) { 78 | spsm[srcPort] = message 79 | } 80 | 81 | func (ms messageStore) sentAdd(target string, QosDSCPIndex uint8, srcPort layers.TCPPort, message tcp.Message) { 82 | ms.target(target, QosDSCPIndex).sent.add(srcPort, message) 83 | } 84 | 85 | func (ms messageStore) rcvdAdd(target string, QosDSCPIndex uint8, srcPort layers.TCPPort, message tcp.Message) { 86 | ms.target(target, QosDSCPIndex).rcvd.add(srcPort, message) 87 | } 88 | 89 | func (ms messageStore) existsRcvd(target string, QosDSCPIndex uint8, srcPort layers.TCPPort) (tcp.Message, bool) { 90 | 91 | if _, exists := ms[target]; !exists { 92 | return tcp.Message{}, false 93 | } 94 | if ms[target][QosDSCPIndex].rcvd == nil { 95 | return tcp.Message{}, false 96 | } 97 | matchedMsg, existsMatch := ms[target][QosDSCPIndex].rcvd[srcPort] 98 | if !existsMatch { 99 | return tcp.Message{}, false 100 | } 101 | return matchedMsg, true 102 | } 103 | 104 | func (ms messageStore) existsSent(target string, QosDSCPIndex uint8, srcPort layers.TCPPort) (tcp.Message, bool) { 105 | 106 | if _, exists := ms[target]; !exists { 107 | return tcp.Message{}, false 108 | } 109 | if ms[target][QosDSCPIndex].sent == nil { 110 | return tcp.Message{}, false 111 | } 112 | matchedMsg, existsMatch := ms[target][QosDSCPIndex].sent[srcPort] 113 | if !existsMatch { 114 | return tcp.Message{}, false 115 | } 116 | return matchedMsg, true 117 | } 118 | 119 | func (rs resultStore) add(target string, QosDSCPIndex uint8, srcPort layers.TCPPort, r report) { 120 | 121 | if rs[target] == nil { 122 | var resDSCP [defines.NumQOSDCSPValues]map[layers.TCPPort]report 123 | rs[target] = &resDSCP 124 | } 125 | if rs[target][QosDSCPIndex] == nil { 126 | rs[target][QosDSCPIndex] = make(map[layers.TCPPort]report) 127 | } 128 | rs[target][QosDSCPIndex][srcPort] = r 129 | } 130 | 131 | type resultWalker func(report, string, string, layers.TCPPort, bool, *log.Logger) 132 | 133 | func (rs resultStore) walkResults( 134 | remotes config.RemoteStore, 135 | currentDSCP *ip.DSCPValue, 136 | foreground bool, 137 | logger *log.Logger, 138 | walkerF ...resultWalker) { 139 | 140 | for target, r := range rs { 141 | remote, existsTarget := remotes[target] 142 | if !existsTarget { 143 | logger.Error("host exists in resultStore, but not in remoteStore", 144 | zap.String("host", target)) 145 | } 146 | 147 | qos := *currentDSCP 148 | if remote.External { 149 | qos = ip.DSCPBeLow 150 | } 151 | 152 | for srcPort, rep := range r[(ip.GetDSCP).Pos(qos, logger)] { 153 | walkerF[0](rep, remote.Hostname, remote.Location, srcPort, foreground, logger) 154 | } 155 | if len(walkerF) > 1 { 156 | logger.Error("only one result walker function expected currently") 157 | } 158 | } 159 | } 160 | 161 | // processResults calculates metrics, uploads stats and stores in results[] for stdout, if needed. 162 | func (rs resultStore) processResults( 163 | gl *config.Global, 164 | remotes config.RemoteStore, 165 | target string, 166 | req tcp.Message, 167 | rep tcp.Message, 168 | logger *log.Logger, 169 | ) report { 170 | 171 | // Calculate metrics 172 | l2w := rep.Ts.Run.Sub(req.Ts.Run) 173 | timedOut := l2w > gl.RemoteConfig.Timeout 174 | if timedOut { 175 | logger.Debug("Received timed-out echo response from", zap.String("target", target)) 176 | } 177 | 178 | l1w := rep.Ts.Payload.Sub(req.Ts.Unix) 179 | if rep.FromExternalTarget(gl.RemoteConfig.TargetTCPPort) { 180 | l1w = -1 181 | } 182 | 183 | r := report{ 184 | latency2Way: l2w, 185 | latency1Way: l1w, 186 | timedOut: timedOut} 187 | 188 | // Store processed report to 'result' data structure for stdout, if needed 189 | if !*(gl.CLI.SenderOnlyMode) { 190 | QosDSCPIndex := (ip.GetDSCP).Pos(req.QosDSCP, logger) 191 | rs.add(target, QosDSCPIndex, req.SrcPort, r) 192 | } 193 | 194 | return r 195 | } 196 | 197 | func (rs resultStore) printResults( 198 | gl *config.Global, 199 | remotes config.RemoteStore, 200 | currentDSCP *ip.DSCPValue, 201 | logger *log.Logger, 202 | ) { 203 | foreground := *gl.CLI.Foreground 204 | 205 | printTableHeader(gl, (*currentDSCP).Text(logger), logger) 206 | rs.walkResults(remotes, currentDSCP, foreground, logger, printTableEntry) 207 | printTableFooter(foreground, logger) 208 | } 209 | 210 | // Run processes the echoes sent and received to compute and report all the metrics desired. 211 | func Run( 212 | gl *config.Global, 213 | sentC chan tcp.Message, 214 | rcvdC chan tcp.Message, 215 | remotes config.RemoteStore, 216 | currentDSCP *ip.DSCPValue, 217 | sr metrics.Reporter, 218 | completeCycleUpload chan bool, 219 | wg *sync.WaitGroup, 220 | kill chan struct{}, 221 | logger *log.Logger, 222 | ) { 223 | go func() { 224 | for { 225 | logger.Debug("Entering new batch cycle collection.") 226 | 227 | // Have garbage collector clean messageStore and resultStore after every bach cycle interval 228 | ms := make(messageStore) 229 | rs := make(resultStore) 230 | 231 | batchWorker(gl, sentC, rcvdC, remotes, ms, rs, currentDSCP, statsUpload, sr, 232 | completeCycleUpload, kill, wg, logger) 233 | logger.Debug("Removing all state from current batch cycle collection.") 234 | 235 | select { 236 | case <-kill: 237 | logger.Debug("Collector goroutine returning.") 238 | return 239 | default: 240 | } 241 | } 242 | }() 243 | } 244 | 245 | func batchWorker( 246 | gl *config.Global, 247 | sentC chan tcp.Message, 248 | rcvdC chan tcp.Message, 249 | remotes config.RemoteStore, 250 | ms messageStore, 251 | rs resultStore, 252 | currentDSCP *ip.DSCPValue, 253 | sfn statsUploader, 254 | sr metrics.Reporter, 255 | completeCycleUpload chan bool, 256 | kill chan struct{}, 257 | wg *sync.WaitGroup, 258 | logger *log.Logger, 259 | ) { 260 | for { 261 | 262 | select { 263 | case out := <-sentC: 264 | if out.Type != tcp.EchoRequest { 265 | logger.Error("unexpected 'echo' type received in 'out' by collector.", 266 | zap.Any("type", out.Type)) 267 | continue 268 | } 269 | QosDSCPIndex := (ip.GetDSCP).Pos(out.QosDSCP, logger) 270 | 271 | // SYN sent 272 | targetKey := out.DstAddr.String() 273 | ms.sentAdd(targetKey, QosDSCPIndex, out.SrcPort, out) 274 | 275 | // Matching SYN ACK already received? 276 | matchedMsg, existsMatch := ms.existsRcvd(targetKey, QosDSCPIndex, out.SrcPort) 277 | if existsMatch && matchedMsg.Type == tcp.EchoReply && matchedMsg.Ack == out.Seq+1 { 278 | logger.Debug("response already exists for same target", 279 | zap.Any("message", matchedMsg)) 280 | 281 | report := rs.processResults(gl, remotes, targetKey, out, matchedMsg, logger) 282 | sfn(gl.RemoteConfig, sr, targetKey, remotes, out.QosDSCP, out.SrcPort, &report, logger) 283 | } 284 | 285 | case in := <-rcvdC: 286 | if in.Type != tcp.EchoReply { 287 | logger.Error("unexpected 'echo' type received in 'in' by collector.", 288 | zap.Any("type", in.Type)) 289 | continue 290 | } 291 | QosDSCPIndex := (ip.GetDSCP).Pos(in.QosDSCP, logger) 292 | 293 | // SYN+ACK received 294 | targetKey := in.SrcAddr.String() 295 | ms.rcvdAdd(targetKey, QosDSCPIndex, in.SrcPort, in) 296 | 297 | // SYN+ACK received from internal target/agent 298 | // SrcPort = source port of pkt received by external target/server 299 | // DstPort = to the well-defined arachne port 300 | portKey := in.SrcPort 301 | if in.FromExternalTarget(gl.RemoteConfig.TargetTCPPort) { 302 | // SYN+ACK received from external target/server 303 | // SrcPort = not well-defined arachne port (e.g. 80) 304 | // DstPort = source port of pkt received by external target/server 305 | portKey = in.DstPort 306 | } 307 | 308 | // Matching SYN probe exists in sent and intended targets (remotes)? 309 | probe, existsMatch := ms.existsSent(targetKey, QosDSCPIndex, portKey) 310 | if !existsMatch { 311 | u := "target" 312 | if _, existsTarget := remotes[targetKey]; existsTarget { 313 | u = "probe" 314 | } 315 | logger.Debug("received following response", 316 | zap.String("non-existing", u), 317 | zap.Any("response", ms[targetKey][QosDSCPIndex].rcvd[in.SrcPort]), 318 | zap.String("source_address", targetKey)) 319 | continue 320 | } 321 | 322 | if in.Ack != probe.Seq+1 { 323 | logger.Warn("unmatched ACK", 324 | zap.Uint32("in_ACK", in.Ack), 325 | zap.Uint32("out_SEQ", probe.Seq), 326 | zap.String("source_address", in.SrcAddr.String())) 327 | continue 328 | } 329 | report := rs.processResults(gl, remotes, targetKey, probe, in, logger) 330 | sfn(gl.RemoteConfig, sr, targetKey, remotes, in.QosDSCP, portKey, &report, logger) 331 | 332 | case <-completeCycleUpload: 333 | for key, value := range ms { 334 | logger.Debug("At end of batch cycle, sent and received 'messages' of", 335 | zap.String("host", key), 336 | zap.Any("messages", value)) 337 | } 338 | 339 | for key, value := range rs { 340 | logger.Debug("At end of batch cycle, 'result' of", 341 | zap.String("host", key), 342 | zap.Any("result", value)) 343 | } 344 | 345 | if !*gl.CLI.SenderOnlyMode { 346 | zeroOutResults(gl.RemoteConfig, ms, rs, remotes, sfn, sr, logger) 347 | 348 | //TODO print only tcp.DSCPBeLow when only external targets exist in remotes? 349 | rs.printResults(gl, remotes, currentDSCP, logger) 350 | } 351 | wg.Done() 352 | return 353 | case <-kill: 354 | logger.Info("Collector asked to exit without uploading.") 355 | return 356 | } 357 | } 358 | } 359 | 360 | type statsUploader func( 361 | glr *config.RemoteConfig, 362 | sr metrics.Reporter, 363 | target string, 364 | remotes config.RemoteStore, 365 | QOSDSCP ip.DSCPValue, 366 | srcPort layers.TCPPort, 367 | r *report, 368 | logger *log.Logger, 369 | ) 370 | 371 | func statsUpload( 372 | glr *config.RemoteConfig, 373 | sr metrics.Reporter, 374 | target string, 375 | remotes config.RemoteStore, 376 | QOSDSCP ip.DSCPValue, 377 | srcPort layers.TCPPort, 378 | r *report, 379 | logger *log.Logger, 380 | ) { 381 | remote, existsTarget := remotes[target] 382 | if !existsTarget { 383 | logger.Error("host exists in resultStore, but not in remoteStore", 384 | zap.String("host", target)) 385 | return 386 | } 387 | 388 | tags := map[string]string{ 389 | "host": glr.HostName, 390 | "host_location": glr.Location, 391 | "target": remote.Hostname, 392 | "target_location": remote.Location, 393 | "dscp": strconv.Itoa(int(QOSDSCP)), 394 | "source_port": strconv.Itoa(int(srcPort)), 395 | "timed_out": strconv.FormatBool((*r).timedOut), 396 | } 397 | 398 | // Both following in nanoseconds 399 | sr.ReportGauge("latency_2way", tags, int64((*r).latency2Way)) 400 | sr.ReportGauge("latency_1way", tags, (*r).latency1Way.Nanoseconds()) 401 | 402 | } 403 | 404 | // zeroOutResults fills latencies for targets not existing in resultStore with zeros. 405 | func zeroOutResults( 406 | glr *config.RemoteConfig, 407 | ms messageStore, 408 | rs resultStore, 409 | remotes config.RemoteStore, 410 | sfn statsUploader, 411 | sr metrics.Reporter, 412 | logger *log.Logger, 413 | ) { 414 | timedOutReport := report{ 415 | latency2Way: 0, 416 | latency1Way: 0, 417 | timedOut: true} 418 | 419 | for targetKey := range ms { 420 | _, existsTarget := rs[targetKey] 421 | if !existsTarget { 422 | var resDSCP [defines.NumQOSDCSPValues]map[layers.TCPPort]report 423 | rs[targetKey] = &resDSCP 424 | } 425 | for qosDSCP := 0; qosDSCP < defines.NumQOSDCSPValues; qosDSCP++ { 426 | if rs[targetKey][qosDSCP] == nil { 427 | rs[targetKey][qosDSCP] = make(map[layers.TCPPort]report) 428 | } 429 | for srcPort := range ms[targetKey][qosDSCP].sent { 430 | if _, existsSrc := rs[targetKey][qosDSCP][srcPort]; existsSrc { 431 | continue 432 | } 433 | rs[targetKey][qosDSCP][srcPort] = timedOutReport 434 | 435 | // Upload timed out results 436 | sfn(glr, sr, targetKey, remotes, ip.GetDSCP[qosDSCP], srcPort, &timedOutReport, logger) 437 | time.Sleep(1 * time.Millisecond) 438 | } 439 | } 440 | } 441 | } 442 | 443 | func printTableHeader(gl *config.Global, currentDSCP string, logger *log.Logger) { 444 | color.Set(color.FgHiYellow, color.Bold) 445 | defer color.Unset() 446 | 447 | if *gl.CLI.Foreground { 448 | fmt.Printf("%74s\n", "Arachne ["+defines.ArachneVersion+"]") 449 | fmt.Printf("%-55s%64s\n", 450 | gl.RemoteConfig.HostName+":"+strconv.Itoa(int(gl.RemoteConfig.TargetTCPPort))+ 451 | " with QoS DSCP '"+currentDSCP+"'", time.Now().Format(time.RFC850)) 452 | if gl.RemoteConfig.Location != "" && gl.RemoteConfig.Location != " " { 453 | fmt.Printf("Location: %s\n", gl.RemoteConfig.Location) 454 | } 455 | 456 | fmt.Printf("\n%51s|%26s|%8s%s%8s|\n", "", "", "", "RTT (msec)", "") 457 | fmt.Printf("Host%47s|%8s%s%10s|%4s%s%7s%s%5s|%2s%s\n", "", 458 | "", "Location", "", 459 | "", "2-way", "", "1-way", "", 460 | "", "Timed Out?") 461 | color.Set(color.FgHiYellow) 462 | fmt.Printf(strings.Repeat("-", hostWidth) + "|" + 463 | strings.Repeat("-", 26) + "|" + 464 | strings.Repeat("-", 26) + "|" + 465 | strings.Repeat("-", 13) + "\n") 466 | } else { 467 | logger.Info("Arachne -- Table of Results", 468 | zap.String("version", defines.ArachneVersion), 469 | zap.String("host", gl.RemoteConfig.HostName), 470 | zap.String("host_location", gl.RemoteConfig.Location), 471 | zap.Any("target_TCP_port", gl.RemoteConfig.TargetTCPPort), 472 | zap.String("QoS_DSCP", currentDSCP), 473 | ) 474 | } 475 | } 476 | 477 | func printTableFooter(foreground bool, logger *log.Logger) { 478 | color.Set(color.FgHiYellow) 479 | defer color.Unset() 480 | 481 | if foreground { 482 | fmt.Printf(strings.Repeat("-", tableWidth) + "\n") 483 | } else { 484 | logger.Info(strings.Repeat("-", tableWidth)) 485 | } 486 | } 487 | 488 | func printTableEntry( 489 | r report, 490 | targetHost string, 491 | targetLocation string, 492 | srcPort layers.TCPPort, 493 | foreground bool, 494 | logger *log.Logger, 495 | ) { 496 | var twoWay, oneWay zapcore.Field 497 | 498 | color.Set(color.FgHiYellow) 499 | defer color.Unset() 500 | 501 | timedOut := "no" 502 | if r.timedOut { 503 | timedOut = "yes" 504 | } 505 | if foreground { 506 | fmt.Printf("%-51s|", targetHost+"("+strconv.Itoa(int(srcPort))+")") 507 | fmt.Printf("%-26s|%3s", " "+targetLocation, "") 508 | } 509 | 510 | if r.latency2Way == 0 { 511 | twoWay = zap.String("2-way", "-") 512 | oneWay = zap.String("1-way", "-") 513 | if foreground { 514 | fmt.Printf("%4s %11s%8s%5s%s\n", "-", "-", "|", "", timedOut) 515 | } 516 | } else { 517 | twoWay = zap.Float64("2-way", float64(r.latency2Way/1e5)/10.0) 518 | // Ignore 1-way when echoing an external server or when estimated value is smaller than a threshold 519 | if r.latency1Way == -1 || r.latency1Way < 10*time.Nanosecond { 520 | if foreground { 521 | fmt.Printf("%5.1f %11s%7s%5s%s\n", float32(r.latency2Way/1e5)/10.0, 522 | "N/A", "|", "", timedOut) 523 | } 524 | oneWay = zap.String("1-way", "N/A") 525 | } else { 526 | if foreground { 527 | fmt.Printf("%5.1f %11.1f%7s%5s%s\n", float32(r.latency2Way/1e5)/10.0, 528 | float32(r.latency1Way/1e5)/10.0, "|", "", timedOut) 529 | } 530 | oneWay = zap.Float64("1-way", float64(r.latency1Way/1e5)/10.0/10.0) 531 | } 532 | } 533 | 534 | if !foreground { 535 | logger.Info("Result", 536 | zap.String("target", targetHost), 537 | zap.String("target_location", targetLocation), 538 | zap.Any("source_port", srcPort), 539 | twoWay, 540 | oneWay, 541 | zap.String("timed_out", timedOut)) 542 | } 543 | } 544 | -------------------------------------------------------------------------------- /collector/collector_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package collector 22 | 23 | import ( 24 | coreLog "log" 25 | "net" 26 | "sync" 27 | "testing" 28 | "time" 29 | 30 | "github.com/uber/arachne/config" 31 | "github.com/uber/arachne/defines" 32 | "github.com/uber/arachne/internal/ip" 33 | "github.com/uber/arachne/internal/log" 34 | "github.com/uber/arachne/internal/network" 35 | "github.com/uber/arachne/internal/tcp" 36 | "github.com/uber/arachne/internal/util" 37 | "github.com/uber/arachne/metrics" 38 | 39 | "github.com/google/gopacket/layers" 40 | "github.com/spacemonkeygo/monotime" 41 | "github.com/stretchr/testify/assert" 42 | "go.uber.org/zap" 43 | ) 44 | 45 | func statsUploadMock( 46 | glr *config.RemoteConfig, 47 | sr metrics.Reporter, 48 | target string, 49 | remotes config.RemoteStore, 50 | QOSDSCP ip.DSCPValue, 51 | srcPort layers.TCPPort, 52 | r *report, 53 | logger *log.Logger, 54 | ) { 55 | return 56 | } 57 | 58 | func TestRun(t *testing.T) { 59 | 60 | l, err := zap.NewDevelopment() 61 | if err != nil { 62 | coreLog.Fatal(err) 63 | } 64 | logger := &log.Logger{ 65 | Logger: l, 66 | PIDPath: "", 67 | RemovePID: util.RemovePID, 68 | } 69 | 70 | source := net.IPv4(10, 0, 0, 1) 71 | target := net.IPv4(20, 0, 0, 1) 72 | unreachableTarget := net.IPv4(21, 0, 0, 1) 73 | sourceIPv6 := net.IP{0x20, 0x01, 0x06, 0x13, 0x93, 0xFF, 0x8B, 0x40, 0, 0, 0, 0, 0, 0, 0, 1} 74 | targetIPv6 := net.IP{0x20, 0x04, 0x0B, 0xBD, 0x03, 0x2F, 0x0E, 0x41, 0, 0, 0, 0, 0, 0, 0, 2} 75 | currentDSCP := ip.DSCPBulkHigh 76 | sp := layers.TCPPort(32000) 77 | CLIArgs := config.CLIConfig{ 78 | SenderOnlyMode: func() *bool { b := false; return &b }(), 79 | Foreground: func() *bool { b := false; return &b }(), 80 | } 81 | gl := config.Global{ 82 | RemoteConfig: new(config.RemoteConfig), 83 | CLI: &CLIArgs, 84 | } 85 | 86 | remotes := make(map[string]config.Remote, defines.MaxNumRemoteTargets) 87 | remotes[target.String()] = config.Remote{ 88 | IP: target, 89 | AF: network.Family(&target), 90 | Hostname: "target.domain.com", 91 | External: false} 92 | remotes[unreachableTarget.String()] = config.Remote{ 93 | IP: unreachableTarget, 94 | AF: network.Family(&unreachableTarget), 95 | Hostname: "unreachabletarget.domain.com", 96 | External: false} 97 | remotes[targetIPv6.String()] = config.Remote{ 98 | IP: targetIPv6, 99 | AF: network.Family(&targetIPv6), 100 | Hostname: "targetIPv6.domain.com", 101 | External: true} 102 | ms := make(messageStore) 103 | rs := make(resultStore) 104 | 105 | sentC := make(chan tcp.Message, defines.ChannelOutBufferSize) 106 | rcvdC := make(chan tcp.Message, defines.ChannelInBufferSize) 107 | var finishedCycleUpload sync.WaitGroup 108 | completeCycleUpload := make(chan bool, 1) 109 | kill := make(chan struct{}) 110 | 111 | go func() { 112 | batchWorker(&gl, sentC, rcvdC, remotes, ms, rs, ¤tDSCP, statsUploadMock, nil, 113 | completeCycleUpload, kill, &finishedCycleUpload, logger) 114 | }() 115 | time.Sleep(50 * time.Millisecond) 116 | 117 | const initialSequence = uint32(50) 118 | sentC <- tcp.Message{ 119 | Type: tcp.EchoRequest, 120 | SrcAddr: source, 121 | DstAddr: target, 122 | Af: defines.AfInet, 123 | SrcPort: sp, 124 | QosDSCP: currentDSCP, 125 | Ts: tcp.Timestamp{ 126 | Run: monotime.Now(), 127 | Unix: time.Now()}, 128 | Seq: initialSequence, 129 | Ack: uint32(0), 130 | } 131 | time.Sleep(50 * time.Millisecond) 132 | rcvdC <- tcp.Message{ 133 | Type: tcp.EchoReply, 134 | SrcAddr: target, 135 | DstAddr: source, 136 | Af: defines.AfInet, 137 | SrcPort: sp, 138 | QosDSCP: currentDSCP, 139 | Ts: tcp.Timestamp{ 140 | Run: monotime.Now(), 141 | Payload: time.Now()}, 142 | Seq: uint32(346), 143 | Ack: initialSequence + 1, 144 | } 145 | time.Sleep(50 * time.Millisecond) 146 | 147 | sentC <- tcp.Message{ 148 | Type: tcp.EchoRequest, 149 | SrcAddr: source, 150 | DstAddr: unreachableTarget, 151 | Af: defines.AfInet, 152 | SrcPort: sp, 153 | QosDSCP: currentDSCP, 154 | Ts: tcp.Timestamp{ 155 | Run: monotime.Now(), 156 | Unix: time.Now()}, 157 | Seq: uint32(60), 158 | } 159 | time.Sleep(50 * time.Millisecond) 160 | 161 | sentC <- tcp.Message{ 162 | Type: tcp.EchoRequest, 163 | SrcAddr: sourceIPv6, 164 | DstAddr: targetIPv6, 165 | Af: defines.AfInet6, 166 | SrcPort: sp, 167 | QosDSCP: currentDSCP, 168 | Ts: tcp.Timestamp{ 169 | Run: monotime.Now(), 170 | Unix: time.Now()}, 171 | Seq: uint32(70), 172 | } 173 | time.Sleep(50 * time.Millisecond) 174 | 175 | finishedCycleUpload.Add(1) 176 | // Request from Collector to complete all stats uploads for this batch cycle 177 | completeCycleUpload <- true 178 | // Wait till the above request is fulfilled 179 | finishedCycleUpload.Wait() 180 | 181 | assert := assert.New(t) 182 | _, existsProbe := ms.existsSent(target.String(), (ip.GetDSCP).Pos(currentDSCP, logger), sp) 183 | assert.True(existsProbe, "Probe to "+target.String()+" should exist in 'sent' of messageStore") 184 | _, existsReply := ms.existsRcvd(target.String(), (ip.GetDSCP).Pos(currentDSCP, logger), sp) 185 | assert.True(existsReply, "Reply from "+target.String()+" should exist in 'rcvd' of messageStore") 186 | assert.Contains(ms, targetIPv6.String(), "Probe to "+targetIPv6.String()+ 187 | " should exist in 'sent' of messageStore") 188 | 189 | assert.Contains(remotes, unreachableTarget.String(), "Probe to "+unreachableTarget.String()+ 190 | " should exist in remotes[]") 191 | assert.Contains(rs, target.String(), "Probe to "+target.String()+" should exist in resultStore") 192 | 193 | assert.Contains(rs, unreachableTarget.String(), "Zero-ed probe to "+unreachableTarget.String()+ 194 | " should exist in resultStore") 195 | close(completeCycleUpload) 196 | } 197 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package config 22 | 23 | import ( 24 | "encoding/json" 25 | "fmt" 26 | "io/ioutil" 27 | "net" 28 | "net/http" 29 | "os" 30 | "runtime" 31 | "strings" 32 | "sync" 33 | "time" 34 | 35 | "github.com/uber/arachne/defines" 36 | "github.com/uber/arachne/internal/log" 37 | "github.com/uber/arachne/internal/network" 38 | "github.com/uber/arachne/internal/tcp" 39 | "github.com/uber/arachne/metrics" 40 | 41 | "github.com/google/gopacket/layers" 42 | cli "github.com/jawher/mow.cli" 43 | "github.com/pkg/errors" 44 | "go.uber.org/zap" 45 | "go.uber.org/zap/zapcore" 46 | "gopkg.in/validator.v2" 47 | "gopkg.in/yaml.v2" 48 | ) 49 | 50 | const defaultConfigFile = "/usr/local/etc/arachne/arachne.yaml" 51 | 52 | // BasicConfig holds the basic parameter configurations for the application. 53 | type BasicConfig struct { 54 | Logging log.Config `yaml:"logging"` 55 | Arachne ArachneConfiguration `yaml:"arachne"` 56 | } 57 | 58 | // ArachneConfiguration contains specific configuration to Arachne. 59 | type ArachneConfiguration struct { 60 | PIDPath string `yaml:"pidPath"` 61 | Orchestrator OrchestratorConfig `yaml:"orchestrator"` 62 | StandaloneTargetConfig string `yaml:"standaloneTargetConfig"` 63 | } 64 | 65 | // OrchestratorConfig contains configuration for the Arachne Orchestrator. 66 | type OrchestratorConfig struct { 67 | Enabled bool `yaml:"enabled"` 68 | AddrPort string `yaml:"addrport"` 69 | RESTVersion string `yaml:"restVersion"` 70 | } 71 | 72 | // Extended holds the parameter configurations implemented by outside callers. 73 | type Extended struct { 74 | Metrics metrics.Opt 75 | } 76 | 77 | // RemoteStore holds all Remotes. 78 | type RemoteStore map[string]Remote 79 | 80 | // Remote holds the info for every target to be echoed. 81 | type Remote struct { 82 | IP net.IP 83 | AF string 84 | Hostname string 85 | Location string 86 | External bool 87 | } 88 | 89 | type target struct { 90 | HostName string `json:"host_name"` 91 | IP string `json:"ip"` 92 | Location string 93 | } 94 | 95 | // RemoteFileConfig needed for the JSON decoder to know which fields to expect and parse. 96 | type RemoteFileConfig struct { 97 | Local struct { 98 | Location string `json:"location"` 99 | HostName string `json:"host_name"` 100 | SrcAddress string `json:"src_address"` 101 | InterfaceName string `json:"interface_name"` 102 | TargetTCPPort layers.TCPPort `json:"target_tcp_port"` 103 | Timeout string `json:"timeout"` 104 | BaseSrcTCPPort layers.TCPPort `json:"base_src_tcp_port"` 105 | NumSrcTCPPorts uint16 `json:"num_src_tcp_ports"` 106 | BatchInterval string `json:"batch_interval"` 107 | QoSEnabled string `json:"qos"` 108 | ResolveDNS string `json:"resolve_dns"` 109 | DNSServersAlt string `json:"dns_servers_alternate"` 110 | PollOrchestratorIntervalSuccess string `json:"poll_orchestrator_interval_success"` 111 | PollOrchestratorIntervalFailure string `json:"poll_orchestrator_interval_failure"` 112 | } `json:"local"` 113 | Internal []target `json:"internal"` 114 | External []target `json:"external"` 115 | } 116 | 117 | // AppConfig holds the info parsed from the local YAML config file. 118 | type AppConfig struct { 119 | Logging *zap.Config 120 | Verbose bool 121 | PIDPath string 122 | Orchestrator OrchestratorConfig 123 | StandaloneTargetConfig string 124 | Metrics metrics.Config 125 | } 126 | 127 | // CLIConfig holds the info parsed from CLI. 128 | type CLIConfig struct { 129 | ConfigFile *string 130 | Foreground *bool 131 | ReceiverOnlyMode *bool 132 | SenderOnlyMode *bool 133 | } 134 | 135 | // RemoteConfig holds the info parsed from the JSON config file. 136 | type RemoteConfig struct { 137 | Location string 138 | HostName string 139 | SrcAddress net.IP 140 | SrcTCPPortRange tcp.PortRange 141 | InterfaceName string 142 | TargetTCPPort layers.TCPPort 143 | Timeout time.Duration 144 | BatchInterval time.Duration 145 | QoSEnabled bool 146 | ResolveDNS bool 147 | DNSServersAlt []net.IP 148 | PollOrchestratorInterval pollInterval 149 | } 150 | 151 | // pollInterval holds the polling interval info. 152 | type pollInterval struct { 153 | Success time.Duration 154 | Failure time.Duration 155 | } 156 | 157 | // Global holds the global application info. 158 | type Global struct { 159 | App *AppConfig 160 | CLI *CLIConfig 161 | RemoteConfig *RemoteConfig 162 | Remotes RemoteStore 163 | } 164 | 165 | func localFileReadable(path string) error { 166 | if _, err := ioutil.ReadFile(path); err != nil { 167 | return err 168 | } 169 | return nil 170 | } 171 | 172 | // ParseCliArgs provides the usage and help menu, and parses the actual arguments. 173 | func ParseCliArgs(logger *log.Logger, service string, version string) *CLIConfig { 174 | args := new(CLIConfig) 175 | 176 | app := cli.App(service, "Utility to echo the DC and Cloud Infrastructure") 177 | 178 | app.Version("v version", "Arachne "+version) 179 | app.Spec = "[--foreground] [-c=] [--receiver_only] [--sender_only]" 180 | 181 | args.Foreground = app.BoolOpt("foreground", false, "Force foreground mode") 182 | args.ConfigFile = app.StringOpt("c config_file", defaultConfigFile, 183 | fmt.Sprintf("Agent's primary yaml configuration file (by default: %s)", defaultConfigFile)) 184 | args.ReceiverOnlyMode = app.BoolOpt("receiver_only", false, "Force TCP receiver-only mode") 185 | args.SenderOnlyMode = app.BoolOpt("sender_only", false, "Force TCP sender-only mode") 186 | 187 | app.Action = func() { 188 | logger.Debug("Command line arguments parsed") 189 | } 190 | 191 | app.Run(os.Args) 192 | return args 193 | } 194 | 195 | // Get fetches the configuration file from local path. 196 | func Get(cc *CLIConfig, ec *Extended, logger *log.Logger) (*AppConfig, error) { 197 | 198 | data, err := ioutil.ReadFile(*cc.ConfigFile) 199 | if err != nil { 200 | return nil, err 201 | } 202 | 203 | b, err := unmarshalBasicConfig(data, *cc.ConfigFile) 204 | if err != nil { 205 | return nil, err 206 | } 207 | 208 | mc, err := ec.Metrics.UnmarshalConfig(data, *cc.ConfigFile, logger.Logger) 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | output := []string{"stderr"} 214 | if b.Logging.StdOut || *cc.Foreground { 215 | output = []string{"stdout"} 216 | } 217 | if b.Logging.LogSink != "" { 218 | logger.Info("Log file path provided", zap.String("path", b.Logging.LogSink)) 219 | output = append(output, b.Logging.LogSink) 220 | } 221 | 222 | osHostname, _ := GetHostname(logger) 223 | initialFields := map[string]interface{}{ 224 | "service_name": defines.ArachneService, 225 | "hostname": osHostname, 226 | "PID": os.Getpid(), 227 | } 228 | 229 | var level zapcore.Level 230 | if err := level.Set(b.Logging.Level); err != nil { 231 | logger.Error("Log level provided", zap.Error(err)) 232 | } 233 | 234 | zc := zap.Config{ 235 | Level: zap.NewAtomicLevelAt(level), 236 | Development: false, 237 | DisableCaller: true, 238 | EncoderConfig: zap.NewProductionEncoderConfig(), 239 | Encoding: "json", 240 | ErrorOutputPaths: []string{"stdout"}, 241 | OutputPaths: output, 242 | InitialFields: initialFields, 243 | } 244 | 245 | cfg := AppConfig{ 246 | Logging: &zc, 247 | PIDPath: b.Arachne.PIDPath, 248 | Orchestrator: b.Arachne.Orchestrator, 249 | StandaloneTargetConfig: b.Arachne.StandaloneTargetConfig, 250 | Metrics: mc, 251 | } 252 | 253 | if !cfg.Orchestrator.Enabled && cfg.StandaloneTargetConfig == "" { 254 | return nil, errors.New("the standalone-mode target configuration file has not been specified") 255 | } 256 | 257 | return &cfg, nil 258 | } 259 | 260 | // unmarshalBasicConfig fetches the configuration file from local path. 261 | func unmarshalBasicConfig(data []byte, fname string) (*BasicConfig, error) { 262 | 263 | cfg := new(BasicConfig) 264 | if err := yaml.Unmarshal(data, cfg); err != nil { 265 | return nil, errors.Wrapf(err, "error unmarshaling the configuration file %s", fname) 266 | } 267 | // Validate on the merged config at the end 268 | if err := validator.Validate(cfg); err != nil { 269 | return nil, errors.Wrapf(err, "invalid info in configuration file %s", fname) 270 | } 271 | 272 | return cfg, nil 273 | } 274 | 275 | // FetchRemoteList fetches the configuration file from local path or, remotely, from Arachne Orchestrator. 276 | func FetchRemoteList( 277 | gl *Global, 278 | maxNumRemoteTargets int, 279 | maxNumSrcTCPPorts uint16, 280 | minBatchInterval time.Duration, 281 | HTTPResponseHeaderTimeout time.Duration, 282 | orchestratorRESTConf string, 283 | kill chan struct{}, 284 | logger *log.Logger, 285 | ) error { 286 | 287 | gl.RemoteConfig = new(RemoteConfig) 288 | // Map of all remote targets found in JSON configuration file 289 | remotes := make(RemoteStore, maxNumRemoteTargets) 290 | 291 | // Standalone (non-Orchestrator) mode 292 | if !gl.App.Orchestrator.Enabled { 293 | logger.Debug("Orchestrator mode disabled, using static target config file.", 294 | zap.String("path", gl.App.StandaloneTargetConfig)) 295 | 296 | if err := localFileReadable(gl.App.StandaloneTargetConfig); err != nil { 297 | logger.Fatal("unable to retrieve local target configuration file", 298 | zap.String("file", gl.App.StandaloneTargetConfig), 299 | zap.Error(err)) 300 | } 301 | logger.Info("Configuration file", zap.String("file", gl.App.StandaloneTargetConfig)) 302 | 303 | raw, err := ioutil.ReadFile(gl.App.StandaloneTargetConfig) 304 | if err != nil { 305 | return errors.Wrap(err, "file error") 306 | } 307 | if err := readRemoteList(raw, gl.RemoteConfig, remotes, maxNumSrcTCPPorts, minBatchInterval, 308 | logger); err != nil { 309 | logger.Fatal("error parsing default target list file", 310 | zap.String("file", gl.App.StandaloneTargetConfig), 311 | zap.Error(err)) 312 | } 313 | gl.Remotes = remotes 314 | return nil 315 | } 316 | 317 | // Orchestrator mode 318 | logger.Info("Orchestrator mode enabled") 319 | // Initial value before JSON file has been parsed 320 | gl.RemoteConfig.PollOrchestratorInterval = pollInterval{ 321 | Success: 2 * time.Hour, 322 | Failure: 2 * time.Minute, 323 | } 324 | err := refreshRemoteList(gl, remotes, maxNumSrcTCPPorts, minBatchInterval, HTTPResponseHeaderTimeout, 325 | orchestratorRESTConf, kill, logger) 326 | return err 327 | } 328 | 329 | // createHTTPClient returns an HTTP client to connect to remote server. 330 | func createHTTPClient(timeout time.Duration, disableKeepAlives bool) *http.Client { 331 | client := &http.Client{ 332 | Transport: &http.Transport{ 333 | ResponseHeaderTimeout: timeout, 334 | Dial: (&net.Dialer{ 335 | Timeout: timeout, 336 | }).Dial, 337 | DisableKeepAlives: disableKeepAlives, 338 | }, 339 | } 340 | 341 | return client 342 | } 343 | 344 | // GetHostname returns the hostname. 345 | func GetHostname(logger *log.Logger) (string, error) { 346 | host, err := os.Hostname() 347 | if err != nil { 348 | logger.Warn("Failed to extract hostname from OS", zap.Error(err)) 349 | return "unknown", err 350 | } 351 | return host, nil 352 | } 353 | 354 | // refreshRemoteList checks with Arachne Orchestrator if new a config file should be fetched. 355 | func refreshRemoteList( 356 | gl *Global, 357 | remotes RemoteStore, 358 | maxNumSrcTCPPorts uint16, 359 | minBatchInterval time.Duration, 360 | HTTPResponseHeaderTimeout time.Duration, 361 | orchestratorRESTConf string, 362 | kill chan struct{}, 363 | logger *log.Logger, 364 | ) error { 365 | 366 | client := createHTTPClient(HTTPResponseHeaderTimeout, true) 367 | retryTime := gl.RemoteConfig.PollOrchestratorInterval.Failure 368 | 369 | for { 370 | hostname, _ := GetHostname(logger) 371 | RESTReq := fmt.Sprintf("http://%s/%s/%s?hostname=%s", 372 | gl.App.Orchestrator.AddrPort, 373 | gl.App.Orchestrator.RESTVersion, 374 | orchestratorRESTConf, 375 | hostname) 376 | logger.Debug("Sending HTTP request to Orchestrator", zap.String("request", RESTReq)) 377 | respCode, raw, err := fetchRemoteListFromOrchestrator(client, RESTReq, logger) 378 | if err == nil { 379 | switch respCode { 380 | case http.StatusOK: 381 | logger.Info("Target list downloaded successfully from Orchestrator", 382 | zap.String("addrport", gl.App.Orchestrator.AddrPort)) 383 | err = readRemoteList(raw, gl.RemoteConfig, remotes, maxNumSrcTCPPorts, 384 | minBatchInterval, logger) 385 | if err != nil { 386 | logger.Error("error parsing downloaded configuration file", 387 | zap.Error(err)) 388 | goto contError 389 | } 390 | gl.Remotes = remotes 391 | logger.Info("Will poll Orchestrator again later", 392 | zap.String("retry_time", 393 | gl.RemoteConfig.PollOrchestratorInterval.Success.String())) 394 | return nil 395 | 396 | case http.StatusNotFound: 397 | retryTime = gl.RemoteConfig.PollOrchestratorInterval.Success 398 | goto stayIdle 399 | } 400 | } 401 | logger.Info("Failed to download configuration file", zap.Error(err)) 402 | 403 | contError: 404 | if len(gl.Remotes) != 0 { 405 | logger.Debug("last successfully fetched target list will be re-used") 406 | return nil 407 | 408 | } 409 | stayIdle: 410 | // Do not proceed until we have attempted to download the config file at least once. 411 | logger.Info("Retrying configuration download", zap.String("retry_time", retryTime.String())) 412 | confRetry := time.NewTicker(retryTime) 413 | defer confRetry.Stop() 414 | 415 | select { 416 | case <-confRetry.C: 417 | continue 418 | case <-kill: 419 | logger.Debug("Requested to exit while trying to fetch configuration file.") 420 | return errors.New("received SIG") 421 | } 422 | } 423 | 424 | } 425 | 426 | func fetchRemoteListFromOrchestrator( 427 | client *http.Client, 428 | url string, 429 | logger *log.Logger, 430 | ) (int, []byte, error) { 431 | 432 | var bResp []byte 433 | 434 | // Build the request 435 | req, err := http.NewRequest("GET", url, nil) 436 | if err != nil { 437 | logger.Warn("NewRequest", zap.Error(err)) 438 | return 0, nil, err 439 | } 440 | resp, err := client.Do(req) 441 | if err != nil { 442 | logger.Warn("HTTP fetch failure", zap.Error(err)) 443 | return 0, nil, err 444 | } 445 | defer resp.Body.Close() 446 | 447 | logger.Logger = logger.With(zap.String("status_text", http.StatusText(resp.StatusCode)), 448 | zap.Int("status_code", resp.StatusCode)) 449 | 450 | switch resp.StatusCode { 451 | case http.StatusOK: 452 | logger.Debug("HTTP response status code from Orchestrator") 453 | bResp, err = ioutil.ReadAll(resp.Body) 454 | case http.StatusNotFound: 455 | err = errors.New("HTTP response from Orchestrator: 'Idle mode'") 456 | case http.StatusBadRequest: 457 | err = errors.New("HTTP response from Orchestrator: 'Please specify hostname or DC!'") 458 | case http.StatusInternalServerError: 459 | logger.Warn("HTTP response from Orchestrator: Error opening requested configuration file") 460 | default: 461 | err = errors.New("unhandled HTTP response from Orchestrator") 462 | } 463 | 464 | return resp.StatusCode, bResp, err 465 | } 466 | 467 | func isTrue(s string) bool { 468 | l := strings.ToLower(s) 469 | return l == "enabled" || l == "true" 470 | } 471 | 472 | // readRemoteList decodes the Arachne JSON config file that includes information 473 | // about all the hosts to be tested and validates all IP addresses. 474 | func readRemoteList( 475 | raw []byte, 476 | glRC *RemoteConfig, 477 | remotes RemoteStore, 478 | maxNumSrcTCPPorts uint16, 479 | minBatchInterval time.Duration, 480 | logger *log.Logger, 481 | ) error { 482 | c := new(RemoteFileConfig) 483 | 484 | if err := json.Unmarshal(raw, c); err != nil { 485 | return errors.Wrap(err, "configuration file parse error") 486 | } 487 | 488 | // Populate global variables 489 | glRC.Location = strings.ToLower(c.Local.Location) 490 | if glRC.Location == "" { 491 | logger.Warn("Location not provided in config file") 492 | } 493 | 494 | glRC.HostName = strings.ToLower(c.Local.HostName) 495 | if glRC.HostName == "" { 496 | logger.Debug("Hostname not provided in config file") 497 | glRC.HostName, _ = GetHostname(logger) 498 | } else { 499 | logger.Info("Remotely assigned hostname for metrics uploading", 500 | zap.String("metrics_hostname", glRC.HostName)) 501 | } 502 | 503 | glRC.InterfaceName = strings.ToLower(c.Local.InterfaceName) 504 | switch { 505 | case runtime.GOOS == "linux" && strings.Contains(glRC.InterfaceName, "en"), 506 | runtime.GOOS == "darwin" && strings.Contains(glRC.InterfaceName, "eth"): 507 | logger.Warn("Specified interface may not be applicable to actual OS", 508 | zap.String("interface", glRC.InterfaceName), 509 | zap.String("OS", runtime.GOOS)) 510 | } 511 | 512 | srcIP, err := network.GetSourceAddr("ip4", strings.ToLower(c.Local.SrcAddress), 513 | glRC.HostName, glRC.InterfaceName, logger) 514 | if err != nil { 515 | srcIP, err = network.GetSourceAddr("ip6", strings.ToLower(c.Local.SrcAddress), 516 | glRC.HostName, glRC.InterfaceName, logger) 517 | if err != nil { 518 | return errors.Wrap(err, "could not retrieve an IPv4 or IPv6 source address") 519 | } 520 | } 521 | glRC.SrcAddress = *srcIP 522 | logger.Debug("Arachne agent's source IP address", zap.Any("address", glRC.SrcAddress)) 523 | 524 | glRC.TargetTCPPort = c.Local.TargetTCPPort 525 | if glRC.Timeout, err = time.ParseDuration(c.Local.Timeout); err != nil { 526 | return errors.Wrap(err, "failed to parse the timeout") 527 | } 528 | glRC.SrcTCPPortRange[0] = c.Local.BaseSrcTCPPort 529 | if c.Local.NumSrcTCPPorts > maxNumSrcTCPPorts { 530 | return errors.Errorf("not more than %d ephemeral source TCP ports may be used", 531 | maxNumSrcTCPPorts) 532 | } 533 | if c.Local.NumSrcTCPPorts == 0 { 534 | return errors.New("cannot specify zero source TCP ports") 535 | } 536 | glRC.SrcTCPPortRange[1] = c.Local.BaseSrcTCPPort + layers.TCPPort(c.Local.NumSrcTCPPorts) - 1 537 | if glRC.SrcTCPPortRange.Contains(glRC.TargetTCPPort) { 538 | return errors.Errorf("the listen TCP port cannot reside in the range of the ephemeral TCP "+ 539 | "source ports [%d-%d]", glRC.SrcTCPPortRange[0], glRC.SrcTCPPortRange[1]) 540 | } 541 | if glRC.BatchInterval, err = time.ParseDuration(c.Local.BatchInterval); err != nil { 542 | return errors.Wrap(err, "failed to parse the batch interval") 543 | } 544 | if glRC.BatchInterval < minBatchInterval { 545 | return errors.Errorf("the batch cycle interval cannot be shorter than %v", minBatchInterval) 546 | } 547 | if glRC.PollOrchestratorInterval.Success, err = 548 | time.ParseDuration(c.Local.PollOrchestratorIntervalSuccess); err != nil { 549 | return errors.Wrap(err, "failed to parse the Orchestrator poll interval for success") 550 | } 551 | if glRC.PollOrchestratorInterval.Failure, err = 552 | time.ParseDuration(c.Local.PollOrchestratorIntervalFailure); err != nil { 553 | return errors.Wrap(err, "failed to parse the Orchestrator poll interval for failure") 554 | } 555 | 556 | glRC.QoSEnabled = isTrue(c.Local.QoSEnabled) 557 | glRC.ResolveDNS = isTrue(c.Local.ResolveDNS) 558 | 559 | DNSInput := strings.Split(c.Local.DNSServersAlt, ",") 560 | for _, server := range DNSInput { 561 | currDNSIP := net.ParseIP(strings.TrimSpace(server)) 562 | if currDNSIP == nil { 563 | return errors.Errorf("configuration file parse error: "+ 564 | "invalid IP address for DNS server: %v", currDNSIP) 565 | } 566 | glRC.DNSServersAlt = append(glRC.DNSServersAlt, currDNSIP) 567 | 568 | } 569 | logger.Debug("Alternate DNS servers configured", zap.Any("servers", glRC.DNSServersAlt)) 570 | 571 | walkTargets(glRC, c.Internal, false, remotes, logger) 572 | walkTargets(glRC, c.External, true, remotes, logger) 573 | 574 | for key, r := range remotes { 575 | logger.Debug("Remote", zap.String("key", key), zap.Any("object", r)) 576 | } 577 | 578 | return nil 579 | } 580 | 581 | // Validate and create map of ipv4 and ipv6 addresses with string as their key. 582 | func walkTargets(grc *RemoteConfig, ts []target, ext bool, remotes RemoteStore, logger *log.Logger) { 583 | 584 | for _, t := range ts { 585 | if grc.ResolveDNS && t.HostName != "" { 586 | addrs, err := net.LookupHost(t.HostName) 587 | if err != nil { 588 | logger.Error("failed to DNS resolve hostname", zap.Error(err)) 589 | continue 590 | } 591 | t.IP = addrs[0] 592 | } 593 | 594 | // Validate address string 595 | currIP := net.ParseIP(t.IP) 596 | if currIP == nil { 597 | logger.Error("configuration file parse error", 598 | zap.String("err", "invalid IP address for host %s"), 599 | zap.String("hostname", t.HostName)) 600 | } 601 | if currIP.Equal(grc.SrcAddress) { 602 | logger.Debug("Local server's address not added in remote target list", 603 | zap.String("JSON_source_address", grc.SrcAddress.String()), 604 | zap.String("target", currIP.String())) 605 | continue 606 | } 607 | remotes[currIP.String()] = Remote{currIP, network.Family(&currIP), 608 | t.HostName, t.Location, ext} 609 | } 610 | } 611 | 612 | // ResolveDNSTargets resolves the DNS names of the IP addresses of all echo targets and the localhost. 613 | func ResolveDNSTargets( 614 | remotes RemoteStore, 615 | grc *RemoteConfig, 616 | DNSRefresh *time.Ticker, 617 | wg *sync.WaitGroup, 618 | kill chan struct{}, 619 | logger *log.Logger, 620 | ) { 621 | go func() { 622 | // Resolve 623 | if localHost, err := network.ResolveIP(grc.SrcAddress.String(), 624 | grc.DNSServersAlt, logger); err == nil { 625 | if grc.HostName == "" { 626 | grc.HostName = localHost 627 | } else if grc.HostName != strings.ToLower(localHost) { 628 | logger.Warn("DNS-resolved local hostname is different from "+ 629 | "configured local hostname", 630 | zap.String("DNS-resolved_hostname", localHost), 631 | zap.String("configured_hostname", grc.HostName)) 632 | } 633 | } 634 | 635 | for { 636 | for addressKey := range remotes { 637 | hostname := remotes[addressKey].Hostname 638 | // Do not update hostname for external targets 639 | if !remotes[addressKey].External { 640 | //hostname = addressKey 641 | if grc.ResolveDNS { 642 | if h, err := network.ResolveIP(addressKey, grc.DNSServersAlt, 643 | logger); err == nil { 644 | hostname = h 645 | logger.Debug("DNS resolution", 646 | zap.String("address", addressKey), 647 | zap.String("hostname", hostname)) 648 | 649 | } 650 | } 651 | } 652 | 653 | currIP := net.ParseIP(addressKey) 654 | remotes[addressKey] = Remote{currIP, network.Family(&currIP), hostname, 655 | remotes[addressKey].Location, remotes[addressKey].External, 656 | } 657 | } 658 | wg.Done() 659 | 660 | select { 661 | case <-DNSRefresh.C: 662 | continue 663 | case <-kill: 664 | DNSRefresh.Stop() 665 | logger.Debug("ResolveDNSTargets goroutine returning") 666 | return 667 | } 668 | } 669 | }() 670 | } 671 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package config 22 | 23 | import ( 24 | "io/ioutil" 25 | coreLog "log" 26 | "runtime" 27 | "testing" 28 | 29 | "github.com/uber/arachne/defines" 30 | "github.com/uber/arachne/internal/log" 31 | "github.com/uber/arachne/internal/util" 32 | 33 | "github.com/stretchr/testify/assert" 34 | "go.uber.org/zap" 35 | ) 36 | 37 | func TestReadConfig(t *testing.T) { 38 | assert := assert.New(t) 39 | var ( 40 | err error 41 | gl Global 42 | testConfigFilePath string 43 | ) 44 | 45 | l, err := zap.NewDevelopment() 46 | if err != nil { 47 | coreLog.Fatal(err) 48 | } 49 | logger := &log.Logger{ 50 | Logger: l, 51 | PIDPath: "", 52 | RemovePID: util.RemovePID, 53 | } 54 | 55 | gl.RemoteConfig = new(RemoteConfig) 56 | remotes := make(RemoteStore, defines.MaxNumRemoteTargets) 57 | 58 | switch runtime.GOOS { 59 | case "linux": 60 | testConfigFilePath = defines.ArachneTestConfigFilePathLinux 61 | case "darwin": 62 | testConfigFilePath = defines.ArachneTestConfigFilePathDarwin 63 | default: 64 | t.Fatalf("unsupported OS for testing: " + runtime.GOOS) 65 | } 66 | 67 | raw, err := ioutil.ReadFile(testConfigFilePath) 68 | if err != nil { 69 | t.Errorf("File error: %v. Exiting...\n", err) 70 | } 71 | err = readRemoteList(raw, gl.RemoteConfig, remotes, defines.MaxNumSrcTCPPorts, defines.MinBatchInterval, 72 | logger) 73 | assert.NoError(err, "error parsing YAML test configuration file: %v", err) 74 | 75 | assert.Equal("us-west", gl.RemoteConfig.Location, 76 | "error parsing 'location' from YAML test configuration file") 77 | assert.Equal(44111, int(gl.RemoteConfig.TargetTCPPort), 78 | "error parsing 'listen_tcp_port' from YAML test configuration file") 79 | assert.Equal(31000, int(gl.RemoteConfig.SrcTCPPortRange[0]), 80 | "error parsing 'base_src_tcp_port' from YAML test configuration file") 81 | if !assert.Equal(int64(10000000000), int64(gl.RemoteConfig.BatchInterval)) { 82 | t.Error("error parsing 'batch_interval' from YAML test configuration file") 83 | } 84 | 85 | assert.False(gl.RemoteConfig.QoSEnabled, "error parsing QoS from YAML test configuration file") 86 | assert.True(gl.RemoteConfig.ResolveDNS, "error parsing resolve_dns from YAML test configuration file") 87 | 88 | if r1, ok := remotes["10.10.39.31"]; !ok { 89 | t.Errorf("Remotes are %+v in %s", remotes, testConfigFilePath) 90 | t.Error("failed to parse a remote target") 91 | } else { 92 | assert.Equal("us-west", r1.Location, 93 | "error parsing location of an 'internal' target from YAML test configuration file") 94 | } 95 | 96 | assert.Equal(int64(10500000000000), int64(gl.RemoteConfig.PollOrchestratorInterval.Success), 97 | "error parsing 'poll_orchestrator_interval_success' from YAML test configuration file") 98 | assert.Equal(int64(60000000000), int64(gl.RemoteConfig.PollOrchestratorInterval.Failure), 99 | "error parsing 'poll_orchestrator_interval_failure' from YAML test configuration file") 100 | } 101 | 102 | func TestDownloadTargetFileFromOrchestrator(t *testing.T) { 103 | 104 | t.Parallel() 105 | 106 | l, err := zap.NewDevelopment() 107 | if err != nil { 108 | coreLog.Fatal(err) 109 | } 110 | logger := &log.Logger{ 111 | Logger: l, 112 | PIDPath: "", 113 | RemovePID: util.RemovePID, 114 | } 115 | 116 | const timeout = defines.HTTPResponseHeaderTimeout 117 | client := createHTTPClient(timeout, true) 118 | RESTUrl := "" 119 | if _, _, err := fetchRemoteListFromOrchestrator(client, RESTUrl, logger); err == nil { 120 | t.Errorf("Configuration file downloaded without URL of Orchestrator provided") 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /debian/README.Debian: -------------------------------------------------------------------------------- 1 | Arachne for Debian 2 | ------------------ 3 | 4 | Arachne is Uber's framework of in-house developed, strategically distributed, light-weight server agents that, 5 | in an always-on and active fashion, perform end-to-end functional testing of all the network connected 6 | infrastructure components and, on top of that, monitor the health of the DC and Cloud infrastructure and 7 | can discern between an infrastructure issue and an application issue. 8 | Arachne is a packet loss detection system and an underperforming path detection system that is able to detect 9 | the following intra-DC, inter-DC, DC-to-Cloud, and DC-to-External-Services issues by generating minimal 10 | traffic that is the ‘closest’ to the typical type of traffic an Uber application generates: 11 | - Reachability 12 | - Round-trip and 1-way (Avg, Best, Worst, StDev) Latency 13 | - Silent packet drops and black holes 14 | - Jitter (average of the deviation from the network mean latency) 15 | - PMTU or Firewall issues too related possibly to network config changes (accidental or not) 16 | - Whether network-level SLAs are met 17 | 18 | Arachne runs on Linux and Darwin (Mac OS X). 19 | 20 | -- Vasileios Lakafosis Fri, 23 Dec 2016 21:37:13 +0000 -------------------------------------------------------------------------------- /debian/README.source: -------------------------------------------------------------------------------- 1 | Arachne for Debian 2 | ------------------ 3 | 4 | Arachne is Uber's framework of in-house developed, strategically distributed, light-weight server agents that, 5 | in an always-on and active fashion, perform end-to-end functional testing of all the network connected 6 | infrastructure components and, on top of that, monitor the health of the DC and Cloud infrastructure and 7 | can discern between an infrastructure issue and an application issue. 8 | Arachne is a packet loss detection system and an underperforming path detection system that is able to detect 9 | the following intra-DC, inter-DC, DC-to-Cloud, and DC-to-External-Services issues by generating minimal 10 | traffic that is the ‘closest’ to the typical type of traffic an Uber application generates: 11 | - Reachability 12 | - Round-trip and 1-way (Avg, Best, Worst, StDev) Latency 13 | - Silent packet drops and black holes 14 | - Jitter (average of the deviation from the network mean latency) 15 | - PMTU or Firewall issues too related possibly to network config changes (accidental or not) 16 | - Whether network-level SLAs are met 17 | 18 | Arachne runs on Linux and Darwin (Mac OS X). 19 | 20 | -- Vasileios Lakafosis Fri, 23 Dec 2016 21:37:13 +0000 21 | -------------------------------------------------------------------------------- /debian/arachne.install: -------------------------------------------------------------------------------- 1 | arachned/arachne /usr/bin 2 | arachned/config/* /usr/local/etc/arachne/ -------------------------------------------------------------------------------- /debian/arachne.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # postinst script for arachne 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | # source debconf library 9 | . /usr/share/debconf/confmodule 10 | 11 | case "$1" in 12 | 13 | configure) 14 | 15 | echo "Creating arachne group, if it doesn't already exist" 16 | if ! getent group arachne >/dev/null; then 17 | addgroup --quiet --system arachne 18 | fi 19 | 20 | echo "Granting CAP_NET_RAW capability to the arachne binary" 21 | setcap cap_net_raw+ep /usr/bin/arachne 22 | 23 | echo "Preparing folder paths" 24 | mkdir -p /var/arachne /var/log/arachne /var/run/arachne 25 | chmod -R 0775 /var/arachne /var/log/arachne /var/run/arachne 26 | rm -f /var/run/arachne/arachne.pid 27 | ;; 28 | 29 | esac 30 | 31 | # dh_installdeb will replace this with shell code automatically 32 | # generated by other debhelper scripts. 33 | 34 | #DEBHELPER# 35 | 36 | db_stop 37 | 38 | exit 0 39 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | arachne (0.6.0) unstable; urgency=low 2 | * Refactor sending and receiving of packets 3 | -- Henri Devieux Sat, 22 July 2017 09:35:28 +0000 4 | 5 | arachne (0.5.3) unstable; urgency=low 6 | * Add support for target location info 7 | -- Vasileios Lakafosis Fri, 11 July 2017 12:40:00 +0000 8 | 9 | arachne (0.2.0) unstable; urgency=low 10 | * Initial open-source release 11 | -- Vasileios Lakafosis Fri, 23 Dec 2016 14:30:00 +0000 12 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: arachne 2 | Section: utils 3 | Priority: optional 4 | Maintainer: Vasileios Lakafosis 5 | Standards-Version: 3.9.5 6 | Homepage: https://github.com/uber/arachne 7 | Build-Depends: debhelper (>= 9), 8 | golang-1.7, 9 | git 10 | 11 | Package: arachne 12 | Architecture: any 13 | Depends: libcap2-bin, ${misc:Depends}, 14 | Description: Arachne is Uber's framework of in-house developed, strategically distributed, 15 | light-weight server agents to probe the health of the DC and Cloud infrastructure. 16 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Author: Uber Technologies, Inc 2 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 3 | Upstream-Name: arachne 4 | Upstream-Contact: Uber Technologies, Inc 5 | 6 | Files: * 7 | Copyright (c) 2016 Uber Technologies, Inc 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | README.md 2 | LICENSE 3 | -------------------------------------------------------------------------------- /debian/gbp.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | upstream-tree=tag 3 | upstream-tag=v%(version)s 4 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # See debhelper(7) (uncomment to enable) 3 | # output every command that modifies files on the build system. 4 | DH_VERBOSE = 1 5 | 6 | # see EXAMPLES in dpkg-buildflags(1) and read /usr/share/dpkg/* 7 | DPKG_EXPORT_BUILDFLAGS = 1 8 | include /usr/share/dpkg/default.mk 9 | 10 | # see FEATURE AREAS in dpkg-buildflags(1) 11 | #export DEB_BUILD_MAINT_OPTIONS = hardening=+all 12 | 13 | # see ENVIRONMENT in dpkg-buildflags(1) 14 | # package maintainers to append CFLAGS 15 | #export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic 16 | # package maintainers to append LDFLAGS 17 | #export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed 18 | 19 | 20 | # main packaging script based on dh7 syntax 21 | %: 22 | dh $@ 23 | # dh $@ --with-systemd 24 | 25 | # debmake generated override targets 26 | # This is example for Cmake (See http://bugs.debian.org/641051 ) 27 | #override_dh_auto_configure: 28 | # dh_auto_configure -- \ 29 | # -DCMAKE_LIBRARY_PATH=$(DEB_HOST_MULTIARCH) 30 | 31 | BINNAME = arachne 32 | DESTBINDIR = /usr/bin 33 | DESTCFGDIR = /usr/local/etc/$(BINNAME) 34 | DESTLOGDIR = /var/$(BINNAME) 35 | 36 | install/arachne:: 37 | mkdir -p $(DESTCFGDIR) 38 | chmod -R 0775 $(DESTCFGDIR) 39 | cp ./config/* $(DESTCFGDIR) 40 | mkdir -p $(DESTLOGDIR) 41 | chmod -R 0775 $(DESTLOGDIR) 42 | setcap cap_net_raw+ep $(DESTBINDIR)/$(BINNAME) 43 | cp debian/tmp/* debian/$(BINNAME)/ -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /defines/defines.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package defines 22 | 23 | import ( 24 | "syscall" 25 | "time" 26 | ) 27 | 28 | // Global constants 29 | const ( 30 | ArachneService = "arachne" 31 | ArachneVersion = "0.6.0" //TODO Read from file version.git 32 | ArachneTestConfigFilePathLinux = "../arachned/config/test_target_config_linux.json" 33 | ArachneTestConfigFilePathDarwin = "../arachned/config/test_target_config_darwin.json" 34 | BatchIntervalEchoingPerc = 0.75 35 | BatchIntervalUploadStats = 0.95 36 | ChannelInBufferSize = 800 37 | ChannelOutBufferSize = 800 38 | DNSRefreshInterval = 12 * time.Hour 39 | HTTPResponseHeaderTimeout = 10 * time.Second 40 | IPTTL = 64 41 | IPv4HeaderLength = 20 42 | IPv6HeaderLength = 40 43 | LogFileSizeMaxMB = 15 44 | LogFileSizeKeepKB = 250 45 | OrchestratorRESTConf = "conf" 46 | MaxNumRemoteTargets = 250 47 | MaxNumSrcTCPPorts = 512 48 | MaxPacketSizeBytes = 1500 49 | MinBatchInterval = 10 * time.Second 50 | NumQOSDCSPValues = 11 51 | PcapMaxSnapLen = 128 52 | PortHTTP = 80 53 | PortHTTPS = 443 54 | TCPHeaderLength = 20 55 | TCPWindowSize = 0xaaaa 56 | TimestampPayloadLengthBytes = 15 57 | ) 58 | 59 | // Internet Address Families 60 | const ( 61 | AfInet = syscall.AF_INET 62 | AfInet6 = syscall.AF_INET6 63 | ) 64 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | /* 22 | 23 | Package arachne provides a packet loss detection system and an underperforming 24 | path detection system. It provides fast and easy active end-to-end functional 25 | testing of all the components in Data Center and Cloud infrastructures. 26 | Arachne is able to detect intra-DC, inter-DC, DC-to-Cloud, and 27 | DC-to-External-Services issues by generating minimal traffic. 28 | 29 | */ 30 | package arachne 31 | -------------------------------------------------------------------------------- /example_run_arachne_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package arachne_test 22 | 23 | import ( 24 | "github.com/uber/arachne" 25 | "github.com/uber/arachne/config" 26 | "github.com/uber/arachne/metrics" 27 | ) 28 | 29 | func ExampleRun() { 30 | 31 | mc := new(metrics.StatsdConfiger) 32 | 33 | ec := &config.Extended{ 34 | Metrics: metrics.Opt(*mc), 35 | } 36 | 37 | arachne.Run( 38 | ec, 39 | arachne.ReceiverOnlyMode(false), 40 | ) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: a9bff16d97def430684caa9d3f9ac72b64d608ccd0681c17e01630d436c47ff3 2 | updated: 2019-06-24T23:14:28.048475873-07:00 3 | imports: 4 | - name: github.com/DataDog/datadog-go 5 | version: 8a2a04a8934ddfed041b578a82119e6616f2c4e7 6 | subpackages: 7 | - statsd 8 | - name: github.com/fatih/color 9 | version: 3f9d52f7176a6927daacff70a3e8d1dc2025c53e 10 | - name: github.com/google/gopacket 11 | version: 836b571ec913f74022809b997ad331ebbf1033d9 12 | subpackages: 13 | - layers 14 | - name: github.com/jawher/mow.cli 15 | version: 9b8ce431ec028691b79d71211c3848f1ad4078d7 16 | subpackages: 17 | - internal/container 18 | - internal/flow 19 | - internal/fsm 20 | - internal/lexer 21 | - internal/matcher 22 | - internal/parser 23 | - internal/values 24 | - name: github.com/mattn/go-colorable 25 | version: 8029fb3788e5a4a9c00e415f586a6d033f5d38b3 26 | - name: github.com/mattn/go-isatty 27 | version: 1311e847b0cb909da63b5fecfb5370aa66236465 28 | - name: github.com/miekg/dns 29 | version: 7f2bf8764aadba1e6d801bac04646ccfc52c0f84 30 | - name: github.com/pkg/errors 31 | version: 27936f6d90f9c8e1145f11ed52ffffbfdb9e0af7 32 | - name: github.com/spacemonkeygo/monotime 33 | version: e3f48a95f98a18c3a2388f616480ec98ff5ba8e3 34 | subpackages: 35 | - _cgo 36 | - name: go.uber.org/atomic 37 | version: df976f2515e274675050de7b3f42545de80594fd 38 | - name: go.uber.org/multierr 39 | version: 3c4937480c32f4c13a875a1829af76c98ca3d40a 40 | - name: go.uber.org/zap 41 | version: ff33455a0e382e8a81d14dd7c922020b6b5e7982 42 | subpackages: 43 | - buffer 44 | - internal/bufferpool 45 | - internal/color 46 | - internal/exit 47 | - zapcore 48 | - name: golang.org/x/crypto 49 | version: cc06ce4a13d484c0101a9e92913248488a75786d 50 | subpackages: 51 | - ed25519 52 | - ed25519/internal/edwards25519 53 | - name: golang.org/x/net 54 | version: 9f648a60d9775ef5c977e7669d1673a7a67bef33 55 | subpackages: 56 | - bpf 57 | - internal/iana 58 | - internal/socket 59 | - ipv4 60 | - ipv6 61 | - name: gopkg.in/validator.v2 62 | version: 135c24b11c19e52befcae2ec3fca5d9b78c4e98e 63 | - name: gopkg.in/yaml.v2 64 | version: 51d6538a90f86fe93ac480b35f37b2be17fef232 65 | testImports: 66 | - name: github.com/davecgh/go-spew 67 | version: d8f796af33cc11cb798c1aaeb27a4ebc5099927d 68 | subpackages: 69 | - spew 70 | - name: github.com/pmezard/go-difflib 71 | version: 5d4384ee4fb2527b0a1256a821ebfc92f91efefc 72 | subpackages: 73 | - difflib 74 | - name: github.com/stretchr/testify 75 | version: 34c6fa2dc70986bccbbffcc6130f6920a924b075 76 | subpackages: 77 | - assert 78 | - require 79 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/uber/arachne 2 | import: 3 | - package: github.com/DataDog/datadog-go 4 | subpackages: 5 | - statsd 6 | - package: github.com/fatih/color 7 | - package: github.com/google/gopacket 8 | subpackages: 9 | - layers 10 | - package: github.com/jawher/mow.cli 11 | - package: github.com/miekg/dns 12 | - package: github.com/pkg/errors 13 | - package: github.com/spacemonkeygo/monotime 14 | - package: go.uber.org/zap 15 | subpackages: 16 | - zapcore 17 | - package: golang.org/x/net 18 | subpackages: 19 | - bpf 20 | - package: gopkg.in/validator.v2 21 | - package: gopkg.in/yaml.v2 22 | testImport: 23 | - package: github.com/stretchr/testify 24 | subpackages: 25 | - assert 26 | - require 27 | -------------------------------------------------------------------------------- /internal/ip/ip.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package ip 22 | 23 | import ( 24 | "fmt" 25 | "net" 26 | "syscall" 27 | 28 | d "github.com/uber/arachne/defines" 29 | "github.com/uber/arachne/internal/log" 30 | 31 | "github.com/google/gopacket" 32 | "github.com/google/gopacket/layers" 33 | "github.com/pkg/errors" 34 | "go.uber.org/zap" 35 | "golang.org/x/net/bpf" 36 | ) 37 | 38 | // DSCPValue represents a QoS DSCP value. 39 | type DSCPValue uint8 40 | 41 | // QoS DSCP values mapped to TOS. 42 | const ( 43 | DSCPBeLow DSCPValue = 0 // 000000 BE 44 | DSCPBeHigh DSCPValue = 4 // 000001 BE 45 | DSCPBulkLow DSCPValue = 40 // 001010 AF11 46 | DSCPBulkHigh DSCPValue = 56 // 001110 AF13 47 | DSCPTier2Low DSCPValue = 72 // 010010 AF21 48 | DSCPTier2High DSCPValue = 88 // 010110 AF23 49 | DSCPTier1Low DSCPValue = 104 // 011010 AF31 50 | DSCPTier1High DSCPValue = 120 // 011110 AF33 51 | DSCPTier0Low DSCPValue = 160 // 101000 EF 52 | DSCPNc6 DSCPValue = 192 // 110000 CS6 53 | DSCPNc7 DSCPValue = 224 // 111000 CS7 54 | ) 55 | 56 | // GetDSCP holds all the DSCP values in a slice. 57 | var GetDSCP = DSCPSlice{ 58 | DSCPBeLow, 59 | DSCPBeHigh, 60 | DSCPBulkLow, 61 | DSCPBulkHigh, 62 | DSCPTier2Low, 63 | DSCPTier2High, 64 | DSCPTier1Low, 65 | DSCPTier1High, 66 | DSCPTier0Low, 67 | DSCPNc6, 68 | DSCPNc7, 69 | } 70 | 71 | // DSCPSlice represents a slice of DSCP values. 72 | type DSCPSlice []DSCPValue 73 | 74 | // Pos returns the index of the DSCP value in the DSCPSlice, not the actual DSCP value. 75 | func (slice DSCPSlice) Pos(value DSCPValue, logger *log.Logger) uint8 { 76 | 77 | for p, v := range slice { 78 | if v == value { 79 | return uint8(p) 80 | } 81 | } 82 | logger.Warn("QoS DSCP value not matching one of supported classes", 83 | zap.Any("DSCP_value", value), 84 | zap.String("supported_classes", fmt.Sprintf("%v", slice))) 85 | return 0 86 | } 87 | 88 | // Text provides the text description of the DSCPValue. 89 | func (q DSCPValue) Text(logger *log.Logger) string { 90 | switch q { 91 | case DSCPBeLow: 92 | return "BE low" 93 | case DSCPBeHigh: 94 | return "BE high" 95 | case DSCPBulkLow: 96 | return "AF11" 97 | case DSCPBulkHigh: 98 | return "AF113" 99 | case DSCPTier2Low: 100 | return "AF21" 101 | case DSCPTier2High: 102 | return "AF23" 103 | case DSCPTier1Low: 104 | return "AF31" 105 | case DSCPTier1High: 106 | return "AF33" 107 | case DSCPTier0Low: 108 | return "EF" 109 | case DSCPNc6: 110 | return "CS6" 111 | case DSCPNc7: 112 | return "CS7" 113 | default: 114 | logger.Error("unhandled QoS DSCP value", zap.Any("DSCP_value", q)) 115 | return "unknown" 116 | } 117 | } 118 | 119 | type recvSource struct { 120 | fd int 121 | } 122 | 123 | // Conn represents the underlying functionality to send and recv Arachne echo requests. 124 | type Conn struct { 125 | SrcAddr net.IP 126 | AF int 127 | sendFD int 128 | recvSrc recvSource 129 | ListenPort layers.TCPPort 130 | } 131 | 132 | // Recvfrom mirrors the syscall of the same name, operating on a recvSource file descriptor. 133 | func (r *recvSource) Recvfrom(b []byte) (int, syscall.Sockaddr, error) { 134 | return syscall.Recvfrom(r.fd, b, 0) 135 | } 136 | 137 | // Close is used to close a Conn's send file descriptor and recv source file desciptor. 138 | func (c *Conn) Close(logger *log.Logger) { 139 | if err := syscall.Close(c.recvSrc.fd); err != nil { 140 | logger.Error("error closing Conn recv file descriptor", zap.Error(err)) 141 | } 142 | if err := syscall.Close(c.sendFD); err != nil { 143 | logger.Error("error closing Conn send file descriptor", zap.Error(err)) 144 | } 145 | } 146 | 147 | // NextPacket gets bytes of next available packet, and returns them in a decoded gopacket.Packet 148 | func (c *Conn) NextPacket() (gopacket.Packet, error) { 149 | buf := make([]byte, d.MaxPacketSizeBytes) 150 | if _, _, err := c.recvSrc.Recvfrom(buf); err != nil { 151 | return nil, err 152 | } 153 | 154 | switch c.AF { 155 | case d.AfInet: 156 | return gopacket.NewPacket(buf, layers.LayerTypeIPv4, gopacket.DecodeOptions{Lazy: true}), nil 157 | case d.AfInet6: 158 | return gopacket.NewPacket(buf, layers.LayerTypeIPv6, gopacket.DecodeOptions{Lazy: true}), nil 159 | } 160 | 161 | return nil, errors.New("no valid decoder available for packet") 162 | 163 | } 164 | 165 | func ipToSockaddr(family int, ip net.IP, port int) (syscall.Sockaddr, error) { 166 | switch family { 167 | case syscall.AF_INET: 168 | if len(ip) == 0 { 169 | ip = net.IPv4zero 170 | } 171 | ip4 := ip.To4() 172 | if ip4 == nil { 173 | return nil, &net.AddrError{Err: "non-IPv4 address", Addr: ip.String()} 174 | } 175 | sa := &syscall.SockaddrInet4{Port: port} 176 | copy(sa.Addr[:], ip4) 177 | return sa, nil 178 | case syscall.AF_INET6: 179 | if len(ip) == 0 || ip.Equal(net.IPv4zero) { 180 | ip = net.IPv6zero 181 | } 182 | ip6 := ip.To16() 183 | if ip6 == nil { 184 | return nil, &net.AddrError{Err: "non-IPv6 address", Addr: ip.String()} 185 | } 186 | sa := &syscall.SockaddrInet6{Port: port} 187 | copy(sa.Addr[:], ip6) 188 | return sa, nil 189 | } 190 | return nil, &net.AddrError{Err: "unhandled AF family", Addr: ip.String()} 191 | } 192 | 193 | // SendTo operates on a Conn file descriptor and mirrors the Sendto syscall. 194 | func (c *Conn) SendTo(b []byte, to net.IP) error { 195 | sockAddr, err := ipToSockaddr(c.AF, to, 0) 196 | if err != nil { 197 | return err 198 | } 199 | 200 | return syscall.Sendto(c.sendFD, b, 0, sockAddr) 201 | } 202 | 203 | // getSendSocket will create a raw socket for sending data. 204 | func getSendSocket(af int) (int, error) { 205 | fd, err := syscall.Socket(af, syscall.SOCK_RAW, syscall.IPPROTO_RAW) 206 | if err != nil { 207 | return 0, err 208 | } 209 | 210 | if err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1); err != nil { 211 | return 0, err 212 | } 213 | 214 | return fd, nil 215 | } 216 | 217 | func getBPFFilter(ipHeaderOffset uint32, listenPort uint32) ([]bpf.RawInstruction, error) { 218 | // The Arachne BPF Filter reads values starting from the TCP Header by adding ipHeaderOffset to all 219 | // offsets. It filters for packets of destination port equal to listenPort, or src port equal to HTTP or HTTPS ports 220 | // and for packets containing a TCP SYN flag (SYN, or SYN+ACK packets) 221 | return bpf.Assemble([]bpf.Instruction{ 222 | bpf.LoadAbsolute{Off: ipHeaderOffset + 2, Size: 2}, // Starting from TCP Header, load DstPort (2nd word) 223 | bpf.JumpIf{Cond: bpf.JumpEqual, Val: listenPort, SkipTrue: 3}, // Return packet if DstPort is listen Port 224 | bpf.LoadAbsolute{Off: ipHeaderOffset, Size: 2}, // Starting from TCP Header, load SrcPort (1st word) 225 | bpf.JumpIf{Cond: bpf.JumpEqual, Val: d.PortHTTP, SkipTrue: 1}, // Return packet if SrcPort is HTTP Port 226 | bpf.JumpIf{Cond: bpf.JumpEqual, Val: d.PortHTTPS, SkipFalse: 2}, // Discard packet if not HTTPS 227 | bpf.LoadAbsolute{Off: ipHeaderOffset + 13, Size: 1}, // Starting from TCP Header, load Flags byte (not including NS bit) 228 | bpf.JumpIf{Cond: bpf.JumpBitsSet, Val: 2, SkipTrue: 1}, // AND Flags byte with 00000010 (SYN), and drop packet if 0 229 | bpf.RetConstant{Val: 0}, // Drop packet 230 | bpf.RetConstant{Val: 4096}, // Return up to 4096 bytes from packet 231 | }) 232 | } 233 | 234 | func getRecvSource(af int, listenPort layers.TCPPort, intf string, logger *log.Logger) (recvSource, error) { 235 | var ( 236 | rs recvSource 237 | ipHeaderOffset uint32 238 | ) 239 | 240 | fd, err := syscall.Socket(af, syscall.SOCK_RAW, syscall.IPPROTO_TCP) 241 | if err != nil { 242 | return rs, err 243 | } 244 | 245 | if err = bindToDevice(fd, intf); err != nil { 246 | return rs, err 247 | } 248 | 249 | rs.fd = fd 250 | 251 | switch af { 252 | case d.AfInet: 253 | ipHeaderOffset = d.IPv4HeaderLength 254 | case d.AfInet6: 255 | ipHeaderOffset = d.IPv6HeaderLength 256 | } 257 | 258 | filter, err := getBPFFilter(ipHeaderOffset, uint32(listenPort)) 259 | if err != nil { 260 | logger.Warn("Failed to compile BPF Filter", zap.Error(err)) 261 | return rs, nil 262 | } 263 | 264 | // Attempt to attach the BPF filter. 265 | // This is currently only supported on Linux systems. 266 | if err := rs.attachBPF(filter); err != nil { 267 | logger.Warn("Failed to attach BPF filter to recvSource. All incoming packets will be processed", 268 | zap.Error(err)) 269 | } 270 | 271 | return rs, nil 272 | } 273 | 274 | // NewConn returns a raw socket connection to send and receive packets. 275 | func NewConn(af int, listenPort layers.TCPPort, intf string, srcAddr net.IP, logger *log.Logger) *Conn { 276 | fdSend, err := getSendSocket(af) 277 | if err != nil { 278 | logger.Fatal("Error creating send socket", 279 | zap.Int("address_family", af), 280 | zap.Error(err)) 281 | } 282 | 283 | rs, err := getRecvSource(af, listenPort, intf, logger) 284 | if err != nil { 285 | logger.Fatal("Error creating recv source", 286 | zap.Any("listenPort", listenPort), 287 | zap.String("interface", intf), 288 | zap.Error(err)) 289 | } 290 | 291 | return &Conn{ 292 | SrcAddr: srcAddr, 293 | AF: af, 294 | sendFD: fdSend, 295 | recvSrc: rs, 296 | ListenPort: listenPort, 297 | } 298 | } 299 | 300 | func getIPHeaderLayerV6(tos DSCPValue, tcpLen uint16, srcIP net.IP, dstIP net.IP) *layers.IPv6 { 301 | return &layers.IPv6{ 302 | Version: 6, // IP Version 6 303 | TrafficClass: uint8(tos), 304 | Length: tcpLen, 305 | NextHeader: layers.IPProtocolTCP, 306 | SrcIP: srcIP, 307 | DstIP: dstIP, 308 | } 309 | } 310 | 311 | // GetIPHeaderLayer returns the appriately versioned gopacket IP layer 312 | func GetIPHeaderLayer(af int, tos DSCPValue, tcpLen uint16, srcIP net.IP, dstIP net.IP) (gopacket.NetworkLayer, error) { 313 | switch af { 314 | case d.AfInet: 315 | return getIPHeaderLayerV4(tos, tcpLen, srcIP, dstIP), nil 316 | case d.AfInet6: 317 | return getIPHeaderLayerV6(tos, tcpLen, srcIP, dstIP), nil 318 | } 319 | 320 | return nil, errors.New("unhandled AF family") 321 | } 322 | -------------------------------------------------------------------------------- /internal/ip/ip_bpf_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package ip 22 | 23 | import ( 24 | "errors" 25 | 26 | "golang.org/x/net/bpf" 27 | ) 28 | 29 | // attachBPF will attach an assembled BPF filter to the recvSource's raw socket file descriptor 30 | func (r *recvSource) attachBPF(filter []bpf.RawInstruction) error { 31 | return errors.New("BPF Filter currently not supported on Darwin") 32 | } 33 | -------------------------------------------------------------------------------- /internal/ip/ip_bpf_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package ip 22 | 23 | import ( 24 | "syscall" 25 | "unsafe" 26 | 27 | "golang.org/x/net/bpf" 28 | ) 29 | 30 | // attachBPF will attach an assembled BPF filter to the recvSource's raw socket file descriptor 31 | func (r *recvSource) attachBPF(filter []bpf.RawInstruction) error { 32 | prog := syscall.SockFprog{ 33 | Len: uint16(len(filter)), 34 | Filter: (*syscall.SockFilter)(unsafe.Pointer(&filter[0])), 35 | } 36 | _, _, err := syscall.Syscall6( 37 | syscall.SYS_SETSOCKOPT, 38 | uintptr(r.fd), 39 | uintptr(syscall.SOL_SOCKET), 40 | uintptr(syscall.SO_ATTACH_FILTER), 41 | uintptr(unsafe.Pointer(&prog)), 42 | uintptr(uint32(unsafe.Sizeof(prog))), 43 | 0, 44 | ) 45 | if err != 0 { 46 | return err 47 | } 48 | 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/ip/ip_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package ip 22 | 23 | import ( 24 | "net" 25 | 26 | "github.com/uber/arachne/defines" 27 | 28 | "github.com/google/gopacket" 29 | "github.com/google/gopacket/layers" 30 | ) 31 | 32 | func bindToDevice(s int, ifname string) error { 33 | return nil 34 | } 35 | 36 | // GetIPLayerOptions is used to get the gopacket serialization options. 37 | func GetIPLayerOptions() gopacket.SerializeOptions { 38 | return gopacket.SerializeOptions{ 39 | ComputeChecksums: true, 40 | // Gopacket does not yet support making lengths host-byte order for BSD-based Kernels 41 | FixLengths: false, 42 | } 43 | } 44 | 45 | func getIPHeaderLayerV4(tos DSCPValue, tcpLen uint16, srcIP net.IP, dstIP net.IP) *layers.IPv4 { 46 | header := &layers.IPv4{ 47 | Version: 4, // IP Version 4 48 | TOS: uint8(tos), 49 | IHL: 5, // IHL: 20 bytes 50 | Length: tcpLen + 20, // Total IP packet length 51 | FragOffset: 0, // No fragmentation 52 | Flags: 0, // Flags for fragmentation 53 | TTL: defines.IPTTL, 54 | Protocol: layers.IPProtocolTCP, 55 | Checksum: 0, // Computed at serialization time 56 | SrcIP: srcIP, 57 | DstIP: dstIP, 58 | } 59 | 60 | // Manually convert Length and FragOffset to host-byte order for Darwin 61 | header.Length = (header.Length << 8) | (header.Length >> 8) 62 | nf := layers.IPv4Flag(header.FragOffset & 0xE0) 63 | header.FragOffset = (header.FragOffset & 0x1F << 8) | (header.FragOffset>>8 | uint16(header.Flags)) 64 | header.Flags = nf 65 | 66 | return header 67 | } 68 | -------------------------------------------------------------------------------- /internal/ip/ip_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package ip 22 | 23 | import ( 24 | "net" 25 | "syscall" 26 | 27 | "github.com/uber/arachne/defines" 28 | 29 | "github.com/google/gopacket" 30 | "github.com/google/gopacket/layers" 31 | ) 32 | 33 | func bindToDevice(s int, ifname string) error { 34 | return syscall.BindToDevice(s, ifname) 35 | } 36 | 37 | // GetIPLayerOptions returns the gopacket options for serialization specific to Linux. 38 | // In linux, gopacket correctly computes the ip Header lengths and checksum. 39 | func GetIPLayerOptions() gopacket.SerializeOptions { 40 | return gopacket.SerializeOptions{ 41 | ComputeChecksums: true, 42 | FixLengths: true, 43 | } 44 | } 45 | 46 | func getIPHeaderLayerV4(tos DSCPValue, tcpLen uint16, srcIP net.IP, dstIP net.IP) *layers.IPv4 { 47 | return &layers.IPv4{ 48 | Version: 4, // IP Version 4 49 | TOS: uint8(tos), 50 | Protocol: layers.IPProtocolTCP, 51 | TTL: defines.IPTTL, 52 | SrcIP: srcIP, 53 | DstIP: dstIP, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /internal/log/log.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package log 22 | 23 | import ( 24 | "io/ioutil" 25 | "os" 26 | 27 | "github.com/pkg/errors" 28 | "go.uber.org/zap" 29 | "go.uber.org/zap/zapcore" 30 | ) 31 | 32 | type removePIDfunc func(fname string, logger *Logger) 33 | 34 | // Logger embeds a zap.Logger instance, and extends its Fatal 35 | // and Panic methods to first remove the pid file. 36 | type Logger struct { 37 | *zap.Logger 38 | PIDPath string 39 | RemovePID removePIDfunc 40 | } 41 | 42 | // Fatal extends the zap Fatal to also remove the PID file. 43 | func (log *Logger) Fatal(msg string, fields ...zapcore.Field) { 44 | log.RemovePID(log.PIDPath, log) 45 | log.Logger.Fatal(msg, fields...) 46 | } 47 | 48 | // Panic extends the zap Panic to also remove the PID file. 49 | func (log *Logger) Panic(msg string, fields ...zapcore.Field) { 50 | log.RemovePID(log.PIDPath, log) 51 | log.Logger.Panic(msg, fields...) 52 | } 53 | 54 | // Config contains configuration for logging. 55 | type Config struct { 56 | Level string `yaml:"level"` 57 | StdOut bool `yaml:"stdout"` 58 | LogSink string `yaml:"logSink"` 59 | } 60 | 61 | // CreateLogger creates a zap logger. 62 | func CreateLogger(c *zap.Config, pidPath string, removePIDfunc removePIDfunc) (*Logger, error) { 63 | 64 | l, err := c.Build() 65 | if err != nil { 66 | return nil, errors.Wrap(err, "failed to create logger") 67 | } 68 | pl := Logger{ 69 | Logger: l, 70 | PIDPath: pidPath, 71 | RemovePID: removePIDfunc, 72 | } 73 | 74 | return &pl, nil 75 | } 76 | 77 | // ResetLogFiles keeps the last 'LogFileSizeKeepKB' KB of the log file if the size of the log file 78 | // has exceeded 'LogFileSizeMaxMB' MB within the last 'PollOrchestratorIntervalSuccess' hours. 79 | func ResetLogFiles(paths []string, fileSizeMaxMB int, fileSizeKeepKB int, logger *Logger) { 80 | 81 | var fileSize int 82 | 83 | for _, path := range paths { 84 | switch path { 85 | case "stdout": 86 | fallthrough 87 | case "stderr": 88 | continue 89 | } 90 | file, err := os.Open(path) 91 | if err != nil { 92 | logger.Error("failed to open existing log file", 93 | zap.String("file", path), 94 | zap.Error(err)) 95 | continue 96 | } 97 | 98 | // Get the file size 99 | stat, err := file.Stat() 100 | if err != nil { 101 | logger.Error("failed to read the FileInfo structure of the log file", 102 | zap.String("file", path), 103 | zap.Error(err)) 104 | goto close 105 | } 106 | 107 | fileSize = int(stat.Size()) 108 | if fileSize > fileSizeMaxMB*1024*1024 { 109 | logger.Debug("Size of log file is larger than maximum allowed. Resetting.", 110 | zap.String("file", path), 111 | zap.Int("current_size_MB", fileSize), 112 | zap.Int("maximum_allowed_size_MB", fileSizeMaxMB)) 113 | 114 | buf := make([]byte, fileSizeKeepKB*1024) 115 | start := stat.Size() - int64(fileSizeKeepKB*1024) 116 | if _, err = file.ReadAt(buf, start); err != nil { 117 | logger.Error("failed to read existing log file", 118 | zap.String("file", path), 119 | zap.Error(err)) 120 | } else if err = ioutil.WriteFile(path, buf, 0644); err != nil { 121 | logger.Error("failed to reset log file", 122 | zap.String("file", path), 123 | zap.Error(err)) 124 | } 125 | } 126 | 127 | // Avoid possible leaks because of using `defer` 128 | close: 129 | file.Close() 130 | 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /internal/network/network.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package network 22 | 23 | import ( 24 | "net" 25 | "strings" 26 | 27 | "github.com/uber/arachne/internal/log" 28 | 29 | "github.com/miekg/dns" 30 | "github.com/pkg/errors" 31 | "go.uber.org/zap" 32 | ) 33 | 34 | // Family returns the string equivalent of the address family provided. 35 | func Family(a *net.IP) string { 36 | if a == nil || len(*a) <= net.IPv4len { 37 | return "ip4" 38 | } 39 | if a.To4() != nil { 40 | return "ip4" 41 | } 42 | return "ip6" 43 | } 44 | 45 | // GetSourceAddr discovers the source address. 46 | func GetSourceAddr( 47 | af string, 48 | srcAddr string, 49 | hostname string, 50 | ifaceName string, 51 | logger *log.Logger, 52 | ) (*net.IP, error) { 53 | 54 | //TODO => resolve if both interface name and source address are specified and they do not match 55 | 56 | // Source address is specified 57 | if srcAddr != "" { 58 | return resolveHost(af, hostname, logger) 59 | } 60 | // Interface name is specified 61 | if ifaceName != "" { 62 | return interfaceAddress(af, ifaceName) 63 | } 64 | 65 | return anyInterfaceAddress(af) 66 | } 67 | 68 | // Resolve given domain hostname/address in the given address family. 69 | //TODO replace with net.LookupHost? 70 | func resolveHost(af string, hostname string, logger *log.Logger) (*net.IP, error) { 71 | addr, err := net.ResolveIPAddr(af, hostname) 72 | if err != nil { 73 | logger.Warn("failed to DNS resolve hostname with default server", 74 | zap.String("hostname", hostname), 75 | zap.Error(err)) 76 | return nil, err 77 | } 78 | 79 | return &addr.IP, nil 80 | } 81 | 82 | // ResolveIP returns DNS name of given IP address. Returns the same input string, if resolution fails. 83 | func ResolveIP(ip string, servers []net.IP, logger *log.Logger) (string, error) { 84 | 85 | names, err := net.LookupAddr(ip) 86 | if err != nil || len(names) == 0 { 87 | logger.Warn("failed to DNS resolve IP with default server", 88 | zap.String("ip", ip), 89 | zap.Error(err)) 90 | return resolveIPwServer(ip, servers, logger) 91 | } 92 | 93 | return names[0], nil 94 | } 95 | 96 | func resolveIPwServer(ip string, servers []net.IP, logger *log.Logger) (string, error) { 97 | 98 | if servers == nil { 99 | return "", errors.New("no alternate DNS servers configured") 100 | } 101 | 102 | c := dns.Client{} 103 | m := dns.Msg{} 104 | 105 | fqdn, err := dns.ReverseAddr(ip) 106 | if err != nil { 107 | return "", err 108 | } 109 | m.SetQuestion(fqdn, dns.TypePTR) 110 | for _, s := range servers { 111 | r, t, err := c.Exchange(&m, s.String()+":53") 112 | if err != nil || len(r.Answer) == 0 { 113 | continue 114 | } 115 | logger.Debug("Reverse DNS resolution for ip with user-configured DNS server took", 116 | zap.String("ip", ip), 117 | zap.Float64("duration", t.Seconds())) 118 | 119 | resolved := strings.Split(r.Answer[0].String(), "\t") 120 | 121 | // return fourth tab-delimited field of DNS query response 122 | return resolved[4], nil 123 | } 124 | 125 | logger.Warn("failed to DNS resolve IP with alternate servers", zap.String("ip", ip)) 126 | return "", errors.Errorf("failed to DNS resolve %s with alternate servers", ip) 127 | } 128 | 129 | func interfaceAddress(af string, name string) (*net.IP, error) { 130 | iface, err := net.InterfaceByName(name) 131 | if err != nil { 132 | return nil, errors.Wrapf(err, "net.InterfaceByName for %s", name) 133 | } 134 | 135 | addrs, err := iface.Addrs() 136 | if err != nil { 137 | return nil, errors.Wrap(err, "iface.Addrs") 138 | } 139 | 140 | return findAddrInRange(af, addrs) 141 | } 142 | 143 | func anyInterfaceAddress(af string) (*net.IP, error) { 144 | interfaces, err := net.Interfaces() 145 | if err != nil { 146 | return nil, errors.Wrap(err, "net.Interfaces") 147 | } 148 | 149 | for _, iface := range interfaces { 150 | // Skip loopback 151 | if (iface.Flags & net.FlagLoopback) == net.FlagLoopback { 152 | continue 153 | } 154 | addrs, err := iface.Addrs() 155 | // Skip if error getting addresses 156 | if err != nil { 157 | return nil, errors.Wrapf(err, "error getting addresses for interface %s", iface.Name) 158 | } 159 | 160 | if len(addrs) > 0 { 161 | return interfaceAddress(af, iface.Name) 162 | } 163 | } 164 | 165 | return nil, err 166 | } 167 | 168 | func findAddrInRange(af string, addrs []net.Addr) (*net.IP, error) { 169 | for _, a := range addrs { 170 | 171 | ipnet, ok := a.(*net.IPNet) 172 | if ok && !(ipnet.IP.IsLoopback() || ipnet.IP.IsMulticast() || ipnet.IP.IsLinkLocalUnicast()) { 173 | if (ipnet.IP.To4() != nil && af == "ip4") || (ipnet.IP.To4() == nil && af == "ip6") { 174 | return &ipnet.IP, nil 175 | } 176 | } 177 | } 178 | return nil, errors.Errorf("could not find a source address in %s address family", af) 179 | } 180 | -------------------------------------------------------------------------------- /internal/network/network_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package network 22 | 23 | import ( 24 | "net" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func TestFindAddrInRange(t *testing.T) { 31 | ip, err := findAddrInRange("", []net.Addr{}) 32 | assert.Nil(t, ip) 33 | assert.Error(t, err) 34 | } 35 | -------------------------------------------------------------------------------- /internal/tcp/tcp.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package tcp 22 | 23 | import ( 24 | "math/rand" 25 | "net" 26 | "reflect" 27 | "sync" 28 | "time" 29 | 30 | "github.com/uber/arachne/defines" 31 | "github.com/uber/arachne/internal/ip" 32 | "github.com/uber/arachne/internal/log" 33 | 34 | "github.com/google/gopacket" 35 | "github.com/google/gopacket/layers" 36 | "github.com/pkg/errors" 37 | "github.com/spacemonkeygo/monotime" 38 | "go.uber.org/zap" 39 | "go.uber.org/zap/zapcore" 40 | ) 41 | 42 | type tcpFlags struct { 43 | fin, syn, rst, psh, ack, urg, ece, cwr, ns bool 44 | } 45 | 46 | type echoType uint8 47 | 48 | // PortRange is the inclusive range of src ports. 49 | type PortRange [2]layers.TCPPort 50 | 51 | // Contains returns true if p is included within the PortRange t. 52 | func (t PortRange) Contains(p layers.TCPPort) bool { 53 | return p >= t[0] && p <= t[1] 54 | } 55 | 56 | //go:generatestringer­type=EchoType 57 | 58 | // 'Echo' Types 59 | const ( 60 | EchoRequest echoType = iota + 1 61 | EchoReply 62 | ) 63 | 64 | func (q echoType) text(logger *log.Logger) string { 65 | switch q { 66 | case EchoRequest: 67 | return "Echo Request" 68 | case EchoReply: 69 | return "Echo Reply" 70 | default: 71 | logger.Fatal("unhandled Echo type family", zap.Any("echo_type", q)) 72 | } 73 | return "" // unreachable 74 | } 75 | 76 | // Message is filled with the info about the 'echo' request sent or 'echo' reply received and 77 | // emitted onto the 'sent' and 'rcvd' channels, respectively, for further processing by the collector. 78 | type Message struct { 79 | Type echoType 80 | SrcAddr net.IP 81 | DstAddr net.IP 82 | Af int 83 | SrcPort layers.TCPPort 84 | DstPort layers.TCPPort 85 | QosDSCP ip.DSCPValue 86 | Ts Timestamp 87 | Seq uint32 88 | Ack uint32 89 | } 90 | 91 | // FromExternalTarget returns true if message has been received from external server and not an arachne agent. 92 | func (m Message) FromExternalTarget(servicePort layers.TCPPort) bool { 93 | return m.DstPort != servicePort 94 | } 95 | 96 | // Timestamp holds all the different types of time stamps. 97 | type Timestamp struct { 98 | Unix time.Time 99 | Run time.Time 100 | Payload time.Time 101 | } 102 | 103 | var ( 104 | monoNow = monotime.Now 105 | timeNow = time.Now 106 | ) 107 | 108 | // parsePktTCP extracts the TCP header layer and payload from an incoming packet. 109 | func parsePktTCP(pkt gopacket.Packet) (layers.TCP, time.Time, error) { 110 | layer := pkt.Layer(layers.LayerTypeTCP) 111 | if layer == nil { 112 | return layers.TCP{}, time.Time{}, errors.New("invalid TCP layer") 113 | } 114 | tcpSegment := layer.(*layers.TCP) 115 | 116 | var payload time.Time 117 | if len(tcpSegment.Payload) >= defines.TimestampPayloadLengthBytes { 118 | ts := append([]byte(nil), tcpSegment.Payload[:defines.TimestampPayloadLengthBytes]...) 119 | if err := payload.UnmarshalBinary(ts); err != nil { 120 | return *tcpSegment, time.Time{}, err 121 | } 122 | } 123 | 124 | return *tcpSegment, payload, nil 125 | } 126 | 127 | // parsePktIP parses the IP header of an incoming packet and extracts the src IP addr and DSCP value. 128 | func parsePktIP(pkt gopacket.Packet) (net.IP, ip.DSCPValue, error) { 129 | switch pkt.NetworkLayer().LayerType() { 130 | case layers.LayerTypeIPv4: 131 | layer := pkt.Layer(layers.LayerTypeIPv4).(*layers.IPv4) 132 | if layer == nil { 133 | return net.IPv4zero, ip.DSCPValue(0), errors.New("layer type IPv4 invalid") 134 | } 135 | return layer.SrcIP, ip.DSCPValue(layer.TOS), nil 136 | case layers.LayerTypeIPv6: 137 | layer := pkt.Layer(layers.LayerTypeIPv6).(*layers.IPv6) 138 | if layer == nil { 139 | return net.IPv6zero, ip.DSCPValue(0), errors.New("layer type IPv6 invalid") 140 | } 141 | return layer.SrcIP, ip.DSCPValue(layer.TrafficClass), nil 142 | } 143 | 144 | return net.IPv4zero, ip.DSCPValue(0), errors.New("unknown network layer type") 145 | } 146 | 147 | // optsTCP contains the gopacket serialization options for the TCP layer 148 | var optsTCP = gopacket.SerializeOptions{ 149 | ComputeChecksums: true, 150 | FixLengths: true, 151 | } 152 | 153 | // makePkt creates and serializes a TCP Echo. 154 | func makePkt( 155 | af int, 156 | srcAddr net.IP, 157 | dstAddr net.IP, 158 | srcPort layers.TCPPort, 159 | dstPort layers.TCPPort, 160 | dscpv ip.DSCPValue, 161 | flags tcpFlags, 162 | seqNum uint32, 163 | ackNum uint32, 164 | ) ([]byte, error) { 165 | buf := gopacket.NewSerializeBuffer() 166 | 167 | // gopacket serialization options for IP header are OS specific 168 | optsIP := ip.GetIPLayerOptions() 169 | 170 | // When replying with SYN+ACK, a time-stamped payload is included 171 | if flags.syn != false && flags.ack != false { 172 | payloadTime, err := timeNow().MarshalBinary() 173 | if err != nil { 174 | return nil, err 175 | } 176 | payloadLayer := gopacket.Payload(payloadTime) 177 | payloadLayer.SerializeTo(buf, optsTCP) 178 | } 179 | 180 | tcpLayer := &layers.TCP{ 181 | SrcPort: srcPort, 182 | DstPort: dstPort, 183 | Seq: seqNum, 184 | Ack: ackNum, 185 | DataOffset: uint8(defines.TCPHeaderLength / 4), // TCP Header size in 32-bit words 186 | SYN: flags.syn, 187 | RST: flags.rst, 188 | ACK: flags.ack, 189 | Window: defines.TCPWindowSize, 190 | Checksum: 0, // computed upon serialization 191 | } 192 | 193 | // Length of TCP portion is payload length + fixed 20 bytes for Header 194 | tcpLen := defines.TCPHeaderLength + len(buf.Bytes()) 195 | 196 | ipLayer, err := ip.GetIPHeaderLayer(af, dscpv, uint16(tcpLen), srcAddr, dstAddr) 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | tcpLayer.SetNetworkLayerForChecksum(ipLayer) 202 | 203 | if err = tcpLayer.SerializeTo(buf, optsTCP); err != nil { 204 | return nil, err 205 | } 206 | 207 | switch layer := ipLayer.(type) { 208 | case *layers.IPv4: 209 | err = layer.SerializeTo(buf, optsIP) 210 | case *layers.IPv6: 211 | err = layer.SerializeTo(buf, optsIP) 212 | } 213 | if err != nil { 214 | return nil, err 215 | } 216 | 217 | return buf.Bytes(), nil 218 | } 219 | 220 | // Receiver checks if the incoming packet is actually a response to our probe and acts accordingly. 221 | //TODO Test IPv6 222 | func Receiver( 223 | conn *ip.Conn, 224 | sentC chan Message, 225 | rcvdC chan Message, 226 | kill chan struct{}, 227 | logger *log.Logger, 228 | ) error { 229 | var receiveTime time.Time 230 | 231 | logger.Info("TCP receiver starting...", zap.Int("AF", conn.AF)) 232 | 233 | // IP + TCP header, this channel is fed from the socket 234 | in := make(chan Message, defines.ChannelInBufferSize) 235 | 236 | go func() { 237 | defer close(in) 238 | 239 | for { 240 | pkt, err := conn.NextPacket() 241 | // parent has closed the socket likely 242 | if err != nil { 243 | logger.Fatal("failed to receive packet from packet source", 244 | zap.Error(err)) 245 | } 246 | receiveTime = monoNow() 247 | 248 | srcIP, DSCPv, err := parsePktIP(pkt) 249 | if err != nil { 250 | logger.Error("error parsing packet IP layer", zap.Error(err), zap.Any("packet", pkt)) 251 | continue 252 | } 253 | 254 | tcpHeader, payloadTime, err := parsePktTCP(pkt) 255 | if err != nil { 256 | logger.Error("error parsing packet TCP layer", zap.Error(err), zap.Any("packet", pkt)) 257 | continue 258 | } 259 | 260 | switch { 261 | case tcpHeader.SYN && !tcpHeader.ACK: 262 | // Received SYN (Open port) 263 | logger.Debug("Received", 264 | zap.String("flag", "SYN"), 265 | zap.Stringer("src_address", srcIP), 266 | zap.Any("src_port", tcpHeader.SrcPort)) 267 | 268 | // Replying with SYN+ACK to Arachne agent 269 | srcPortRange := PortRange{tcpHeader.SrcPort, tcpHeader.SrcPort} 270 | seqNum := rand.Uint32() 271 | ackNum := tcpHeader.Seq + 1 272 | flags := tcpFlags{syn: true, ack: true} 273 | // Replies are sent to the same port as the one this agent is listening on 274 | if err := send(conn, &srcIP, conn.ListenPort, srcPortRange, DSCPv, 275 | flags, seqNum, ackNum, sentC, kill, logger); err != nil { 276 | logger.Error("failed to send SYN-ACK", zap.Error(err)) 277 | } 278 | 279 | case tcpHeader.SYN && tcpHeader.ACK: 280 | // Received SYN+ACK (Open port) 281 | logger.Debug("Received", 282 | zap.String("flag", "SYN ACK"), 283 | zap.Stringer("src_address", srcIP), 284 | zap.Any("src_port", tcpHeader.SrcPort)) 285 | 286 | inMsg := Message{ 287 | Type: EchoReply, 288 | SrcAddr: srcIP, 289 | DstAddr: conn.SrcAddr, 290 | Af: conn.AF, 291 | SrcPort: tcpHeader.SrcPort, 292 | DstPort: tcpHeader.DstPort, 293 | QosDSCP: DSCPv, 294 | Ts: Timestamp{ 295 | Run: receiveTime, 296 | Payload: payloadTime, 297 | }, 298 | Seq: tcpHeader.Seq, 299 | Ack: tcpHeader.Ack, 300 | } 301 | // Send 'echo' reply message received to collector 302 | in <- inMsg 303 | 304 | if inMsg.FromExternalTarget(conn.ListenPort) { 305 | //TODO verify 306 | // Replying with RST only to external target 307 | srcPortRange := PortRange{tcpHeader.SrcPort, tcpHeader.SrcPort} 308 | seqNum := tcpHeader.Ack 309 | ackNum := tcpHeader.Seq + 1 310 | flags := tcpFlags{rst: true} 311 | err = send(conn, &srcIP, defines.PortHTTPS, srcPortRange, 312 | ip.DSCPBeLow, flags, seqNum, ackNum, sentC, kill, logger) 313 | if err != nil { 314 | logger.Error("failed to send RST", zap.Error(err)) 315 | } 316 | } 317 | } 318 | 319 | select { 320 | case <-kill: 321 | logger.Info("TCP receiver terminating...", zap.Int("AF", conn.AF)) 322 | return 323 | default: 324 | continue 325 | } 326 | } 327 | }() 328 | 329 | go func() { 330 | for { 331 | select { 332 | case reply := <-in: 333 | rcvdC <- reply 334 | case <-kill: 335 | logger.Info("'rcvdC' channel goroutine returning.") 336 | return 337 | } 338 | } 339 | }() 340 | 341 | return nil 342 | } 343 | 344 | // EchoTargets sends echoes (SYNs) to all targets included in 'remotes.' 345 | func EchoTargets( 346 | remotes interface{}, 347 | conn *ip.Conn, 348 | targetPort layers.TCPPort, 349 | srcPortRange PortRange, 350 | QoSEnabled bool, 351 | currentDSCP *ip.DSCPValue, 352 | realBatchInterval time.Duration, 353 | batchEndCycle *time.Ticker, 354 | sentC chan Message, 355 | senderOnlyMode bool, 356 | completeCycleUpload chan bool, 357 | finishedCycleUpload *sync.WaitGroup, 358 | kill chan struct{}, 359 | logger *log.Logger, 360 | ) { 361 | go func() { 362 | for { 363 | for i := range ip.GetDSCP { 364 | t0 := time.Now() 365 | if !QoSEnabled { 366 | *currentDSCP = ip.GetDSCP[0] 367 | } else { 368 | *currentDSCP = ip.GetDSCP[i] 369 | } 370 | echoTargetsWorker(remotes, conn, targetPort, srcPortRange, *currentDSCP, 371 | realBatchInterval, batchEndCycle, sentC, kill, logger) 372 | select { 373 | case <-kill: 374 | //Stop the batch cycle Ticker. 375 | batchEndCycle.Stop() 376 | return 377 | case <-batchEndCycle.C: 378 | if !(senderOnlyMode) { 379 | finishedCycleUpload.Add(1) 380 | // Request from Collector to complete all stats uploads for this 381 | // batch cycle 382 | completeCycleUpload <- true 383 | // Wait till the above request is fulfilled 384 | finishedCycleUpload.Wait() 385 | t1 := time.Now() 386 | logger.Debug("Completed echoing and uploading all "+ 387 | "stats of current batch cycle", 388 | zap.String("duration", t1.Sub(t0).String())) 389 | continue 390 | } 391 | t1 := time.Now() 392 | logger.Debug("Completed echoing current batch cycle", 393 | zap.String("duration", t1.Sub(t0).String())) 394 | continue 395 | } 396 | } 397 | } 398 | }() 399 | } 400 | 401 | func echoTargetsWorker( 402 | remotes interface{}, 403 | conn *ip.Conn, 404 | targetPort layers.TCPPort, 405 | srcPortRange PortRange, 406 | DSCPv ip.DSCPValue, 407 | realBatchInterval time.Duration, 408 | batchEndCycle *time.Ticker, 409 | sentC chan Message, 410 | kill chan struct{}, 411 | logger *log.Logger, 412 | ) error { 413 | 414 | r := reflect.ValueOf(remotes) 415 | 416 | if r.Kind() != reflect.Map { 417 | return errors.New("remote interface not a map in echoTargetsWorker()") 418 | } 419 | 420 | // Echo interval is half the time of the 'real' batch interval 421 | echoInterval := time.Duration(int(realBatchInterval) / 2 / len(r.MapKeys())) 422 | tickCh := time.NewTicker(echoInterval) 423 | defer tickCh.Stop() 424 | 425 | for _, key := range r.MapKeys() { 426 | remoteStruct := r.MapIndex(key) 427 | if remoteStruct.Kind() != reflect.Struct { 428 | return errors.New("remote field not a struct in tcp.EchoTargets()") 429 | } 430 | dstAddr := net.IP(remoteStruct.FieldByName("IP").Bytes()) 431 | ext := remoteStruct.FieldByName("External").Bool() 432 | 433 | // Send SYN with random SEQ 434 | flags := tcpFlags{syn: true} 435 | port := targetPort 436 | qos := DSCPv 437 | if ext { 438 | port = defines.PortHTTPS 439 | qos = ip.DSCPBeLow 440 | } 441 | if err := send(conn, &dstAddr, port, srcPortRange, qos, 442 | flags, rand.Uint32(), 0, sentC, kill, logger); err != nil { 443 | return err 444 | } 445 | 446 | select { 447 | case <-tickCh.C: 448 | continue 449 | case <-batchEndCycle.C: 450 | return nil 451 | } 452 | } 453 | return nil 454 | } 455 | 456 | // Sender generates TCP packet probes with given TTL at given packet per second rate. 457 | // The packet are injected into raw socket and their descriptions are published to the output channel as Probe messages. 458 | //TODO Test IPv6 459 | func send( 460 | conn *ip.Conn, 461 | dstAddr *net.IP, 462 | targetPort layers.TCPPort, 463 | srcPortRange PortRange, 464 | DSCPv ip.DSCPValue, 465 | ctrlFlags tcpFlags, 466 | seqNum uint32, 467 | ackNum uint32, 468 | sentC chan Message, 469 | kill chan struct{}, 470 | logger *log.Logger, 471 | ) error { 472 | var flag string 473 | 474 | switch { 475 | case (ctrlFlags.syn != false) && (ctrlFlags.ack == false): 476 | flag = "SYN" 477 | case ctrlFlags.syn != false && (ctrlFlags.ack != false): 478 | flag = "SYN ACK" 479 | case ctrlFlags.rst != false: 480 | flag = "RST" 481 | default: 482 | flag = "" 483 | } 484 | 485 | go func() { 486 | rand.Seed(time.Now().UnixNano()) 487 | for srcPort := srcPortRange[0]; srcPort <= srcPortRange[1]; srcPort++ { 488 | 489 | zf := []zapcore.Field{ 490 | zap.String("flag", flag), 491 | zap.String("src_address", conn.SrcAddr.String()), 492 | zap.Any("src_port", srcPort), 493 | zap.String("dst_address", dstAddr.String()), 494 | zap.Any("dst_port", targetPort)} 495 | 496 | packet, err := makePkt(conn.AF, conn.SrcAddr, *dstAddr, srcPort, targetPort, DSCPv, ctrlFlags, seqNum, ackNum) 497 | if err != nil { 498 | logger.Error("error creating packet", zap.Error(err)) 499 | goto cont 500 | } 501 | 502 | if err = conn.SendTo(packet, *dstAddr); err == nil { 503 | logger.Debug("Sent", zf...) 504 | if flag == "SYN" { 505 | // Send 'echo' request message to collector 506 | sentC <- Message{ 507 | Type: EchoRequest, 508 | SrcAddr: conn.SrcAddr, 509 | DstAddr: *dstAddr, 510 | Af: conn.AF, 511 | SrcPort: srcPort, 512 | QosDSCP: DSCPv, 513 | Ts: Timestamp{ 514 | Run: monoNow(), 515 | Unix: timeNow()}, 516 | Seq: seqNum, 517 | Ack: ackNum, 518 | } 519 | } 520 | } else { 521 | logger.Error("failed to send out", zf...) 522 | } 523 | 524 | cont: 525 | select { 526 | case <-kill: 527 | logger.Info("Sender requested to exit prematurely.", 528 | zap.String("destination", dstAddr.String())) 529 | return 530 | default: 531 | continue 532 | } 533 | } 534 | }() 535 | 536 | return nil 537 | } 538 | -------------------------------------------------------------------------------- /internal/tcp/tcp_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package tcp 22 | 23 | func bindToDevice(s int, ifname string) error { 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /internal/tcp/tcp_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package tcp 22 | 23 | import "syscall" 24 | 25 | func bindToDevice(s int, ifname string) error { 26 | return syscall.BindToDevice(s, ifname) 27 | } 28 | -------------------------------------------------------------------------------- /internal/tcp/tcp_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package tcp 22 | 23 | import ( 24 | "encoding/hex" 25 | "net" 26 | "runtime" 27 | "testing" 28 | "time" 29 | 30 | "github.com/uber/arachne/defines" 31 | "github.com/uber/arachne/internal/ip" 32 | 33 | "github.com/google/gopacket" 34 | "github.com/google/gopacket/layers" 35 | "github.com/stretchr/testify/assert" 36 | "github.com/stretchr/testify/require" 37 | ) 38 | 39 | type testPkt struct { 40 | hexData string 41 | name string 42 | } 43 | 44 | func TestParsePktTCP(t *testing.T) { 45 | assert := assert.New(t) 46 | 47 | pkt := testPkt{ 48 | hexData: "4500003791cc4000380651910a01010a0a00000a7918ac4f768dae70fbf6c7115012aaaa9aed0000010000000ed0de286824a952740000", 49 | name: "TCP test packet (ipv4), TCP Src Port: 31000, TCP Dst Port 44111, payload TimeStamp: 2017-06-22T21:06:48.615076468+00:00", 50 | } 51 | data, _ := hex.DecodeString(pkt.hexData) 52 | 53 | createdPkt := gopacket.NewPacket(data, layers.LayerTypeIPv4, gopacket.Default) 54 | require.NotNil(t, createdPkt, "creating packet %s failed", pkt.name) 55 | 56 | tcpHeader, payload, err := parsePktTCP(createdPkt) 57 | require.NoError(t, err, "decoding TCP header from packet %s failed: %v", pkt.name, err) 58 | 59 | assert.Equal(tcpHeader.SrcPort, layers.TCPPort(31000), "unexpectedly formatted TCP Src Port") 60 | assert.Equal(tcpHeader.DstPort, layers.TCPPort(44111), "unexpectedly formatted TCP Dst Port") 61 | 62 | expectedTime, err := time.Parse(time.RFC3339, "2017-06-22T21:06:48.615076468+00:00") 63 | require.NoError(t, err, "parsing timestamp from TCP packet %s failed: %v", pkt.name, err) 64 | 65 | assert.Equal(payload, expectedTime, "unexpectedly formatted TCP payload timestamp") 66 | } 67 | 68 | func TestParsePktIP(t *testing.T) { 69 | assert := assert.New(t) 70 | 71 | pkt := testPkt{ 72 | hexData: "4558003791cc4000380651910a01010a0a00000a7918ac4f768dae70fbf6c7115012aaaa9aed0000010000000ed0de286824a952740000", 73 | name: "IPv4 test packet, Src IP 10.1.1.10, DSCP value 88", 74 | } 75 | 76 | data, _ := hex.DecodeString(pkt.hexData) 77 | createdPkt := gopacket.NewPacket(data, layers.LayerTypeIPv4, gopacket.Default) 78 | require.NotNil(t, createdPkt, "creating packet %s failed", pkt.name) 79 | 80 | srcIP, dscpv, err := parsePktIP(createdPkt) 81 | require.NoError(t, err, "decoded IP Header from packet %s failed: %v", pkt.name, err) 82 | 83 | expectedIP := net.ParseIP("10.1.1.10") 84 | assert.Equal(expectedIP.Equal(srcIP), true, "unexpectedly formatted Src IP address") 85 | assert.Equal(dscpv, ip.DSCPValue(88), "unexpectedly formatted IP Header DSCP value") 86 | } 87 | 88 | func TestMakePkt(t *testing.T) { 89 | assert := assert.New(t) 90 | 91 | var ( 92 | af int 93 | srcAddr net.IP 94 | dstAddr net.IP 95 | srcPort layers.TCPPort 96 | dstPort layers.TCPPort 97 | expectedPkt testPkt 98 | want []byte 99 | err error 100 | ) 101 | 102 | // IPv4 103 | af = defines.AfInet 104 | srcAddr = net.IPv4(10, 0, 0, 1) 105 | dstAddr = net.IPv4(20, 0, 0, 1) 106 | srcPort = layers.TCPPort(31100) 107 | dstPort = layers.TCPPort(44111) 108 | // Darwin uses Host-byte order for Length and FragOffset in IPv4 Headers 109 | switch runtime.GOOS { 110 | case "linux": 111 | expectedPkt = testPkt{ 112 | hexData: "456400280000000040065c6b0a00000114000001797cac4f00000000000000005002aaaac16a0000", 113 | name: "Linux IPv4/TCP test Packet, SrcIP: 10.0.0.1, DstIP: 20.0.0.1, SrcPort 31100, DstPort: 44111, Flags: SYN", 114 | } 115 | case "darwin": 116 | expectedPkt = testPkt{ 117 | hexData: "4564280000000000400634930a00000114000001797cac4f00000000000000005002aaaac16a0000", 118 | name: "Darwin IPv4/TCP test Packet, SrcIP: 10.0.0.1, DstIP: 20.0.0.1, SrcPort 31100, DstPort: 44111, Flags: SYN", 119 | } 120 | default: 121 | t.Fatalf("unsupported OS for testing: " + runtime.GOOS) 122 | } 123 | 124 | want, _ = hex.DecodeString(expectedPkt.hexData) 125 | got, err := makePkt(af, srcAddr, dstAddr, srcPort, dstPort, 100, tcpFlags{syn: true}, 0, 0) 126 | require.NoError(t, err, "creating IPv4 TCP packet %s failed: %v", expectedPkt.name, err) 127 | 128 | assert.Equal(got, want, "unexpectedly formatted IPv4 TCP Syn packet generated") 129 | 130 | // IPv6 131 | af = defines.AfInet6 132 | srcAddr = net.IP{0x20, 0x01, 0x06, 0x13, 0x93, 0xFF, 0x8B, 0x40, 0, 0, 0, 0, 0, 0, 0, 1} 133 | dstAddr = net.IP{0x20, 0x04, 0x0B, 0xBD, 0x03, 0x2F, 0x0E, 0x41, 0, 0, 0, 0, 0, 0, 0, 2} 134 | srcPort = layers.TCPPort(1200) 135 | dstPort = layers.TCPPort(44) 136 | expectedPkt = testPkt{ 137 | hexData: "66400000001406002001061393ff8b40000000000000000120040bbd032f0e41000000000000000204b0002c00000000000000005002aaaa7dd40000", 138 | name: "IPv6/TCP test Packet, SrcIP: 2001:613:93ff:8b40::1, DstIP: 2004:bbd:32f:e41::2, SrcPort 1200, DstPort: 44, Flags: SYN", 139 | } 140 | 141 | want, _ = hex.DecodeString(expectedPkt.hexData) 142 | got, err = makePkt(af, srcAddr, dstAddr, srcPort, dstPort, 100, tcpFlags{syn: true}, 0, 0) 143 | require.NoError(t, err, "creating IPv6 TCP packet %s failed: %v", expectedPkt.name, err) 144 | 145 | assert.Equal(want, got, "unexpectedly formatted IPv6 TCP Syn packet generated") 146 | } 147 | -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package util 22 | 23 | import ( 24 | "bufio" 25 | "io/ioutil" 26 | "os" 27 | "os/signal" 28 | "path" 29 | "strconv" 30 | "syscall" 31 | "time" 32 | 33 | "github.com/uber/arachne/internal/ip" 34 | "github.com/uber/arachne/internal/log" 35 | "github.com/uber/arachne/metrics" 36 | 37 | "github.com/fatih/color" 38 | "github.com/pkg/errors" 39 | "go.uber.org/zap" 40 | ) 41 | 42 | const bannerText = ` 43 | ____________________________________________________________/\\\______________________________________ 44 | ___________________________________________________________\/\\\______________________________________ 45 | ___________________________________________________________\/\\\______________________________________ 46 | __/\\\\\\\\\_____/\\/\\\\\\\___/\\\\\\\\\________/\\\\\\\\_\/\\\__________/\\/\\\\\\_______/\\\\\\\\__ 47 | _\////////\\\___\/\\\/////\\\_\////////\\\_____/\\\//////__\/\\\\\\\\\\__\/\\\////\\\____/\\\/////\\\_ 48 | ___/\\\\\\\\\\__\/\\\___\///____/\\\\\\\\\\___/\\\_________\/\\\/////\\\_\/\\\__\//\\\__/\\\\\\\\\\\__ 49 | __/\\\/////\\\__\/\\\__________/\\\/////\\\__\//\\\________\/\\\___\/\\\_\/\\\___\/\\\_\//\\///////___ 50 | _\//\\\\\\\\/\\_\/\\\_________\//\\\\\\\\/\\__\///\\\\\\\\_\/\\\___\/\\\_\/\\\___\/\\\__\//\\\\\\\\\\_ 51 | __\////////\//__\///___________\////////\//_____\////////__\///____\///__\///____\///____\//////////__ 52 | 53 | ` 54 | 55 | // KillChannels includes all channels to tell goroutines to terminate. 56 | type KillChannels struct { 57 | Receiver chan struct{} 58 | Echo chan struct{} 59 | Collector chan struct{} 60 | DNSRefresh chan struct{} 61 | } 62 | 63 | // PrintBanner prints the binary's banner. 64 | func PrintBanner() { 65 | 66 | color.Set(color.FgHiYellow, color.Bold) 67 | defer color.Unset() 68 | 69 | f := bufio.NewWriter(os.Stdout) 70 | defer f.Flush() 71 | f.Write([]byte(bannerText)) 72 | } 73 | 74 | // UnixSignals handles the UNIX signals received. 75 | func UnixSignals(sigC chan struct{}, logger *log.Logger) { 76 | // Set up channel on which to send signal notifications. 77 | // We must use a buffered channel or risk missing the signal 78 | // if we're not ready to receive when the signal is sent. 79 | sigc := make(chan os.Signal, 1) 80 | 81 | signal.Notify(sigc, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGKILL, syscall.SIGTERM, 82 | syscall.SIGUSR1, syscall.SIGUSR2, syscall.SIGHUP, os.Interrupt) 83 | 84 | go func() { 85 | sigType := <-sigc 86 | switch sigType { 87 | //TODO Handle following cases 88 | case os.Interrupt: 89 | //handle SIGINT 90 | case syscall.SIGHUP: 91 | logger.Info("got Hangup/SIGHUP - portable number 1") 92 | case syscall.SIGINT: 93 | logger.Info("got Terminal interrupt signal/SIGINT - portable number 2") 94 | case syscall.SIGQUIT: 95 | logger.Fatal("got Terminal quit signal/SIGQUIT - portable number 3 - will core dump") 96 | case syscall.SIGABRT: 97 | logger.Fatal("got Process abort signal/SIGABRT - portable number 6 - will core dump") 98 | case syscall.SIGKILL: 99 | logger.Info("got Kill signal/SIGKILL - portable number 9") 100 | case syscall.SIGALRM: 101 | logger.Fatal("got Alarm clock signal/SIGALRM - portable number 14") 102 | case syscall.SIGTERM: 103 | logger.Info("got Termination signal/SIGTERM - portable number 15") 104 | case syscall.SIGUSR1: 105 | logger.Info("got User-defined signal 1/SIGUSR1") 106 | case syscall.SIGUSR2: 107 | logger.Info("got User-defined signal 2/SIGUSR2") 108 | default: 109 | logger.Fatal("unhandled Unix signal", zap.Any("sig_type", sigType)) 110 | 111 | } 112 | sigC <- struct{}{} 113 | return 114 | }() 115 | } 116 | 117 | // CheckPID checks if another Arachne process is already running. 118 | func CheckPID(fname string, logger *log.Logger) error { 119 | 120 | if _, err := os.Stat(fname); os.IsNotExist(err) { 121 | return savePID(fname, os.Getpid(), logger) 122 | 123 | } 124 | 125 | content, err := ioutil.ReadFile(fname) 126 | if err != nil { 127 | logger.Error("unable to read PID file", zap.String("file", fname), zap.Error(err)) 128 | return err 129 | } 130 | readPID, err := strconv.Atoi(string(content)) 131 | if err != nil { 132 | logger.Error("invalid content inside PID file", zap.String("file", fname), zap.Error(err)) 133 | return savePID(fname, os.Getpid(), logger) 134 | 135 | } 136 | 137 | // Sending the signal 0 to a given PID just checks if any process with the given PID is running 138 | // and you have the permission to send a signal to it. 139 | if err = syscall.Kill(readPID, 0); err == nil { 140 | logger.Error("Arachne already running and different from self PID", 141 | zap.Int("other_PID", readPID), 142 | zap.Int("self_PID", os.Getpid())) 143 | return errors.New("Arachne already running and different from self PID") 144 | } 145 | return savePID(fname, os.Getpid(), logger) 146 | } 147 | 148 | func savePID(fname string, pid int, logger *log.Logger) error { 149 | 150 | if err := os.MkdirAll(path.Dir(fname), 0777); err != nil { 151 | logger.Error("failed to create PID directory", zap.String("path", path.Dir(fname)), zap.Error(err)) 152 | return err 153 | } 154 | if err := ioutil.WriteFile(fname, []byte(strconv.Itoa(pid)), 0644); err != nil { 155 | logger.Error("failed to create PID file", zap.String("file", fname), zap.Error(err)) 156 | return err 157 | } 158 | 159 | logger.Debug("Created PID file", zap.String("name", fname), zap.Int("PID", pid)) 160 | return nil 161 | } 162 | 163 | // RemovePID removes the PID file. 164 | func RemovePID(fname string, logger *log.Logger) { 165 | if err := os.Remove(fname); err != nil { 166 | logger.Error("failed to remove PID file", zap.String("name", fname), zap.Error(err)) 167 | } else { 168 | logger.Debug("PID file removed", zap.String("name", fname)) 169 | } 170 | } 171 | 172 | // CleanUpRefresh removes state of past refresh. 173 | func CleanUpRefresh(killC *KillChannels, receiverOnlyMode bool, senderOnlyMode bool, resolveDNS bool) { 174 | // Close all the channels 175 | if !receiverOnlyMode { 176 | close(killC.Echo) 177 | } 178 | if !senderOnlyMode { 179 | close(killC.Receiver) 180 | time.Sleep(500 * time.Millisecond) 181 | } 182 | if !receiverOnlyMode && !senderOnlyMode { 183 | close(killC.Collector) 184 | time.Sleep(50 * time.Millisecond) 185 | } 186 | if resolveDNS { 187 | close(killC.DNSRefresh) 188 | } 189 | } 190 | 191 | // CleanUpAll conducts a clean exit. 192 | func CleanUpAll( 193 | killC *KillChannels, 194 | receiverOnlyMode bool, 195 | senderOnlyMode bool, 196 | resolveDNS bool, 197 | conn *ip.Conn, 198 | PIDPath string, 199 | sr metrics.Reporter, 200 | logger *log.Logger, 201 | ) { 202 | 203 | CleanUpRefresh(killC, receiverOnlyMode, senderOnlyMode, resolveDNS) 204 | 205 | conn.Close(logger) 206 | 207 | sr.Close() 208 | 209 | RemovePID(PIDPath, logger) 210 | } 211 | -------------------------------------------------------------------------------- /metrics/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package metrics 22 | 23 | import ( 24 | "time" 25 | 26 | "go.uber.org/zap" 27 | ) 28 | 29 | // Opt is an interface for config unmarshaled in local configuration files. 30 | type Opt interface { 31 | UnmarshalConfig(data []byte, fname string, logger *zap.Logger) (Config, error) 32 | } 33 | 34 | // Config is an interface for creating metrics-specific stats reporters. 35 | type Config interface { 36 | NewReporter(logger *zap.Logger) (Reporter, error) 37 | } 38 | 39 | // Tags is an alias of map[string]string, a type for tags associated with a statistic. 40 | type Tags map[string]string 41 | 42 | // Reporter is an interface for stats reporting functions. Its methods take optional 43 | // tag dictionaries which may be ignored by concrete implementations. 44 | type Reporter interface { 45 | // ReportCounter reports a counter value 46 | ReportCounter(name string, tags Tags, value int64) 47 | 48 | // ReportGauge reports a gauge value 49 | ReportGauge(name string, tags Tags, value int64) 50 | 51 | // RecordTimer 52 | RecordTimer(name string, tags Tags, d time.Duration) 53 | 54 | // Flush is expected to be called by a Scope when it completes a round or reporting 55 | Flush() 56 | 57 | // Close conducts a clean exit for the stats reporting. 58 | Close() error 59 | } 60 | -------------------------------------------------------------------------------- /metrics/metrics_statsd.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package metrics 22 | 23 | import ( 24 | "fmt" 25 | "time" 26 | 27 | "github.com/DataDog/datadog-go/statsd" 28 | "github.com/pkg/errors" 29 | "go.uber.org/zap" 30 | "gopkg.in/validator.v2" 31 | "gopkg.in/yaml.v2" 32 | ) 33 | 34 | // StatsdConfiger implements metrics.Opt. 35 | type StatsdConfiger struct{} 36 | 37 | // StatsdConfig implements metrics.Config. 38 | type StatsdConfig struct { 39 | Metrics struct { 40 | Statsd *statsdFileConfig `yaml:"statsd"` 41 | } `yaml:"metrics"` 42 | } 43 | 44 | type statsdFileConfig struct { 45 | // The host and port of the statsd server 46 | HostPort string `yaml:"hostPort" validate:"nonzero"` 47 | 48 | // The prefix to use in reporting to statsd 49 | Prefix string `yaml:"prefix" validate:"nonzero"` 50 | 51 | // FlushInterval is the maximum interval for sending packets. 52 | // If it is not specified, it defaults to 1 second. 53 | FlushInterval time.Duration `yaml:"flushInterval"` 54 | 55 | // FlushBytes specifies the maximum udp packet size you wish to send. 56 | // If FlushBytes is unspecified, it defaults to 1432 bytes, which is 57 | // considered safe for local traffic. 58 | FlushBytes int `yaml:"flushBytes"` 59 | } 60 | 61 | // statsdReporter is a backend to report metrics to. 62 | type statsdReporter struct { 63 | client *statsd.Client 64 | } 65 | 66 | // Assert that we continue to implement the required interfaces. 67 | var ( 68 | _ Opt = (*StatsdConfiger)(nil) 69 | _ Config = (*StatsdConfig)(nil) 70 | ) 71 | 72 | // UnmarshalConfig fetches the configuration file from local path. 73 | func (c StatsdConfiger) UnmarshalConfig(data []byte, fname string, logger *zap.Logger) (Config, error) { 74 | 75 | cfg := new(StatsdConfig) 76 | if err := yaml.Unmarshal(data, cfg); err != nil { 77 | return nil, errors.Wrapf(err, "error unmarshaling the statsd section in the "+ 78 | "configuration file %s", fname) 79 | } 80 | // Validate on the merged config at the end 81 | if err := validator.Validate(cfg); err != nil { 82 | return nil, errors.Wrapf(err, "invalid info in the statsd section in the "+ 83 | "configuration file %s", fname) 84 | } 85 | 86 | return Config(cfg), nil 87 | } 88 | 89 | // NewReporter creates a new metrics backend talking to Statsd. 90 | func (c StatsdConfig) NewReporter(logger *zap.Logger) (Reporter, error) { 91 | s, err := statsd.New(c.Metrics.Statsd.HostPort) 92 | if err != nil { 93 | return nil, err 94 | } 95 | // add service as prefix 96 | s.Namespace = fmt.Sprintf("%s.", "arachne") 97 | logger.Info("Statsd Metrics configuration", zap.String("object", fmt.Sprintf("%+v", s))) 98 | 99 | return &statsdReporter{client: s}, nil 100 | } 101 | 102 | func (b *statsdReporter) ReportCounter(name string, tags Tags, value int64) { 103 | t := make([]string, 0, len(tags)) 104 | for k, v := range tags { 105 | t = append(t, fmt.Sprintf("%s:%s", k, v)) 106 | } 107 | b.client.Count(name, value, t, 1.0) 108 | } 109 | 110 | func (b *statsdReporter) ReportGauge(name string, tags Tags, value int64) { 111 | t := make([]string, 0, len(tags)) 112 | for k, v := range tags { 113 | t = append(t, fmt.Sprintf("%s:%s", k, v)) 114 | } 115 | b.client.Gauge(name, float64(value), t, 1.0) 116 | } 117 | 118 | func (b *statsdReporter) RecordTimer(name string, tags Tags, d time.Duration) { 119 | t := make([]string, 0, len(tags)) 120 | for k, v := range tags { 121 | t = append(t, fmt.Sprintf("%s:%s", k, v)) 122 | } 123 | b.client.TimeInMilliseconds(name, d.Seconds()*1000, t, 1.0) 124 | } 125 | 126 | func (b *statsdReporter) Flush() { 127 | } 128 | 129 | func (b *statsdReporter) Close() error { 130 | return nil 131 | } 132 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Uber Technologies, Inc. 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package arachne 22 | 23 | import "github.com/uber/arachne/config" 24 | 25 | // Option wraps a function to configure GlobalConfig. 26 | type Option func(*config.Global) Option 27 | 28 | // apply sets the options specified. It also returns an option to 29 | // restore the arguments' previous values, if needed. 30 | func apply(c *config.Global, opts ...Option) []Option { 31 | prevs := make([]Option, len(opts)) 32 | for i, opt := range opts { 33 | prevs[i] = opt(c) 34 | } 35 | return prevs 36 | } 37 | 38 | // ReceiverOnlyMode sets receiver-only mode to `b`. 39 | // To set this option temporarily and have it reverted, do: 40 | // prevRxOnlyMode := apply(&gl, ReceiverOnlyMode(true)) 41 | // DoSomeDebugging() 42 | // apply(prevRxOnlyMode) 43 | func ReceiverOnlyMode(b bool) Option { 44 | return func(gl *config.Global) Option { 45 | previous := *gl.CLI.ReceiverOnlyMode 46 | *gl.CLI.ReceiverOnlyMode = b 47 | return ReceiverOnlyMode(previous) 48 | } 49 | } 50 | 51 | // SenderOnlyMode sets sender-only mode to `b.` 52 | func SenderOnlyMode(b bool) Option { 53 | return func(gl *config.Global) Option { 54 | previous := *gl.CLI.ReceiverOnlyMode 55 | *gl.CLI.SenderOnlyMode = b 56 | return SenderOnlyMode(previous) 57 | } 58 | } 59 | --------------------------------------------------------------------------------