diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index e566fc864..4da0c2c7b 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -1302,3 +1302,59 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error { return nil } + +// tsmpPing sends a Ping to pr.IP, and sends an http request back to pr.URL +// with ping response data. +func tsmpPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger) error { + var err error + if pr.URL == "" { + return errors.New("invalid PingRequest with no URL") + } + if pr.IP.IsZero() { + return fmt.Errorf("PingRequest with no proper IP got %v", pr.IP) + } + if !strings.Contains(pr.Types, "TSMP") { + return fmt.Errorf("PingRequest with no TSMP in Types, got : %v", pr.Types) + } + + now := time.Now() + pinger.Ping(pr.IP, true, func(res *ipnstate.PingResult) { + // Currently does not check for error since we just return if it fails. + err = postPingResult(now, logf, c, pr, res) + }) + return err +} + +func postPingResult(now time.Time, logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, res *ipnstate.PingResult) error { + if res.Err != "" { + return errors.New(res.Err) + } + duration := time.Since(now) + if pr.Log { + logf("TSMP ping to %v completed in %v seconds. pinger.Ping took %v seconds", pr.IP, res.LatencySeconds, duration.Seconds()) + } + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + jsonPingRes, err := json.Marshal(res) + if err != nil { + return err + } + // Send the results of the Ping, back to control URL. + req, err := http.NewRequestWithContext(ctx, "POST", pr.URL, bytes.NewBuffer(jsonPingRes)) + if err != nil { + return fmt.Errorf("http.NewRequestWithContext(%q): %w", pr.URL, err) + } + if pr.Log { + logf("tsmpPing: sending ping results to %v ...", pr.URL) + } + t0 := time.Now() + _, err = c.Do(req) + d := time.Since(t0).Round(time.Millisecond) + if err != nil { + return fmt.Errorf("tsmpPing error: %w to %v (after %v)", err, pr.URL, d) + } else if pr.Log { + logf("tsmpPing complete to %v (after %v)", pr.URL, d) + } + return nil +} diff --git a/control/controlclient/direct_test.go b/control/controlclient/direct_test.go index 01605248b..206ba5e49 100644 --- a/control/controlclient/direct_test.go +++ b/control/controlclient/direct_test.go @@ -6,9 +6,13 @@ import ( "encoding/json" + "net/http" + "net/http/httptest" "testing" + "time" "inet.af/netaddr" + "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" "tailscale.com/types/wgkey" ) @@ -103,3 +107,56 @@ func TestNewHostinfo(t *testing.T) { } t.Logf("Got: %s", j) } + +func TestTsmpPing(t *testing.T) { + hi := NewHostinfo() + ni := tailcfg.NetInfo{LinkType: "wired"} + hi.NetInfo = &ni + + key, err := wgkey.NewPrivate() + if err != nil { + t.Error(err) + } + opts := Options{ + ServerURL: "https://example.com", + Hostinfo: hi, + GetMachinePrivateKey: func() (wgkey.Private, error) { + return key, nil + }, + } + + c, err := NewDirect(opts) + if err != nil { + t.Fatal(err) + } + + pingRes := &ipnstate.PingResult{ + IP: "123.456.7890", + Err: "", + NodeName: "testnode", + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + body := new(ipnstate.PingResult) + if err := json.NewDecoder(r.Body).Decode(body); err != nil { + t.Fatal(err) + } + if pingRes.IP != body.IP { + t.Fatalf("PingResult did not have the correct IP : got %v, expected : %v", body.IP, pingRes.IP) + } + w.WriteHeader(200) + })) + defer ts.Close() + + now := time.Now() + + pr := &tailcfg.PingRequest{ + URL: ts.URL, + } + + err = postPingResult(now, t.Logf, c.httpc, pr, pingRes) + if err != nil { + t.Fatal(err) + } +} diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index c0c27bfa0..9d54d9a6f 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -851,6 +851,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error { DiscoPublicKey: discoPublic, DebugFlags: debugFlags, LinkMonitor: b.e.GetLinkMonitor(), + Pinger: b.e, // Don't warn about broken Linux IP forwading when // netstack is being used. diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 0c381218e..a02761719 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -897,19 +897,30 @@ type DNSRecord struct { Value string } -// PingRequest is a request to send an HTTP request to prove the +// PingRequest with no IP and Types is a request to send an HTTP request to prove the // long-polling client is still connected. +// PingRequest with Types and IP, will send a ping to the IP and send a +// POST request to the URL to prove that the ping succeeded. type PingRequest struct { // URL is the URL to send a HEAD request to. // It will be a unique URL each time. No auth headers are necessary. // // If the client sees multiple PingRequests with the same URL, // subsequent ones should be ignored. + // If Types and IP are defined, then URL is the URL to send a POST request to. URL string // Log is whether to log about this ping in the success case. // For failure cases, the client will log regardless. Log bool `json:",omitempty"` + + // Types is the types of ping that is initiated. Can be TSMP, ICMP or disco. + // Types will be comma separated, such as TSMP,disco. + Types string + + // IP is the ping target. + // It is used in TSMP pings, if IP is invalid or empty then do a HEAD request to the URL. + IP netaddr.IP } type MapResponse struct {