Skip to content

Commit b946afc

Browse files
Return manual resolution when access is denied to LocalBackend (#60)
This PR returns an explicit json error with status 403 for when LocalBackend returns an AccessDenied error on SetServe. The UI could then catch the error and display the command to the user as a manual resolution
1 parent f690ea4 commit b946afc

File tree

1 file changed

+39
-2
lines changed

1 file changed

+39
-2
lines changed

tsrelay/main.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ const (
5050
// Offline can mean a user is not logged in
5151
// or is logged in but their key has expired.
5252
Offline = "OFFLINE"
53+
// RequiredSudo for when LocalBackend is run
54+
// with sudo but tsrelay is not
55+
RequiredSudo = "REQUIRES_SUDO"
5356
)
5457

5558
func main() {
@@ -148,13 +151,22 @@ type serveStatus struct {
148151

149152
// RelayError is a wrapper for Error
150153
type RelayError struct {
151-
Errors []Error
154+
statusCode int
155+
Errors []Error
156+
}
157+
158+
// Error implements error. It returns a
159+
// static string as it is only needed to be
160+
// used for programatic type assertion.
161+
func (RelayError) Error() string {
162+
return "relay error"
152163
}
153164

154165
// Error is a programmable error returned
155166
// to the typescript client
156167
type Error struct {
157-
Type string `json:",omitempty"`
168+
Type string `json:",omitempty"`
169+
Command string `json:",omitempty"`
158170
}
159171

160172
type peerStatus struct {
@@ -208,6 +220,12 @@ func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
208220
switch r.Method {
209221
case http.MethodPost:
210222
if err := h.createServe(r.Context(), r.Body); err != nil {
223+
var re RelayError
224+
if errors.As(err, &re) {
225+
w.WriteHeader(re.statusCode)
226+
json.NewEncoder(w).Encode(re)
227+
return
228+
}
211229
h.l.Println("error creating serve:", err)
212230
http.Error(w, err.Error(), 500)
213231
return
@@ -441,6 +459,8 @@ func (h *httpHandler) deleteServe(ctx context.Context, body io.Reader) error {
441459
return nil
442460
}
443461

462+
// createServe is the programtic equivalent of "tailscale serve --set-raw"
463+
// it returns the config as json in case of an error.
444464
func (h *httpHandler) createServe(ctx context.Context, body io.Reader) error {
445465
var req serveRequest
446466
err := json.NewDecoder(body).Decode(&req)
@@ -466,6 +486,23 @@ func (h *httpHandler) createServe(ctx context.Context, body io.Reader) error {
466486
}
467487
err = h.lc.SetServeConfig(ctx, sc)
468488
if err != nil {
489+
if tailscale.IsAccessDeniedError(err) {
490+
cfgJSON, err := json.Marshal(sc)
491+
if err != nil {
492+
return fmt.Errorf("error marshaling own config: %w", err)
493+
}
494+
re := RelayError{
495+
statusCode: http.StatusForbidden,
496+
Errors: []Error{{
497+
Type: RequiredSudo,
498+
Command: fmt.Sprintf(`echo %s | sudo tailscale serve --set-raw`, cfgJSON),
499+
}},
500+
}
501+
return re
502+
}
503+
if err != nil {
504+
return fmt.Errorf("error marshaling config: %w", err)
505+
}
469506
return fmt.Errorf("error setting serve config: %w", err)
470507
}
471508
return nil

0 commit comments

Comments
 (0)