├── 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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------