├── Netwatch.png ├── Web_Demo.png ├── Dive-in_result.png ├── MikroTik-Speedtest ├── go.mod ├── go.sum ├── main.go └── function │ └── download │ └── download.go ├── Dockerfile ├── LICENSE └── README.md /Netwatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdhtlr/MikroTik-Speedtest/HEAD/Netwatch.png -------------------------------------------------------------------------------- /Web_Demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdhtlr/MikroTik-Speedtest/HEAD/Web_Demo.png -------------------------------------------------------------------------------- /Dive-in_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdhtlr/MikroTik-Speedtest/HEAD/Dive-in_result.png -------------------------------------------------------------------------------- /MikroTik-Speedtest/go.mod: -------------------------------------------------------------------------------- 1 | module MikroTik-Speedtest 2 | 3 | go 1.19 4 | 5 | require github.com/go-echarts/go-echarts/v2 v2.2.5 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # builder image 2 | FROM golang:alpine as builder 3 | USER root 4 | RUN mkdir -m 777 /MikroTik-Speedtest/ 5 | COPY MikroTik-Speedtest/. /MikroTik-Speedtest/ 6 | WORKDIR /MikroTik-Speedtest/ 7 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -extldflags=-static" -a -o speedtest . 8 | 9 | # generate clean, final image for end users 10 | FROM cdhtlr/busybox 11 | 12 | LABEL maintainer="Sidik Hadi Kurniadi" name="Mikrotik Speedtest" description="Base minimum MikroTik-Terminal friendly Download Speedtest" version="4.5" 13 | 14 | ENV URL="https://jakarta.speedtest.telkom.net.id.prod.hosts.ooklaserver.net:8080/download?size=25000000" 15 | ENV MAX_DLSIZE="1.0" 16 | ENV MIN_THRESHOLD="1.0" 17 | ENV CONCURENT_CONNECTION="4" 18 | ENV ALLOW_MEMORY_BUFFER="YES" 19 | 20 | COPY --from=builder /MikroTik-Speedtest/speedtest . 21 | 22 | EXPOSE 80 23 | 24 | CMD ["/speedtest"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sidik Hadi Kurniadi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MikroTik-Speedtest/go.sum: -------------------------------------------------------------------------------- 1 | github.com/cinar/indicator v1.2.24/go.mod h1:5eX8f1PG9g3RKSoHsoQxKd8bIN97Cf/gbgxXjihROpI= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-echarts/go-echarts/v2 v2.2.5 h1:Jl0gtQa9i/iTZHEsmzf89HoxX2WTGa4K5r0be4qaquE= 6 | github.com/go-echarts/go-echarts/v2 v2.2.5/go.mod h1:IN5P8jIRZKENmAJf2lHXBzv8U9YwdVnY9urdzGkEDA0= 7 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 8 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 9 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho= 14 | github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 16 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 18 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | -------------------------------------------------------------------------------- /MikroTik-Speedtest/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | . "bufio" 5 | "net/http" 6 | . "os" 7 | "runtime/debug" 8 | 9 | "MikroTik-Speedtest/function/download" 10 | 11 | "github.com/go-echarts/go-echarts/v2/charts" 12 | "github.com/go-echarts/go-echarts/v2/opts" 13 | "github.com/go-echarts/go-echarts/v2/types" 14 | ) 15 | 16 | func main() { 17 | createDB() 18 | 19 | http.HandleFunc("/", speedtestHandler) 20 | http.HandleFunc("/condition", speedtestHandler) 21 | http.HandleFunc("/chart", chart) 22 | 23 | Stdout.Write([]byte("Speedtest web application runs on port 80\n")) 24 | http.ListenAndServe(":80", nil) 25 | } 26 | 27 | func createDB() { 28 | file, _ := Create("data.txt") 29 | defer file.Close() 30 | } 31 | 32 | func speedtestHandler(w http.ResponseWriter, r *http.Request) { 33 | defer debug.FreeOSMemory() 34 | 35 | w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0, s-maxage=0") 36 | w.Header().Set("Pragma", "no-cache") 37 | w.Header().Set("Expires", "0") 38 | 39 | code := 404 40 | result := "Not found" 41 | 42 | if r.URL.Path == "/" { 43 | code, result = download.Run(false) 44 | } else if r.URL.Path == "/condition" { 45 | code, result = download.Run(true) 46 | } 47 | w.WriteHeader(code) 48 | w.Write([]byte(result)) 49 | } 50 | 51 | func generateChartItems() []opts.LineData { 52 | file, _ := Open("data.txt") 53 | defer file.Close() 54 | 55 | fileScanner := NewScanner(file) 56 | fileScanner.Split(ScanLines) 57 | 58 | items := make([]opts.LineData, 0) 59 | for fileScanner.Scan() { 60 | items = append(items, opts.LineData{Value: fileScanner.Text()}) 61 | } 62 | return items 63 | } 64 | 65 | func chart(w http.ResponseWriter, _ *http.Request) { 66 | defer debug.FreeOSMemory() 67 | 68 | items := generateChartItems() 69 | 70 | line := charts.NewLine() 71 | line.SetGlobalOptions( 72 | charts.WithInitializationOpts(opts.Initialization{Theme: types.ThemeWalden, PageTitle: "Speedtest Chart"}), 73 | charts.WithTooltipOpts(opts.Tooltip{Show: true}), 74 | ) 75 | 76 | line.SetXAxis(items). 77 | AddSeries("Download Speed", items). 78 | SetSeriesOptions(charts.WithLineChartOpts(opts.LineChart{Smooth: true}), charts.WithMarkPointNameTypeItemOpts( 79 | opts.MarkPointNameTypeItem{Name: "Maximum", Type: "max"}, 80 | opts.MarkPointNameTypeItem{Name: "Average", Type: "average"}, 81 | opts.MarkPointNameTypeItem{Name: "Minimum", Type: "min"}, 82 | )) 83 | line.Render(w) 84 | } 85 | -------------------------------------------------------------------------------- /MikroTik-Speedtest/function/download/download.go: -------------------------------------------------------------------------------- 1 | package download 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | . "io" 7 | "math" 8 | "net/http" 9 | "os" 10 | . "strconv" 11 | "strings" 12 | . "time" 13 | ) 14 | 15 | type buf_downloader struct { 16 | iterNum int 17 | buf []byte 18 | r Reader 19 | } 20 | 21 | var ( 22 | downloadCompleted = 0 23 | max_dl_size_int = int(math.Floor(max_dl_size)) * 1024 * 1024 24 | max_dl_size, _ = ParseFloat(os.Getenv("MAX_DLSIZE"), 64) 25 | threshold, _ = ParseFloat(os.Getenv("MIN_THRESHOLD"), 64) 26 | url = os.Getenv("URL") 27 | ) 28 | 29 | func isAcceptRangeSupported() (bool, int) { 30 | req, _ := http.NewRequest("HEAD", url, nil) 31 | client := &http.Client{ 32 | Timeout: 5 * Second, 33 | Transport: &http.Transport{ 34 | TLSClientConfig: &tls.Config{ 35 | InsecureSkipVerify: true, 36 | }, 37 | }, 38 | } 39 | resp, err := client.Do(req) 40 | if err != nil { 41 | return false, 0 42 | } 43 | defer resp.Body.Close() 44 | 45 | if resp.StatusCode >= 400 { 46 | return false, 0 47 | } 48 | 49 | acceptRanges := strings.ToLower(resp.Header.Get("Accept-Ranges")) 50 | if acceptRanges == "" || acceptRanges == "none" { 51 | return false, int(resp.ContentLength) 52 | } 53 | 54 | return true, int(resp.ContentLength) 55 | } 56 | 57 | func downloadPart(start int, end int, done chan bool) { 58 | download("NO", Itoa(int(start))+"-"+Itoa(int(end))) 59 | done <- true 60 | } 61 | 62 | func download(opts ...string) { 63 | req, _ := http.NewRequest("GET", url, nil) 64 | if len(opts) > 1 { 65 | req.Header.Add("Range", "bytes="+opts[1]) 66 | } 67 | client := &http.Client{ 68 | Timeout: 30 * Second, 69 | Transport: &http.Transport{ 70 | TLSClientConfig: &tls.Config{ 71 | InsecureSkipVerify: true, 72 | }, 73 | }, 74 | } 75 | resp, err := client.Do(req) 76 | if err != nil { 77 | downloadCompleted -= 1 78 | } else { 79 | downloadCompleted += 1 80 | } 81 | defer resp.Body.Close() 82 | 83 | if opts[0] == "YES" { 84 | d := &buf_downloader{ 85 | buf: make([]byte, 1024), 86 | r: resp.Body, 87 | } 88 | d.bufDown() 89 | } else { 90 | Copy(Discard, resp.Body) 91 | } 92 | } 93 | 94 | func (d *buf_downloader) bufDown() { 95 | for { 96 | _, err := ReadFull(d.r, d.buf) 97 | d.iterNum++ 98 | if err == EOF || err == ErrUnexpectedEOF || float64((d.iterNum/1024)) >= float64(max_dl_size_int/1024/1024) { 99 | break 100 | } 101 | } 102 | } 103 | 104 | func saveResult(speedtest_result string) error { 105 | file, err := os.OpenFile("data.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 106 | if err != nil { 107 | return err 108 | } 109 | defer file.Close() 110 | 111 | writer := bufio.NewWriter(file) 112 | _, err = writer.WriteString(speedtest_result + "\n") 113 | 114 | if err != nil { 115 | return err 116 | } 117 | 118 | return writer.Flush() 119 | } 120 | 121 | func Run(explain bool) (int, string) { 122 | downloadCompleted = 0 123 | 124 | concurentConn, _ := Atoi(os.Getenv("CONCURENT_CONNECTION")) 125 | 126 | acceptRangeSupported, fileSize := isAcceptRangeSupported() 127 | 128 | begin := Now() 129 | 130 | if fileSize > 0 { 131 | if 0 < max_dl_size_int && max_dl_size_int < fileSize { 132 | fileSize = max_dl_size_int 133 | } 134 | 135 | if acceptRangeSupported { 136 | partSize := fileSize / concurentConn 137 | done := make(chan bool, concurentConn) 138 | 139 | for i := 0; i < concurentConn; i++ { 140 | start := i * partSize 141 | end := (i+1)*partSize - 1 142 | if i == concurentConn-1 { 143 | end = fileSize - 1 144 | } 145 | go downloadPart(start, end, done) 146 | } 147 | for i := 0; i < concurentConn; i++ { 148 | <-done 149 | } 150 | } else { 151 | concurentConn = 1 152 | download(strings.ToUpper(os.Getenv("ALLOW_MEMORY_BUFFER"))) 153 | } 154 | } 155 | 156 | elapsed_time := Since(begin).Seconds() 157 | speedtest_result := float64(fileSize*8/1024/1024) / elapsed_time 158 | speedtest_result_string := FormatFloat(speedtest_result, 'f', 2, 64) 159 | code := 200 160 | condition := "Good" 161 | 162 | if speedtest_result < threshold { 163 | code = 201 164 | condition = "Bad" 165 | } 166 | 167 | if speedtest_result_string == "NaN" || downloadCompleted != concurentConn { 168 | speedtest_result_string = "0.0" 169 | } 170 | 171 | saveResult(speedtest_result_string) 172 | 173 | if explain { 174 | return code, condition 175 | } 176 | 177 | return code, speedtest_result_string 178 | } 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Preview videos : https://www.youtube.com/playlist?list=PLyx101r52fx7jqx3gHLP9T83HwaJSULEn 2 | 3 | DockerHub : https://hub.docker.com/r/cdhtlr/mikrotik-speedtest 4 | 5 | ![](https://raw.githubusercontent.com/cdhtlr/MikroTik-Speedtest/main/Web_Demo.png "Web Demo") 6 | 7 | System requirements: 8 | - Any linux PC or MikroTik RouterOS v7.6 (and above) (Currently support Container for ARM, ARM64, X86 and X86-64 based machine, like the CCR2004-16G-2S+ device with ARM64 architecture) 9 | - 10.5MB of disk space left (3MB for compressed TAR image, 7.5MB for uncompressed running image. You can delete the compressed TAR image once the image is successfully decompressed) 10 | - 6MB RAM space left (Could be more, depending on the workload) 11 | 12 | =============================================================== 13 | 14 | For use with PC (outside MikroTik): 15 | 16 | docker run --restart=unless-stopped \ 17 | --name speedtest -d \ #set name "speedtest" and Run container in background and print container ID 18 | -p 8080:80 \ #Host-port:Container-port to access the app inside this container via port 8080 19 | -e 'URL=https://jakarta.speedtest.telkom.net.id.prod.hosts.ooklaserver.net:8080/download?size=25000000' \ #url to download (optional) 20 | -e 'MAX_DLSIZE=1.0' \ #maximum size in MB (Megabytes) to download (optional) 21 | -e 'MIN_THRESHOLD=1.0' \ #download threshold in Mbps (Mbits per sec), to check for download speed condition (optional) 22 | -e 'CONCURENT_CONNECTION=4' \ #number of concurrent connections to speed up download tests using parallelism (optional) 23 | -e 'ALLOW_MEMORY_BUFFER=YES' \ #YES to allow memory usage if the URL does not support parallel downloading and MAX_DLSIZE under download file size, enter NO if you do not allow memory usage for streaming downloads (optional) 24 | 25 | cdhtlr/mikrotik-speedtest:amd64 #Image for amd64 architecture 26 | 27 | You can use the above example on docker-compose. 28 | 29 | =============================================================== 30 | 31 | For use as Container (inside MikroTik): 32 | 33 | Example configuration for MikroTik: 34 | 35 | /interface veth add name=veth1-speedtest address=192.168.1.2/24 gateway=192.168.1.1 36 | /interface bridge add name=bridge-docker 37 | /interface bridge port add interface=veth1-speedtest bridge=bridge-docker 38 | /ip address add address=192.168.1.1/24 interface=bridge-docker 39 | /ip firewall nat add chain=srcnat action=masquerade src-address=192.168.1.0/24 40 | /ip firewall nat add chain=dstnat action=dst-nat protocol=tcp to-addresses=192.168.1.2 to-ports=80 dst-port=8080 41 | /container envs add name=speedtest key=URL value="https://jakarta.speedtest.telkom.net.id.prod.hosts.ooklaserver.net:8080/download?size=25000000" 42 | /container envs add name=speedtest key=MAX_DLSIZE value="1.0" 43 | /container envs add name=speedtest key=MIN_THRESHOLD value="1.0" 44 | /container envs add name=speedtest key=CONCURENT_CONNECTION value="4" 45 | /container envs add name=speedtest key=ALLOW_MEMORY_BUFFER value="YES" 46 | 47 | Get an image from an external library: 48 | 49 | /container/config/set registry-url=https://registry-1.docker.io tmpdir=disk1/pull 50 | /container/add interface=veth1-speedtest root-dir=disk1/speedtest envlist=speedtest hostname=speedtest logging=yes remote-image=cdhtlr/mikrotik-speedtest:amd64 51 | 52 | or pull this image to your computer (You can use any computer with any cpu architecture). 53 | 54 | Example to pull image for ARM64 based MikroTik Router: 55 | 56 | docker pull cdhtlr/mikrotik-speedtest:arm64 57 | 58 | Save to TAR: 59 | 60 | docker save cdhtlr/mikrotik-speedtest:arm64 > speedtest.tar 61 | 62 | then upload to your MikroTik Router. 63 | 64 | Import image from computer: 65 | 66 | /container add interface=veth1-speedtest root-dir=disk1/speedtest envlist=speedtest hostname=speedtest logging=yes file=speedtest.tar 67 | 68 | Check your container list in MikroTik Router: 69 | 70 | /container/print 71 | 72 | /container/start 0 73 | 74 | 0 is your container ID, please see the list of containers you got from the /container/print command. 75 | 76 | =============================================================== 77 | 78 | How to use: 79 | 80 | Now access speedtest from your web browser (IP Router:Port). 81 | 82 | You can go to http://192.168.1.2:8080 to do download test, go to http://192.168.1.2:8080/condition to check for threshold based download speed condition or http://192.168.1.2:8080/chart to check for speedtest history chart. 83 | 84 | Finally, you can now get your actual download speedtest (in Megabits per second) using MikroTik Terminal and do some scripting like speedtest based failover. 85 | 86 | For example: 87 | 88 | :local result [tool fetch mode=http url="http://192.168.1.2:8080" output=user as-value] 89 | :local result [:put ($result->"data")] 90 | :log info "Your actual download bandwidth is $result Mbps" 91 | the example output from the script above is: Your actual download bandwidth is 3.14 Mbps 92 | 93 | or 94 | 95 | :local result [tool fetch mode=http url="http://192.168.1.2:8080/condition" output=user as-value] 96 | :local result [:put ($result->"data")] 97 | :log info "Your actual download bandwidth currently $result" 98 | the example output from the script above is: Your actual download bandwidth is currently Good 99 | 100 | You can also use MikroTik Netwatch for easier scripting like this: 101 | 102 | ![](https://raw.githubusercontent.com/cdhtlr/MikroTik-Speedtest/main/Netwatch.png "Netwatch") 103 | 104 | Response code 200 means the current download bandwidth is Good and the network status will be "up" 105 | Response code 201 means the current download bandwidth is Bad and the network status will be "down" 106 | 107 | For performance and speedtest accuracy: 108 | 109 | Larger MAX_DLSIZE can give better download speedtest results, but MAX_DLSIZE setting that is too large can cause MikroTik to fail to execute scripts due to timeout. 110 | 111 | The command line application in this container is made using Golang which is well known for its performance but it is more difficult to do manual memory management. 112 | 113 | If the memory usage in the container continues to grow and you are not comfortable with this, you can set the memory limit on the container. 114 | 115 | Memory limit that is too small can reduce CPU performance. So please set the memory limit wisely. 116 | 117 | Image size efficiency: 118 | 119 | ![](https://raw.githubusercontent.com/cdhtlr/MikroTik-Speedtest/main/Dive-in_result.png "Dive-in efficiency test result") 120 | 121 | Copyright notice: 122 | 123 | The source code of this program is similar to GoParallelDownload and the chart used in this Docker Image is made by go-echarts. 124 | --------------------------------------------------------------------------------