├── .gitignore ├── Makefile ├── .forgejo └── workflows │ └── test.yml ├── Dockerfile ├── go.mod ├── README.md ├── LICENSE ├── go.sum └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.a 3 | *.so 4 | _obj 5 | _test 6 | *.[568vq] 7 | [568vq].out 8 | *.cgo1.go 9 | *.cgo2.c 10 | _cgo_defun.c 11 | _cgo_gotypes.go 12 | _cgo_export.* 13 | _testmain.go 14 | *.exe 15 | *.test 16 | *.prof 17 | cmdns-cli 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SOURCES := $(wildcard *.go) 2 | 3 | all: cmdns-cli 4 | 5 | fmt: format 6 | 7 | clean: 8 | rm -f cmdns-cli 9 | 10 | format: 11 | gofmt -w *.go 12 | sed -i -e 's% % %g' *.go 13 | 14 | cmdns-cli: $(SOURCES) 15 | go build -v -x 16 | -------------------------------------------------------------------------------- /.forgejo/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: pull_request 3 | jobs: 4 | test: 5 | runs-on: docker 6 | steps: 7 | - uses: actions/checkout@v4 8 | - uses: actions/setup-go@v4 9 | with: 10 | go-version: '>=1.22.0' 11 | - run: go build -v 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Usage: 2 | # 3 | # docker build -t cmdns-cli . 4 | # docker run --rm -ti cmdns-cli -help 5 | 6 | FROM golang:1.18-alpine 7 | 8 | WORKDIR /app 9 | 10 | COPY go.mod ./ 11 | COPY go.sum ./ 12 | RUN go mod download 13 | 14 | COPY *.go ./ 15 | 16 | RUN go build -o /usr/bin/cmdns-cli 17 | 18 | ENTRYPOINT [ "/usr/bin/cmdns-cli" ] 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module codeberg.org/DNS-OARC/cmdns-cli 2 | 3 | go 1.23 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/gorilla/websocket v1.5.3 9 | github.com/miekg/dns v1.1.62 10 | ) 11 | 12 | require ( 13 | golang.org/x/mod v0.18.0 // indirect 14 | golang.org/x/net v0.27.0 // indirect 15 | golang.org/x/sync v0.7.0 // indirect 16 | golang.org/x/sys v0.22.0 // indirect 17 | golang.org/x/tools v0.22.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Check My DNS Client 2 | 3 | This [Go](https://golang.org) program is a command line interface for 4 | [Check My DNS](https://cmdns.dev.dns-oarc.net) and will test the system 5 | configured DNS resolver. All output on stdout is streamed JSON, each 6 | object is separated with a new line. Status and errors are outputted on 7 | stderr. 8 | 9 | Use CTRL-C to break the program when it's done (or `-done`, see `-help`), 10 | it does not exit on it's own because you can still get results after all 11 | checks are done. 12 | 13 | ## Install 14 | 15 | Requires Go v1.18+ 16 | 17 | ```shell 18 | go install codeberg.org/DNS-OARC/cmdns-cli@latest 19 | ``` 20 | 21 | ## License 22 | 23 | ``` 24 | MIT License 25 | 26 | Copyright (c) 2022 OARC, Inc. 27 | ``` 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OARC, Inc. 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 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 2 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 3 | github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= 4 | github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 5 | golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= 6 | golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 7 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 8 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 9 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 10 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 11 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 12 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 13 | golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= 14 | golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= 15 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "os/signal" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/miekg/dns" 17 | 18 | "github.com/gorilla/websocket" 19 | ) 20 | 21 | type CheckInfoMsg struct { 22 | Name string `json:"name,omitempty"` 23 | Display string `json:"display,omitempty"` 24 | Description string `json:"desc,omitempty"` 25 | Category string `json:"cat,omitempty"` 26 | Score int `json:"score,omitempty"` 27 | Meta bool `json:"meta,omitempty"` 28 | Async bool `json:"async,omitempty"` 29 | Default bool `json:"default,omitempty"` 30 | } 31 | 32 | type ListMsg struct { 33 | Checks []string `json:"checks,omitempty"` 34 | 35 | GetInfos bool `json:"info,omitempty"` 36 | CheckInfos []CheckInfoMsg `json:"infos,omitempty"` 37 | } 38 | 39 | type CheckMsg struct { 40 | All bool `json:"all"` 41 | } 42 | 43 | type PrepareMsg struct { 44 | Done bool `json:"done"` 45 | Total int `json:"total"` 46 | 47 | Id string `json:"id,omitempty"` 48 | Name string `json:"name,omitempty"` 49 | Description string `json:"desc,omitempty"` 50 | Category string `json:"cat,omitempty"` 51 | Score int `json:"score,omitempty"` 52 | Meta bool `json:"meta,omitempty"` 53 | 54 | Checks []string `json:"checks,omitempty"` 55 | } 56 | 57 | type ProgressMsg struct { 58 | For string `json:"for"` 59 | At int `json:"at"` 60 | Failure int `json:"fail"` 61 | Success int `json:"succ"` 62 | } 63 | 64 | type CompleteMsg struct { 65 | Id string `json:"id"` 66 | Success bool `json:"succ"` 67 | Message string `json:"msg,omitempty"` 68 | } 69 | 70 | type RatingMsg struct { 71 | Text string `json:"text"` 72 | Class string `json:"class"` 73 | } 74 | 75 | type NetworkMsg struct { 76 | Port int `json:"port"` 77 | IpVersion int `json:"ipv"` 78 | Protocol string `json:"proto"` 79 | DnsId int `json:"id"` 80 | } 81 | 82 | type RrMsg struct { 83 | Name string `json:"n"` 84 | Rrtype string `json:"t"` 85 | Class string `json:"c"` 86 | Ttl int `json:"l,omitempty"` 87 | Rdata string `json:"rdata,omitempty"` 88 | } 89 | 90 | type DnsMsg struct { 91 | CheckId string `json:"cid"` 92 | UnixNano int64 `json:"unts"` 93 | Source string `json:"src"` 94 | Destination string `json:"dst"` 95 | Port int `json:"port"` 96 | Protocol string `json:"proto"` 97 | IpVersion int `json:"ipv"` 98 | 99 | DnsId int `json:"id"` 100 | Qr bool `json:"qr,omitempty"` 101 | Opcode string `json:"op,omitempty"` 102 | Aa bool `json:"aa,omitempty"` 103 | Tc bool `json:"tc,omitempty"` 104 | Rd bool `json:"rd,omitempty"` 105 | Ra bool `json:"ra,omitempty"` 106 | Z bool `json:"z,omitempty"` 107 | Ad bool `json:"ad,omitempty"` 108 | Cd bool `json:"cd,omitempty"` 109 | Do bool `json:"do,omitempty"` 110 | Rcode string `json:"rc,omitempty"` 111 | 112 | Questions []*RrMsg `json:"q,omitempty"` 113 | Answers []*RrMsg `json:"ans,omitempty"` 114 | Authorities []*RrMsg `json:"ns,omitempty"` 115 | Additionals []*RrMsg `json:"add,omitempty"` 116 | } 117 | 118 | type WhoisMsg struct { 119 | Rir string `json:"rir"` 120 | Netname string `json:"nn"` 121 | Ip string `json:"ip"` 122 | } 123 | 124 | type LookupMsg struct { 125 | Id string `json:"id"` 126 | Dn string `json:"dn"` 127 | Success bool `json:"ok"` 128 | Error string `json:"err,omitempty"` 129 | } 130 | 131 | type UserAgentMsg struct { 132 | Text string `json:"text"` 133 | } 134 | 135 | type ClientMsg struct { 136 | List *ListMsg `json:"list,omitempty"` 137 | Check *CheckMsg `json:"check,omitempty"` 138 | Prepare *PrepareMsg `json:"prepare,omitempty"` 139 | Progress *ProgressMsg `json:"progress,omitempty"` 140 | Complete *CompleteMsg `json:"complete,omitempty"` 141 | Rating *RatingMsg `json:"rating,omitempty"` 142 | Network *NetworkMsg `json:"network,omitempty"` 143 | Whois *WhoisMsg `json:"whois,omitempty"` 144 | Lookup *LookupMsg `json:"lookup,omitempty"` 145 | Dns *DnsMsg `json:"dns,omitempty"` 146 | UserAgent *UserAgentMsg `json:"ua,omitempty"` 147 | } 148 | 149 | var addr = flag.String("addr", "cmdns.dev.dns-oarc.net", "websocket address") 150 | var exitWhenDone = flag.Bool("done", false, "Exit when done") 151 | var res = flag.String("res", "", "resolver IP:port to use (default system)") 152 | var checks = flag.String("checks", "", "comma separated list of checks to run, ex trans_tcp,feat_qnmini") 153 | var listChecks = flag.Bool("list-checks", false, "Get a list of checks from the server and exit") 154 | var listCheckInfos = flag.Bool("list-check-infos", false, "Get a detailed list of checks from the server and exit") 155 | var noSSL = flag.Bool("no-ssl", false, "Use plain ws://") 156 | var port = flag.String("port", "", "Custom port for websocket") 157 | var resTimeout = flag.Int("res-timeout", 5, "resolver lookup timeout in seconds") 158 | var dumpDns = flag.Bool("dump-dns", false, "Dump DNS to/from resolver when using -res (stderr)") 159 | 160 | var c *websocket.Conn 161 | var cmux sync.Mutex 162 | 163 | func send(m *ClientMsg) error { 164 | b, err := json.Marshal(m) 165 | if err != nil { 166 | log.Println("send json.Marshal():", err) 167 | return err 168 | } 169 | 170 | cmux.Lock() 171 | defer cmux.Unlock() 172 | 173 | err = c.WriteMessage(websocket.TextMessage, b) 174 | if err != nil { 175 | log.Println("send conn.WriteMessage():", err) 176 | return err 177 | } 178 | fmt.Println("{\"send\":" + string(b) + "}") 179 | return nil 180 | } 181 | 182 | func main() { 183 | flag.Parse() 184 | log.SetFlags(0) 185 | log.SetOutput(os.Stderr) 186 | 187 | interrupt := make(chan os.Signal, 1) 188 | signal.Notify(interrupt, os.Interrupt) 189 | 190 | if *port == "" { 191 | if *noSSL { 192 | *port = "80" 193 | } else { 194 | *port = "443" 195 | } 196 | } 197 | 198 | u := url.URL{Scheme: "wss", Host: *addr + ":" + *port, Path: "/ws/"} 199 | if *noSSL { 200 | u = url.URL{Scheme: "ws", Host: *addr + ":" + *port, Path: "/ws/"} 201 | } 202 | log.Printf("connecting to %s", u.String()) 203 | 204 | var err error 205 | c, _, err = websocket.DefaultDialer.Dial(u.String(), nil) 206 | if err != nil { 207 | log.Fatal("dial:", err) 208 | } 209 | defer c.Close() 210 | 211 | done := make(chan struct{}) 212 | lookup := make(chan *ClientMsg) 213 | 214 | go func() { 215 | for { 216 | select { 217 | case <-done: 218 | return 219 | case m, ok := <-lookup: 220 | if !ok { 221 | return 222 | } 223 | go func() { 224 | if *res != "" { 225 | c := new(dns.Client) 226 | c.Timeout = time.Second * time.Duration(*resTimeout) 227 | 228 | q := &dns.Msg{} 229 | q.SetQuestion(dns.Fqdn(m.Lookup.Dn), dns.TypeA) 230 | if *dumpDns { 231 | log.Println(q.String()) 232 | } 233 | a, _, err := c.Exchange(q, *res) 234 | if err != nil { 235 | m.Lookup.Success = false 236 | m.Lookup.Error = fmt.Sprintf("%v", err) 237 | } else { 238 | if *dumpDns { 239 | log.Println(a.String()) 240 | } 241 | ok := false 242 | if len(a.Answer) > 0 { 243 | _, ok = a.Answer[0].(*dns.A) 244 | } 245 | if ok { 246 | m.Lookup.Success = true 247 | } else { 248 | q = &dns.Msg{} 249 | q.SetQuestion(dns.Fqdn(m.Lookup.Dn), dns.TypeAAAA) 250 | a, _, err = c.Exchange(q, *res) 251 | if err != nil { 252 | m.Lookup.Success = false 253 | m.Lookup.Error = fmt.Sprintf("%v", err) 254 | } else { 255 | if len(a.Answer) < 1 { 256 | m.Lookup.Success = false 257 | m.Lookup.Error = "no answer records" 258 | } else if _, ok := a.Answer[0].(*dns.AAAA); ok { 259 | m.Lookup.Success = true 260 | } else { 261 | m.Lookup.Success = false 262 | m.Lookup.Error = "no A/AAAA record found in answer" 263 | } 264 | } 265 | } 266 | } 267 | } else { 268 | _, err := http.Get("https://" + m.Lookup.Dn + "/dot.png") 269 | if err != nil { 270 | m.Lookup.Success = false 271 | m.Lookup.Error = fmt.Sprintf("%v", err) 272 | } else { 273 | m.Lookup.Success = true 274 | } 275 | } 276 | if err = send(m); err != nil { 277 | return 278 | } 279 | }() 280 | } 281 | } 282 | }() 283 | 284 | go func() { 285 | defer close(done) 286 | defer close(lookup) 287 | 288 | prepareDone := 0 289 | prepareTotal := 0 290 | 291 | for { 292 | _, message, err := c.ReadMessage() 293 | if err != nil { 294 | log.Println("read:", err) 295 | return 296 | } 297 | lines := strings.Split(string(message), "\n") 298 | for _, line := range lines { 299 | fmt.Println(line) 300 | 301 | var m ClientMsg 302 | if err = json.Unmarshal([]byte(line), &m); err != nil { 303 | log.Println("read json.Unmarshal():", err) 304 | return 305 | } else { 306 | if m.List != nil { 307 | err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 308 | if err != nil { 309 | log.Println("write close:", err) 310 | } 311 | return 312 | } 313 | if m.Prepare != nil { 314 | if !m.Prepare.Done { 315 | prepareTotal = m.Prepare.Total 316 | } else { 317 | prepareDone++ 318 | } 319 | if prepareTotal > 0 && prepareTotal == prepareDone { 320 | if err = send(&ClientMsg{Check: &CheckMsg{All: true}}); err != nil { 321 | return 322 | } 323 | } 324 | } 325 | if m.Lookup != nil { 326 | func() { 327 | defer func() { 328 | recover() 329 | }() 330 | lookup <- &m 331 | }() 332 | } 333 | if m.Rating != nil && *exitWhenDone == true { 334 | return 335 | } 336 | } 337 | } 338 | } 339 | }() 340 | 341 | if *listChecks { 342 | if err = send(&ClientMsg{List: &ListMsg{}}); err != nil { 343 | return 344 | } 345 | } else if *listCheckInfos { 346 | if err = send(&ClientMsg{List: &ListMsg{GetInfos: true}}); err != nil { 347 | return 348 | } 349 | } else { 350 | if *checks == "" { 351 | if err = send(&ClientMsg{Prepare: &PrepareMsg{}}); err != nil { 352 | return 353 | } 354 | } else { 355 | if err = send(&ClientMsg{Prepare: &PrepareMsg{Checks: strings.Split(*checks, ",")}}); err != nil { 356 | return 357 | } 358 | } 359 | } 360 | 361 | for { 362 | select { 363 | case <-interrupt: 364 | log.Println("interrupt") 365 | err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 366 | if err != nil { 367 | log.Println("write close:", err) 368 | return 369 | } 370 | select { 371 | case <-done: 372 | case <-time.After(time.Second): 373 | } 374 | c.Close() 375 | return 376 | case <-done: 377 | c.Close() 378 | return 379 | } 380 | } 381 | } 382 | --------------------------------------------------------------------------------