2023-01-27 21:37:20 +00:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
2021-07-13 05:04:16 +00:00
|
|
|
|
|
|
|
// The derpprobe binary probes derpers.
|
2023-01-27 14:49:50 +00:00
|
|
|
package main
|
2021-07-13 05:04:16 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"html"
|
|
|
|
"io"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"sort"
|
|
|
|
"time"
|
|
|
|
|
2023-01-27 14:49:50 +00:00
|
|
|
"tailscale.com/prober"
|
|
|
|
"tailscale.com/tsweb"
|
2024-04-02 18:55:33 +00:00
|
|
|
"tailscale.com/version"
|
2021-07-13 05:04:16 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2024-02-28 20:27:44 +00:00
|
|
|
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
|
2024-04-02 18:55:33 +00:00
|
|
|
versionFlag = flag.Bool("version", false, "print version and exit")
|
2024-02-28 20:27:44 +00:00
|
|
|
listen = flag.String("listen", ":8030", "HTTP listen address")
|
|
|
|
probeOnce = flag.Bool("once", false, "probe once and print results, then exit; ignores the listen flag")
|
|
|
|
spread = flag.Bool("spread", true, "whether to spread probing over time")
|
|
|
|
interval = flag.Duration("interval", 15*time.Second, "probe interval")
|
|
|
|
meshInterval = flag.Duration("mesh-interval", 15*time.Second, "mesh probe interval")
|
|
|
|
stunInterval = flag.Duration("stun-interval", 15*time.Second, "STUN probe interval")
|
|
|
|
tlsInterval = flag.Duration("tls-interval", 15*time.Second, "TLS probe interval")
|
|
|
|
bwInterval = flag.Duration("bw-interval", 0, "bandwidth probe interval (0 = no bandwidth probing)")
|
|
|
|
bwSize = flag.Int64("bw-probe-size-bytes", 1_000_000, "bandwidth probe size")
|
2021-07-13 05:04:16 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
flag.Parse()
|
2024-04-02 18:55:33 +00:00
|
|
|
if *versionFlag {
|
|
|
|
fmt.Println(version.Long())
|
|
|
|
return
|
|
|
|
}
|
2022-03-01 04:13:33 +00:00
|
|
|
|
2023-04-03 10:35:12 +00:00
|
|
|
p := prober.New().WithSpread(*spread).WithOnce(*probeOnce).WithMetricNamespace("derpprobe")
|
2024-02-28 20:27:44 +00:00
|
|
|
opts := []prober.DERPOpt{
|
|
|
|
prober.WithMeshProbing(*meshInterval),
|
|
|
|
prober.WithSTUNProbing(*stunInterval),
|
|
|
|
prober.WithTLSProbing(*tlsInterval),
|
|
|
|
}
|
|
|
|
if *bwInterval > 0 {
|
|
|
|
opts = append(opts, prober.WithBandwidthProbing(*bwInterval, *bwSize))
|
|
|
|
}
|
|
|
|
dp, err := prober.DERP(p, *derpMapURL, opts...)
|
2023-01-27 14:49:50 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
p.Run("derpmap-probe", *interval, nil, dp.ProbeMap)
|
2022-03-01 04:13:33 +00:00
|
|
|
|
2022-11-24 00:09:24 +00:00
|
|
|
if *probeOnce {
|
2023-01-27 14:49:50 +00:00
|
|
|
log.Printf("Waiting for all probes (may take up to 1m)")
|
|
|
|
p.Wait()
|
|
|
|
|
|
|
|
st := getOverallStatus(p)
|
2022-11-24 00:09:24 +00:00
|
|
|
for _, s := range st.good {
|
|
|
|
log.Printf("good: %s", s)
|
|
|
|
}
|
|
|
|
for _, s := range st.bad {
|
|
|
|
log.Printf("bad: %s", s)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-01-27 14:49:50 +00:00
|
|
|
mux := http.NewServeMux()
|
|
|
|
tsweb.Debugger(mux)
|
|
|
|
mux.HandleFunc("/", http.HandlerFunc(serveFunc(p)))
|
2024-02-28 20:27:44 +00:00
|
|
|
log.Printf("Listening on %s", *listen)
|
2023-01-27 14:49:50 +00:00
|
|
|
log.Fatal(http.ListenAndServe(*listen, mux))
|
2022-01-26 19:58:56 +00:00
|
|
|
}
|
|
|
|
|
2021-07-13 05:04:16 +00:00
|
|
|
type overallStatus struct {
|
|
|
|
good, bad []string
|
|
|
|
}
|
|
|
|
|
2022-03-16 23:27:57 +00:00
|
|
|
func (st *overallStatus) addBadf(format string, a ...any) {
|
2021-07-13 05:04:16 +00:00
|
|
|
st.bad = append(st.bad, fmt.Sprintf(format, a...))
|
|
|
|
}
|
|
|
|
|
2022-03-16 23:27:57 +00:00
|
|
|
func (st *overallStatus) addGoodf(format string, a ...any) {
|
2021-07-13 05:04:16 +00:00
|
|
|
st.good = append(st.good, fmt.Sprintf(format, a...))
|
|
|
|
}
|
|
|
|
|
2023-01-27 14:49:50 +00:00
|
|
|
func getOverallStatus(p *prober.Prober) (o overallStatus) {
|
|
|
|
for p, i := range p.ProbeInfo() {
|
|
|
|
if i.End.IsZero() {
|
|
|
|
// Do not show probes that have not finished yet.
|
2022-01-26 19:58:56 +00:00
|
|
|
continue
|
|
|
|
}
|
2023-01-27 14:49:50 +00:00
|
|
|
if i.Result {
|
|
|
|
o.addGoodf("%s: %s", p, i.Latency)
|
|
|
|
} else {
|
|
|
|
o.addBadf("%s: %s", p, i.Error)
|
2022-01-26 19:58:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-27 14:49:50 +00:00
|
|
|
sort.Strings(o.bad)
|
|
|
|
sort.Strings(o.good)
|
2021-07-13 05:04:16 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-01-27 14:49:50 +00:00
|
|
|
func serveFunc(p *prober.Prober) func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
st := getOverallStatus(p)
|
|
|
|
summary := "All good"
|
|
|
|
if (float64(len(st.bad)) / float64(len(st.bad)+len(st.good))) > 0.25 {
|
|
|
|
// Returning a 500 allows monitoring this server externally and configuring
|
|
|
|
// an alert on HTTP response code.
|
|
|
|
w.WriteHeader(500)
|
|
|
|
summary = fmt.Sprintf("%d problems", len(st.bad))
|
2021-07-13 05:04:16 +00:00
|
|
|
}
|
2022-01-26 19:58:56 +00:00
|
|
|
|
2023-01-27 14:49:50 +00:00
|
|
|
io.WriteString(w, "<html><head><style>.bad { font-weight: bold; color: #700; }</style></head>\n")
|
|
|
|
fmt.Fprintf(w, "<body><h1>derp probe</h1>\n%s:<ul>", summary)
|
|
|
|
for _, s := range st.bad {
|
|
|
|
fmt.Fprintf(w, "<li class=bad>%s</li>\n", html.EscapeString(s))
|
2021-07-13 05:04:16 +00:00
|
|
|
}
|
2023-01-27 14:49:50 +00:00
|
|
|
for _, s := range st.good {
|
|
|
|
fmt.Fprintf(w, "<li>%s</li>\n", html.EscapeString(s))
|
2021-07-13 05:04:16 +00:00
|
|
|
}
|
2023-01-27 14:49:50 +00:00
|
|
|
io.WriteString(w, "</ul></body></html>\n")
|
2021-07-13 05:04:16 +00:00
|
|
|
}
|
|
|
|
}
|