diff --git a/.goreleaser.yml b/.goreleaser.yml index 3b347f3..a0ee28a 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,74 +1,84 @@ -project_name: 'librespeed-cli' +version: 2 +project_name: "librespeed-cli" #dist: ./out before: hooks: - go mod download builds: -- main: ./main.go - id: upx - env: - - CGO_ENABLED=0 - flags: - - -trimpath - ldflags: - - -w -s -X "librespeed-cli/defs.ProgName={{ .ProjectName }}" -X "librespeed-cli/defs.ProgVersion=v{{ .Version }}" -X "librespeed-cli/defs.BuildDate={{ .Date }}" - goos: - - linux - - darwin - - freebsd - goarch: - - 386 - - amd64 - - arm - - arm64 - - mips - - mipsle - - mips64 - - mips64le - goarm: - - 5 - - 6 - - 7 - gomips: - - hardfloat - - softfloat - ignore: - - goos: darwin - goarch: 386 - - goos: darwin - goarch: arm64 - hooks: - post: ./upx.sh -9 "{{ .Path }}" -- main: ./main.go - id: no-upx - env: - - CGO_ENABLED=0 - flags: - - -trimpath - ldflags: - - -w -s -X "librespeed-cli/defs.ProgName={{ .ProjectName }}" -X "librespeed-cli/defs.ProgVersion=v{{ .Version }}" -X "librespeed-cli/defs.BuildDate={{ .Date }}" - goos: - - windows - - darwin - goarch: - - 386 - - amd64 - - arm64 - ignore: - - goos: darwin - goarch: 386 - - goos: darwin - goarch: amd64 + - main: ./main.go + id: upx + env: + - CGO_ENABLED=0 + flags: + - -trimpath + ldflags: + - -w -s -X "librespeed-cli/defs.ProgName={{ .ProjectName }}" -X "librespeed-cli/defs.ProgVersion=v{{ .Version }}" -X "librespeed-cli/defs.BuildDate={{ .Date }}" + goos: + - linux + - darwin + - freebsd + goarch: + - "386" + - amd64 + - arm + - arm64 + goarm: + - "5" + - "6" + - "7" + ignore: + - goos: darwin + goarch: "386" + - goos: darwin + goarch: arm64 + hooks: + post: + - ./upx.sh -9 "{{ .Path }}" + - main: ./main.go + id: no-upx + env: + - CGO_ENABLED=0 + flags: + - -trimpath + ldflags: + - -w -s -X "librespeed-cli/defs.ProgName={{ .ProjectName }}" -X "librespeed-cli/defs.ProgVersion=v{{ .Version }}" -X "librespeed-cli/defs.BuildDate={{ .Date }}" + goos: + - linux + - windows + - darwin + goarch: + - "386" + - amd64 + - arm64 + - mips + - mipsle + - mips64 + - mips64le + - riscv64 + gomips: + - hardfloat + - softfloat + ignore: + - goos: linux + goarch: "386" + - goos: linux + goarch: amd64 + - goos: linux + goarch: arm64 + - goos: darwin + goarch: "386" + - goos: darwin + goarch: amd64 archives: - format_overrides: - goos: windows - format: zip + formats: ['zip'] files: - LICENSE checksum: - name_template: 'checksums.txt' + name_template: "checksums.txt" changelog: - skip: false + disable: false sort: asc release: github: diff --git a/README.md b/README.md index 7fe14a8..d3945cf 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![LibreSpeed Logo](https://github.com/librespeed/speedtest/blob/master/.logo/logo3.png?raw=true) # LibreSpeed command line tool -Don't have a GUI but wants to use LibreSpeed servers to test your Internet speed? 🚀 +Don't have a GUI but want to use LibreSpeed servers to test your Internet speed? 🚀 `librespeed-cli` comes to rescue! @@ -22,7 +22,7 @@ This is a command line interface for LibreSpeed speed test backends, written in [![asciicast](https://asciinema.org/a/J17bUAilWI3qR12JyhfGvPwu2.svg)](https://asciinema.org/a/J17bUAilWI3qR12JyhfGvPwu2) ## Requirements for compiling -- Go 1.14+ +- Go 1.18+ ## Runtime requirements - Any [Go supported platforms](https://github.com/golang/go/wiki/MinimumRequirements) @@ -42,7 +42,7 @@ For Linux users, you can use either the archive from golang.org, or install from ```shell script # pacman -S go ``` - + 2. Then, clone the repository: ```shell script @@ -59,10 +59,10 @@ can now proceed to build `librespeed-cli` with the build script: If you want to build for another operating system or system architecture, use the `GOOS` and `GOARCH` environment variables. Run `go tool dist list` to get a list of possible combinations of `GOOS` and `GOARCH`. - + Note: Technically, the CLI can be compiled with older Go versions that support Go modules, with `GO111MODULE=on` set. If you're compiling with an older Go runtime, you might have to change the Go version in `go.mod`. - + ```shell script # Let's say we're building for 64-bit Windows on Linux $ GOOS=windows GOARCH=amd64 ./build.sh @@ -74,7 +74,7 @@ can now proceed to build `librespeed-cli` with the build script: $ ls out librespeed-cli-windows-amd64.exe ``` - + 5. Now you can use the `librespeed-cli` and test your Internet speed! ## Install from AUR @@ -97,11 +97,45 @@ $ makepkg -si See the [librespeed-cli Homebrew tap](https://github.com/librespeed/homebrew-tap#setup). +## Install on Windows + +If you have either [Scoop](https://scoop.sh/) or [Chocolatey](https://chocolatey.org/) installed you can use one of the following commands: + +- Scoop (ensure you have the `extras` bucket added): + ``` + > scoop install librespeed-cli + ``` + +- Chocolatey: + ``` + > choco install librespeed-cli + ``` + +## Container Image + +You can run `librespeed-cli` in a container. + +1. Build the container image: + + ```shell script + docker build -t librespeed-cli:latest . + ``` + +2. Run the container: + + ```shell script + docker run --rm --name librespeed-cli librespeed-cli:latest + # With options + docker run --rm --name librespeed-cli librespeed-cli:latest --telemetry-level disabled --no-upload + # To avoid "Failed to ping target host: socket: permission denied" errors when using --verbose + docker run --rm --name librespeed-cli --sysctl net.ipv4.ping_group_range="0 2147483647" librespeed-cli:latest --verbose + ``` + ## Usage You can see the full list of supported options with `librespeed-cli -h`: -```shell script +``` $ librespeed-cli -h NAME: librespeed-cli - Test your Internet speed with LibreSpeed 🚀 @@ -116,6 +150,8 @@ GLOBAL OPTIONS: --ipv6, -6 Force IPv6 only (default: false) --no-download Do not perform download test (default: false) --no-upload Do not perform upload test (default: false) + --no-icmp Do not use ICMP ping. ICMP doesn't work well under Linux + at this moment, so you might want to disable it (default: false) --concurrent value Concurrent HTTP requests being made (default: 3) --bytes Display values in bytes instead of bits. Does not affect the image generated by --share, nor output from @@ -126,27 +162,37 @@ GLOBAL OPTIONS: --share Generate and provide a URL to the LibreSpeed.org share results image, not displayed with --csv (default: false) --simple Suppress verbose output, only show basic information - (default: false) + (default: false) --csv Suppress verbose output, only show basic information in CSV format. Speeds listed in bit/s and not affected by --bytes - (default: false) + (default: false) --csv-delimiter CSV_DELIMITER Single character delimiter (CSV_DELIMITER) to use in CSV output. (default: ",") --csv-header Print CSV headers (default: false) --json Suppress verbose output, only show basic information in JSON format. Speeds listed in bit/s and not - affected by --bytes (default: false) + affected by --bytes (default: false) --list Display a list of LibreSpeed.org servers (default: false) --server SERVER Specify a SERVER ID to test against. Can be supplied multiple times. Cannot be used with --exclude --exclude EXCLUDE EXCLUDE a server from selection. Can be supplied multiple times. Cannot be used with --server --server-json value Use an alternative server list from remote JSON file - --local-json value Use an alternative server list from local JSON file - --source SOURCE SOURCE IP address to bind to + --local-json value Use an alternative server list from local JSON file, + or read from stdin with "--local-json -". + --source SOURCE SOURCE IP address to bind to. Incompatible with --interface. + --interface INTERFACE The name of the network interface to bind to. Example: "enp0s3". + Not supported on Windows and incompatible with --source. + Implies --no-icmp. --timeout TIMEOUT HTTP TIMEOUT in seconds. (default: 15) + --duration value Upload and download test duration in seconds (default: 15) + --chunks value Chunks to download from server, chunk size depends on server configuration (default: 100) + --upload-size value Size of payload being uploaded in KiB (default: 1024) --secure Use HTTPS instead of HTTP when communicating with LibreSpeed.org operated servers (default: false) + --ca-cert value Use the specified CA certificate PEM bundle file instead + of the system certificate trust store + --skip-cert-verify Skip verifying SSL certificate for HTTPS connections (self-signed certs) (default: false) --no-pre-allocate Do not pre allocate upload data. Pre allocation is enabled by default to improve upload performance. To support systems with insufficient memory, use this @@ -190,11 +236,15 @@ locally via `--local-json`). The format is as below: ] ``` +The `--local-json` option can also read from `stdin`: + +`echo '[{"id": 1,"name": "a","server": "https://speedtest.example.com/","dlURL": "garbage.php","ulURL": "empty.php","pingURL": "empty.php","getIpURL": "getIP.php"}]' | librespeed-cli --local-json - ` + As you can see in the example, all servers have their schemes defined. In case of undefined scheme (e.g. `//example.com`), `librespeed-cli` will use `http` by default, or `https` when the `--secure` option is enabled. ## Use a custom telemetry server -By default, the telemetry result will be sent to `librespeed.org`. You can also customize your telemetry settings +By default, the telemetry result will be sent to `librespeed.org`. You can also customize your telemetry settings via the `--telemetry` prefixed options. In order to load a custom telemetry endpoint configuration, you'll have to use the `--telemetry-json` option to specify a local JSON file containing the configuration bits. The format is as below: diff --git a/build.sh b/build.sh index ca51d85..b06797a 100755 --- a/build.sh +++ b/build.sh @@ -10,9 +10,10 @@ CURRENT_DIR=$(pwd) OUT_DIR=${CURRENT_DIR}/out PROGNAME="librespeed-cli" +DEFS_PATH="github.com/librespeed/speedtest-cli" BINARY=${PROGNAME}-$(go env GOOS)-$(go env GOARCH) BUILD_DATE=$(date -u "+%Y-%m-%d %H:%M:%S %Z") -LDFLAGS="-w -s -X \"librespeed-cli/defs.ProgName=${PROGNAME}\" -X \"librespeed-cli/defs.ProgVersion=${PROGVER}\" -X \"librespeed-cli/defs.BuildDate=${BUILD_DATE}\"" +LDFLAGS="-w -s -X \"${DEFS_PATH}/defs.ProgName=${PROGNAME}\" -X \"${DEFS_PATH}/defs.ProgVersion=${PROGVER}\" -X \"${DEFS_PATH}/defs.BuildDate=${BUILD_DATE}\"" if [[ -n "${GOARM}" ]] && [[ "${GOARM}" -gt 0 ]]; then BINARY=${BINARY}v${GOARM} diff --git a/defs/bytes_counter.go b/defs/bytes_counter.go index 1553aff..64c155b 100644 --- a/defs/bytes_counter.go +++ b/defs/bytes_counter.go @@ -14,7 +14,7 @@ import ( type BytesCounter struct { start time.Time pos int - total int + total uint64 payload []byte reader io.ReadSeeker mebi bool @@ -33,7 +33,7 @@ func NewCounter() *BytesCounter { func (c *BytesCounter) Write(p []byte) (int, error) { n := len(p) c.lock.Lock() - c.total += n + c.total += uint64(n) c.lock.Unlock() return n, nil @@ -43,7 +43,7 @@ func (c *BytesCounter) Write(p []byte) (int, error) { func (c *BytesCounter) Read(p []byte) (int, error) { n, err := c.reader.Read(p) c.lock.Lock() - c.total += n + c.total += uint64(n) c.pos += n if c.pos == c.uploadSize { c.resetReader() @@ -116,7 +116,7 @@ func (c *BytesCounter) Start() { } // Total returns the total bytes read/written -func (c *BytesCounter) Total() int { +func (c *BytesCounter) Total() uint64 { return c.total } diff --git a/defs/options.go b/defs/options.go index 255a5b1..84662b5 100644 --- a/defs/options.go +++ b/defs/options.go @@ -24,11 +24,13 @@ const ( OptionExclude = "exclude" OptionServerJSON = "server-json" OptionSource = "source" + OptionInterface = "interface" OptionTimeout = "timeout" OptionChunks = "chunks" OptionUploadSize = "upload-size" OptionDuration = "duration" OptionSecure = "secure" + OptionCACert = "ca-cert" OptionSkipCertVerify = "skip-cert-verify" OptionNoPreAllocate = "no-pre-allocate" OptionVersion = "version" @@ -40,4 +42,5 @@ const ( OptionTelemetryPath = "telemetry-path" OptionTelemetryShare = "telemetry-share" OptionTelemetryExtra = "telemetry-extra" + OptionFwmark = "fwmark" ) diff --git a/defs/server.go b/defs/server.go index 33417eb..ed2ffad 100644 --- a/defs/server.go +++ b/defs/server.go @@ -60,8 +60,11 @@ func (s *Server) IsUp() bool { } defer resp.Body.Close() b, _ := ioutil.ReadAll(resp.Body) + if len(b) > 0 { + log.Debugf("Failed when parsing get IP result: %s", b) + } // only return online if the ping URL returns nothing and 200 - return len(b) == 0 && resp.StatusCode == http.StatusOK + return resp.StatusCode == http.StatusOK } // ICMPPingAndJitter pings the server via ICMP echos and calculate the average ping and jitter @@ -185,7 +188,7 @@ func (s *Server) PingAndJitter(count int) (float64, float64, error) { } // Download performs the actual download test -func (s *Server) Download(silent bool, useBytes, useMebi bool, requests int, chunks int, duration time.Duration) (float64, int, error) { +func (s *Server) Download(silent bool, useBytes, useMebi bool, requests int, chunks int, duration time.Duration) (float64, uint64, error) { t := time.Now() defer func() { s.TLog.Logf("Download took %s", time.Now().Sub(t).String()) @@ -277,7 +280,7 @@ Loop: } // Upload performs the actual upload test -func (s *Server) Upload(noPrealloc, silent, useBytes, useMebi bool, requests int, uploadSize int, duration time.Duration) (float64, int, error) { +func (s *Server) Upload(noPrealloc, silent, useBytes, useMebi bool, requests int, uploadSize int, duration time.Duration) (float64, uint64, error) { t := time.Now() defer func() { s.TLog.Logf("Upload took %s", time.Now().Sub(t).String()) @@ -412,6 +415,7 @@ func (s *Server) GetIPInfo(distanceUnit string) (*GetIPResult, error) { if err := json.Unmarshal(b, &ipInfo); err != nil { log.Debugf("Failed when parsing get IP result: %s", err) log.Debugf("Received payload: %s", b) + ipInfo.ProcessedString = string(b[:]) } } diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..944de29 --- /dev/null +++ b/dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.20.3-alpine as builder + +RUN apk add --no-cache bash upx + +# Set working directory +WORKDIR /usr/src/librespeed-cli + +# Copy librespeed-cli +COPY . . + +# Build librespeed-cli +RUN ./build.sh + +FROM alpine:3.17 + +# Copy librespeed-cli binary +COPY --from=builder /usr/src/librespeed-cli/out/librespeed-cli* /bin/librespeed-cli + +CMD ["/bin/librespeed-cli"] diff --git a/go.mod b/go.mod index ddc99a7..da5d4ff 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,27 @@ -module librespeed-cli +module github.com/librespeed/speedtest-cli -go 1.14 +go 1.23.0 + +toolchain go1.24.2 require ( - github.com/briandowns/spinner v1.12.0 - github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect - github.com/fatih/color v1.10.0 // indirect - github.com/go-ping/ping v0.0.0-20210407214646-e4e642a95741 - github.com/gocarina/gocsv v0.0.0-20210408192840-02d7211d929d - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sirupsen/logrus v1.8.1 - github.com/stretchr/testify v1.3.0 // indirect - github.com/urfave/cli/v2 v2.3.0 - golang.org/x/net v0.0.0-20210421230115-4e50805a0758 // indirect - golang.org/x/sys v0.0.0-20210421221651-33663a62ff08 // indirect + github.com/briandowns/spinner v1.23.1 + github.com/go-ping/ping v1.2.0 + github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 + github.com/sirupsen/logrus v1.9.3 + github.com/urfave/cli/v2 v2.27.4 + golang.org/x/sys v0.33.0 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/term v0.32.0 // indirect ) diff --git a/go.sum b/go.sum index cb424de..51dbae2 100644 --- a/go.sum +++ b/go.sum @@ -1,59 +1,56 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/briandowns/spinner v1.12.0 h1:72O0PzqGJb6G3KgrcIOtL/JAGGZ5ptOMCn9cUHmqsmw= -github.com/briandowns/spinner v1.12.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= +github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/go-ping/ping v0.0.0-20210407214646-e4e642a95741 h1:b0sLP++Tsle+s57tqg5sUk1/OQsC6yMCciVeqNzOcwU= -github.com/go-ping/ping v0.0.0-20210407214646-e4e642a95741/go.mod h1:35JbSyV/BYqHwwRA6Zr1uVDm1637YlNOU61wI797NPI= -github.com/gocarina/gocsv v0.0.0-20210408192840-02d7211d929d h1:r3mStZSyjKhEcgbJ5xtv7kT5PZw/tDiFBTMgQx2qsXE= -github.com/gocarina/gocsv v0.0.0-20210408192840-02d7211d929d/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/go-ping/ping v1.2.0 h1:vsJ8slZBZAXNCK4dPcI2PEE9eM9n9RbXbGouVQ/Y4yQ= +github.com/go-ping/ping v1.2.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ= +github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20210421230115-4e50805a0758 h1:aEpZnXcAmXkd6AvLb2OPt+EN1Zu/8Ne3pCqPjja5PXY= -golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210421221651-33663a62ff08 h1:qyN5bV+96OX8pL78eXDuz6YlDPzCYgdW74H5yE9BoSU= -golang.org/x/sys v0.0.0-20210421221651-33663a62ff08/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index c467b2f..cc948cc 100644 --- a/main.go +++ b/main.go @@ -6,8 +6,8 @@ import ( log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" - "librespeed-cli/defs" - "librespeed-cli/speedtest" + "github.com/librespeed/speedtest-cli/defs" + "github.com/librespeed/speedtest-cli/speedtest" ) // init sets up the essential bits on start up @@ -20,7 +20,7 @@ func init() { // warn level is for suppress modes // error level is for errors - log.SetOutput(os.Stdout) + log.SetOutput(os.Stderr) log.SetFormatter(formatter) log.SetLevel(log.InfoLevel) } @@ -59,7 +59,7 @@ func main() { &cli.BoolFlag{ Name: defs.OptionNoICMP, Usage: "Do not use ICMP ping. ICMP doesn't work well under Linux\n" + - "at this moment, so you might want to disable it", + "\tat this moment, so you might want to disable it", }, &cli.IntFlag{ Name: defs.OptionConcurrent, @@ -110,7 +110,7 @@ func main() { Name: defs.OptionJSON, Usage: "Suppress verbose output, only show basic information\n" + "\tin JSON format. Speeds listed in bit/s and not\n" + - "\t affected by --bytes", + "\taffected by --bytes", }, &cli.BoolFlag{ Name: defs.OptionList, @@ -139,9 +139,13 @@ func main() { Name: defs.OptionSource, Usage: "`SOURCE` IP address to bind to", }, + &cli.StringFlag{ + Name: defs.OptionInterface, + Usage: "network INTERFACE to bind to", + }, &cli.IntFlag{ Name: defs.OptionTimeout, - Usage: "HTTP `TIMEOUT` in seconds", + Usage: "HTTP `TIMEOUT` in seconds.", Value: 15, }, &cli.IntFlag{ @@ -164,6 +168,11 @@ func main() { Usage: "Use HTTPS instead of HTTP when communicating with\n" + "\tLibreSpeed.org operated servers", }, + &cli.StringFlag{ + Name: defs.OptionCACert, + Usage: "Use the specified CA certificate PEM bundle file instead\n" + + "\tof the system certificate trust store", + }, &cli.BoolFlag{ Name: defs.OptionSkipCertVerify, Usage: "Skip verifying SSL certificate for HTTPS connections (self-signed certs)", @@ -209,6 +218,11 @@ func main() { Usage: "Send a custom message along with the telemetry results.\n" + "\tImplies --" + defs.OptionShare, }, + &cli.IntFlag{ + Name: defs.OptionFwmark, + Usage: "firewall mark to set on socket.", + Value: 0, + }, }, } diff --git a/report/json.go b/report/json.go index a8e7dd7..d399aed 100644 --- a/report/json.go +++ b/report/json.go @@ -3,7 +3,7 @@ package report import ( "time" - "librespeed-cli/defs" + "github.com/librespeed/speedtest-cli/defs" ) // JSONReport represents the output data fields in a JSON file @@ -11,8 +11,8 @@ type JSONReport struct { Timestamp time.Time `json:"timestamp"` Server Server `json:"server"` Client Client `json:"client"` - BytesSent int `json:"bytes_sent"` - BytesReceived int `json:"bytes_received"` + BytesSent uint64 `json:"bytes_sent"` + BytesReceived uint64 `json:"bytes_received"` Ping float64 `json:"ping"` Jitter float64 `json:"jitter"` Upload float64 `json:"upload"` diff --git a/speedtest/helper.go b/speedtest/helper.go index a357c66..1be6203 100644 --- a/speedtest/helper.go +++ b/speedtest/helper.go @@ -8,17 +8,17 @@ import ( "math" "mime/multipart" "net/http" + "os" "strconv" "strings" "time" "github.com/briandowns/spinner" "github.com/gocarina/gocsv" + "github.com/librespeed/speedtest-cli/defs" + "github.com/librespeed/speedtest-cli/report" log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" - - "librespeed-cli/defs" - "librespeed-cli/report" +"github.com/urfave/cli/v2" ) const ( @@ -27,11 +27,14 @@ const ( ) // doSpeedTest is where the actual speed test happens -func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.TelemetryServer, network string, silent bool) error { +func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.TelemetryServer, network string, silent bool, noICMP bool) error { if serverCount := len(servers); serverCount > 1 { log.Infof("Testing against %d servers", serverCount) } + var reps_json []report.JSONReport + var reps_csv []report.CSVReport + // fetch current user's IP info for _, currentServer := range servers { // get telemetry level @@ -66,7 +69,7 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel } // skip ICMP if option given - currentServer.NoICMP = c.Bool(defs.OptionNoICMP) + currentServer.NoICMP = noICMP p, jitter, err := currentServer.ICMPPingAndJitter(pingCount, c.String(defs.OptionSource), network) if err != nil { @@ -75,13 +78,13 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel } if pb != nil { - pb.FinalMSG = fmt.Sprintf("Ping: %.0f ms\tJitter: %.0f ms\n", p, jitter) + pb.FinalMSG = fmt.Sprintf("Ping: %.2f ms\tJitter: %.2f ms\n", p, jitter) pb.Stop() } // get download value var downloadValue float64 - var bytesRead int + var bytesRead uint64 if c.Bool(defs.OptionNoDownload) { log.Info("Download test is disabled") } else { @@ -91,12 +94,12 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel return err } downloadValue = download - bytesRead = br + bytesRead = uint64(br) } // get upload value var uploadValue float64 - var bytesWritten int + var bytesWritten uint64 if c.Bool(defs.OptionNoUpload) { log.Info("Upload test is disabled") } else { @@ -106,16 +109,16 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel return err } uploadValue = upload - bytesWritten = bw + bytesWritten = uint64(bw) } // print result if --simple is given if c.Bool(defs.OptionSimple) { if c.Bool(defs.OptionBytes) { useMebi := c.Bool(defs.OptionMebiBytes) - log.Warnf("Ping:\t%.0f ms\tJitter:\t%.0f ms\nDownload rate:\t%s\nUpload rate:\t%s", p, jitter, humanizeMbps(downloadValue, useMebi), humanizeMbps(uploadValue, useMebi)) + log.Warnf("Ping:\t%.2f ms\tJitter:\t%.2f ms\nDownload rate:\t%s\nUpload rate:\t%s", p, jitter, humanizeMbps(downloadValue, useMebi), humanizeMbps(uploadValue, useMebi)) } else { - log.Warnf("Ping:\t%.0f ms\tJitter:\t%.0f ms\nDownload rate:\t%.2f Mbps\nUpload rate:\t%.2f Mbps", p, jitter, downloadValue, uploadValue) + log.Warnf("Ping:\t%.2f ms\tJitter:\t%.2f ms\nDownload rate:\t%.2f Mbps\nUpload rate:\t%.2f Mbps", p, jitter, downloadValue, uploadValue) } } @@ -140,34 +143,25 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel // check for --csv or --json. the program prioritize the --csv before the --json. this is the same behavior as speedtest-cli if c.Bool(defs.OptionCSV) { // print csv if --csv is given - var reps []report.CSVReport - var rep report.CSVReport rep.Timestamp = time.Now() rep.Name = currentServer.Name rep.Address = u.String() - rep.Ping = p + rep.Ping = math.Round(p*100) / 100 rep.Jitter = math.Round(jitter*100) / 100 rep.Download = math.Round(downloadValue*100) / 100 rep.Upload = math.Round(uploadValue*100) / 100 rep.Share = shareLink rep.IP = ispInfo.RawISPInfo.IP - reps = append(reps, rep) - - var buf bytes.Buffer - if err := gocsv.MarshalWithoutHeaders(&reps, &buf); err != nil { - log.Errorf("Error generating CSV report: %s", err) - } else { - log.Warn(buf.String()) - } + reps_csv = append(reps_csv, rep) } else if c.Bool(defs.OptionJSON) { // print json if --json is given var rep report.JSONReport rep.Timestamp = time.Now() - rep.Ping = p + rep.Ping = math.Round(p*100) / 100 rep.Jitter = math.Round(jitter*100) / 100 rep.Download = math.Round(downloadValue*100) / 100 rep.Upload = math.Round(uploadValue*100) / 100 @@ -181,22 +175,34 @@ func doSpeedTest(c *cli.Context, servers []defs.Server, telemetryServer defs.Tel rep.Client = report.Client{ispInfo.RawISPInfo} rep.Client.Readme = "" - if b, err := json.Marshal(&rep); err != nil { - log.Errorf("Error generating JSON report: %s", err) - } else { - log.Warnf("%s", b) - } + reps_json = append(reps_json, rep) } } else { log.Infof("Selected server %s (%s) is not responding at the moment, try again later", currentServer.Name, u.Hostname()) } - // add a new line after each test if testing multiple servers - if len(servers) > 1 { + //add a new line after each test if testing multiple servers + if len(servers) > 1 && !silent { log.Warn() } } + // check for --csv or --json. the program prioritize the --csv before the --json. this is the same behavior as speedtest-cli + if c.Bool(defs.OptionCSV) { + var buf bytes.Buffer + if err := gocsv.MarshalWithoutHeaders(&reps_csv, &buf); err != nil { + log.Errorf("Error generating CSV report: %s", err) + } else { + os.Stdout.WriteString(buf.String()) + } + } else if c.Bool(defs.OptionJSON) { + if b, err := json.Marshal(&reps_json); err != nil { + log.Errorf("Error generating JSON report: %s", err) + } else { + os.Stdout.Write(b[:]) + } + } + return nil } @@ -233,7 +239,7 @@ func sendTelemetry(telemetryServer defs.TelemetryServer, ispInfo *defs.GetIPResu if fPing, err := wr.CreateFormField("ping"); err != nil { log.Debugf("Error creating form field: %s", err) return "", err - } else if _, err = fPing.Write([]byte(strconv.Itoa(int(pingVal)))); err != nil { + } else if _, err = fPing.Write([]byte(strconv.FormatFloat(pingVal, 'f', 2, 64))); err != nil { log.Debugf("Error writing form field: %s", err) return "", err } @@ -241,7 +247,7 @@ func sendTelemetry(telemetryServer defs.TelemetryServer, ispInfo *defs.GetIPResu if fJitter, err := wr.CreateFormField("jitter"); err != nil { log.Debugf("Error creating form field: %s", err) return "", err - } else if _, err = fJitter.Write([]byte(strconv.Itoa(int(jitter)))); err != nil { + } else if _, err = fJitter.Write([]byte(strconv.FormatFloat(jitter, 'f', 2, 64))); err != nil { log.Debugf("Error writing form field: %s", err) return "", err } diff --git a/speedtest/speedtest.go b/speedtest/speedtest.go index b74934e..20018ac 100644 --- a/speedtest/speedtest.go +++ b/speedtest/speedtest.go @@ -3,6 +3,7 @@ package speedtest import ( "context" "crypto/tls" + "crypto/x509" "encoding/json" "errors" "fmt" @@ -19,8 +20,8 @@ import ( log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" - "librespeed-cli/defs" - "librespeed-cli/report" + "github.com/librespeed/speedtest-cli/defs" + "github.com/librespeed/speedtest-cli/report" ) const ( @@ -64,6 +65,7 @@ func SpeedTest(c *cli.Context) error { // print version if c.Bool(defs.OptionVersion) { + log.SetOutput(os.Stdout) log.Warnf("%s %s (built on %s)", defs.ProgName, defs.ProgVersion, defs.BuildDate) log.Warn("https://github.com/librespeed/speedtest-cli") log.Warn("Licensed under GNU Lesser General Public License v3.0") @@ -73,6 +75,10 @@ func SpeedTest(c *cli.Context) error { return nil } + if c.String(defs.OptionSource) != "" && c.String(defs.OptionInterface) != "" { + return fmt.Errorf("incompatible options '%s' and '%s'", defs.OptionSource, defs.OptionInterface) + } + // set CSV delimiter gocsv.TagSeparator = c.String(defs.OptionCSVDelimiter) @@ -80,7 +86,7 @@ func SpeedTest(c *cli.Context) error { if c.Bool(defs.OptionCSVHeader) { var rep []report.CSVReport b, _ := gocsv.MarshalBytes(&rep) - log.Warnf("%s", b) + os.Stdout.WriteString(string(b)) return nil } @@ -99,7 +105,7 @@ func SpeedTest(c *cli.Context) error { return err } if err := json.Unmarshal(b, &telemetryServer); err != nil { - log.Errorf("Error parsing %s: %s", err) + log.Errorf("Error parsing %s: %s", telemetryJSON, err) return err } } @@ -137,6 +143,8 @@ func SpeedTest(c *cli.Context) error { return errors.New("invalid concurrent requests setting") } + noICMP := c.Bool(defs.OptionNoICMP) + // HTTP requests timeout http.DefaultClient.Timeout = time.Duration(c.Int(defs.OptionTimeout)) * time.Second @@ -154,59 +162,71 @@ func SpeedTest(c *cli.Context) error { } transport := http.DefaultTransport.(*http.Transport).Clone() - transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: c.Bool(defs.OptionSkipCertVerify)} - // bind to source IP address if given, or if ipv4/ipv6 is forced - if src := c.String(defs.OptionSource); src != "" || (forceIPv4 || forceIPv6) { - var localTCPAddr *net.TCPAddr - if src != "" { - // first we parse the IP to see if it's valid - addr, err := net.ResolveIPAddr(network, src) - if err != nil { - if strings.Contains(err.Error(), "no suitable address") { - if forceIPv6 { - log.Errorf("Address %s is not a valid IPv6 address", src) - } else { - log.Errorf("Address %s is not a valid IPv4 address", src) - } - } else { - log.Errorf("Error parsing source IP: %s", err) - } - return err - } - - log.Debugf("Using %s as source IP", src) - localTCPAddr = &net.TCPAddr{IP: addr.IP} + if caCertFileName := c.String(defs.OptionCACert); caCertFileName != "" { + caCert, err := ioutil.ReadFile(caCertFileName) + if err != nil { + log.Fatal(err) } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) - var dialContext func(context.Context, string, string) (net.Conn, error) - defaultDialer := &net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: c.Bool(defs.OptionSkipCertVerify), + RootCAs: caCertPool, } - - if localTCPAddr != nil { - defaultDialer.LocalAddr = localTCPAddr + } else { + transport.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: c.Bool(defs.OptionSkipCertVerify), } - - switch { - case forceIPv4: - dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) { - return defaultDialer.DialContext(ctx, "tcp4", address) - } - case forceIPv6: - dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) { - return defaultDialer.DialContext(ctx, "tcp6", address) - } - default: - dialContext = defaultDialer.DialContext - } - - // set default HTTP client's Transport to the one that binds the source address - // this is modified from http.DefaultTransport - transport.DialContext = dialContext } + dialer := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + } + // bind to source IP address if given + if src := c.String(defs.OptionSource); src != "" { + var err error + dialer, err = newDialerAddressBound(src, network) + if err != nil { + return err + } + } + + // bind to interface if given + // bind to interface if given + iface := c.String(defs.OptionInterface) + fwmark := c.Int(defs.OptionFwmark) + + if iface != "" || fwmark > 0 { + var err error + dialer, err = newDialerInterfaceOrFwmarkBound(iface, fwmark) + if err != nil { + return err + } + // ICMP ping does not support interface binding. + noICMP = true + } + + // enforce if ipv4/ipv6 is forced + var dialContext func(context.Context, string, string) (net.Conn, error) + switch { + case forceIPv4: + dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) { + return dialer.DialContext(ctx, "tcp4", address) + } + case forceIPv6: + dialContext = func(ctx context.Context, network, address string) (conn net.Conn, err error) { + return dialer.DialContext(ctx, "tcp6", address) + } + default: + dialContext = dialer.DialContext + } + + // set default HTTP client's Transport to the one that binds the source address + // this is modified from http.DefaultTransport + transport.DialContext = dialContext http.DefaultClient.Transport = transport // load server list @@ -257,21 +277,21 @@ func SpeedTest(c *cli.Context) error { // if --server is given, do speed tests with all of them if len(c.IntSlice(defs.OptionServer)) > 0 { - return doSpeedTest(c, servers, telemetryServer, network, silent) + return doSpeedTest(c, servers, telemetryServer, network, silent, noICMP) } else { // else select the fastest server from the list log.Info("Selecting the fastest server based on ping") var wg sync.WaitGroup - jobs := make(chan PingJob, 10) - results := make(chan PingResult, 10) + jobs := make(chan PingJob, len(servers)) + results := make(chan PingResult, len(servers)) done := make(chan struct{}) pingList := make(map[int]float64) // spawn 10 concurrent pingers for i := 0; i < 10; i++ { - go pingWorker(jobs, results, &wg, c.String(defs.OptionSource), network, c.Bool(defs.OptionNoICMP)) + go pingWorker(jobs, results, &wg, c.String(defs.OptionSource), network, noICMP) } // send ping jobs to workers @@ -308,7 +328,7 @@ func SpeedTest(c *cli.Context) error { } // do speed test on the server - return doSpeedTest(c, []defs.Server{servers[serverIdx]}, telemetryServer, network, silent) + return doSpeedTest(c, []defs.Server{servers[serverIdx]}, telemetryServer, network, silent, noICMP) } } @@ -453,6 +473,10 @@ func preprocessServers(servers []defs.Server, forceHTTPS bool, excludes, specifi ret = append(ret, server) } } + if len(ret) == 0 { + error_message := fmt.Sprintf("specified server(s) not found: %v", specific) + return nil, errors.New(error_message) + } return ret, nil } } @@ -469,3 +493,31 @@ func contains(arr []int, val int) bool { } return false } + +func newDialerAddressBound(src string, network string) (dialer *net.Dialer, err error) { + // first we parse the IP to see if it's valid + addr, err := net.ResolveIPAddr(network, src) + if err != nil { + if strings.Contains(err.Error(), "no suitable address") { + if network == "ip6" { + log.Errorf("Address %s is not a valid IPv6 address", src) + } else { + log.Errorf("Address %s is not a valid IPv4 address", src) + } + } else { + log.Errorf("Error parsing source IP: %s", err) + } + return nil, err + } + + log.Debugf("Using %s as source IP", src) + localTCPAddr := &net.TCPAddr{IP: addr.IP} + + defaultDialer := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + } + + defaultDialer.LocalAddr = localTCPAddr + return defaultDialer, nil +} diff --git a/speedtest/util.go b/speedtest/util.go new file mode 100644 index 0000000..0a35393 --- /dev/null +++ b/speedtest/util.go @@ -0,0 +1,13 @@ +//go:build !linux +// +build !linux + +package speedtest + +import ( + "fmt" + "net" +) + +func newDialerInterfaceOrFwmarkBound(iface string, fwmark int) (dialer *net.Dialer, err error) { + return nil, fmt.Errorf("cannot bound to interface on this platform") +} diff --git a/speedtest/util_linux.go b/speedtest/util_linux.go new file mode 100644 index 0000000..e153e87 --- /dev/null +++ b/speedtest/util_linux.go @@ -0,0 +1,38 @@ +package speedtest + +import ( + "net" + "syscall" + "time" + + "golang.org/x/sys/unix" +) + +func newDialerInterfaceOrFwmarkBound(iface string, fwmark int) (dialer *net.Dialer, err error) { + // In linux there is the socket option SO_BINDTODEVICE. + // Therefore we can really bind the socket to the device instead of binding to the address that + // would be affected by the default routes. + control := func(network, address string, c syscall.RawConn) error { + var errSock error + err := c.Control((func(fd uintptr) { + if iface != "" { + errSock = unix.BindToDevice(int(fd), iface) + } + + if fwmark > 0 { + errSock = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, fwmark) + } + })) + if err != nil { + return err + } + return errSock + } + + dialer = &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + Control: control, + } + return dialer, nil +}