cmd/tailscale/cli: add tailscale exit-node suggest --force-probe
Add a new `--force-probe` flag to `tailscale exit-node suggest` that waits for a routecheck.Refresh to finish before suggesting an exit node. This flag is currently hidden from the help text, but this flag is a hint to the user that exit-node suggestions are based on routecheck reachability reports. Updates #17366 Updates tailscale/corp#33033 Signed-off-by: Simon Law <sfllaw@tailscale.com>
This commit is contained in:
@@ -7,6 +7,7 @@ package localapi
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -1814,16 +1815,68 @@ func dnsMessageTypeForString(s string) (t dnsmessage.Type, err error) {
|
||||
return 0, errors.New("unknown DNS message type: " + s)
|
||||
}
|
||||
|
||||
// HookRouteCheckRefresh is the hook to request a refresh of the routecheck Report
|
||||
// for this LocalBackend.
|
||||
// It is used by serveSuggestExitNode to probe for reachable exit nodes
|
||||
// before it makes a suggestion.
|
||||
var HookRouteCheckRefresh feature.Hook[func(*ipnlocal.LocalBackend, context.Context, time.Duration) error]
|
||||
|
||||
// serveSuggestExitNode serves a POST endpoint for returning a suggested exit node.
|
||||
// If the probe query parameter is true,
|
||||
// then a new routecheck report will be probed
|
||||
// so that the suggested exit node isn’t based on stale reachability data.
|
||||
// Combined with probe=true:
|
||||
// if the timeout query parameter is 0, any probes will immediately timeout;
|
||||
// if the timeout is positive, probes will take that duration before timing out;
|
||||
// if the timeout is negative, probes will use the default routecheck timeout.
|
||||
func (h *Handler) serveSuggestExitNode(w http.ResponseWriter, r *http.Request) {
|
||||
if !buildfeatures.HasUseExitNode {
|
||||
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.GET {
|
||||
http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
|
||||
|
||||
switch r.Method {
|
||||
case httpm.GET:
|
||||
// The GET method is inappropriate because it isn’t cacheable.
|
||||
// However, we retain it for backwards compatibility.
|
||||
//
|
||||
// The original implementation used to only allow GET requests,
|
||||
// but since this endpoint powers the `tailscale exit-node suggest` command
|
||||
// whose results can change based on the shape of the network,
|
||||
// it was never a proper GET method in the first place.
|
||||
//
|
||||
// Now that the suggestions are backed by routecheck
|
||||
// and we provide the user the ability to trigger a probe,
|
||||
// this is obviously a POST method because it has side-effects.
|
||||
// However, we don’t want to break existing clients,
|
||||
// so we silently support the old GET method
|
||||
// without the extra query parameters.
|
||||
//
|
||||
// This is also why the default case returns a "want POST" error
|
||||
// and not an "only POST allowed" like the other endpoints.
|
||||
// We still accept GET requests but we don’t want them.
|
||||
case httpm.POST:
|
||||
if !def.Bool(r.FormValue("probe"), false) {
|
||||
break
|
||||
}
|
||||
timeout := def.Duration(r.FormValue("timeout"), -1)
|
||||
|
||||
// Force routecheck to probe for a new report
|
||||
// and use it to suggest an exit node.
|
||||
routecheckRefresh := HookRouteCheckRefresh.GetOrNil()
|
||||
if routecheckRefresh == nil {
|
||||
break
|
||||
}
|
||||
if err := routecheckRefresh(h.b, r.Context(), timeout); err != nil {
|
||||
WriteErrorJSON(w, err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
// Discourage the GET method:
|
||||
http.Error(w, "want POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := h.b.SuggestExitNode()
|
||||
if err != nil {
|
||||
WriteErrorJSON(w, err)
|
||||
|
||||
Reference in New Issue
Block a user