-
Notifications
You must be signed in to change notification settings - Fork 238
Protect metadata shutdown endpoint #422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -20,13 +20,16 @@ | |||||||||||||||||||
| import ( | ||||||||||||||||||||
| "bytes" | ||||||||||||||||||||
| "context" | ||||||||||||||||||||
| "crypto/rand" | ||||||||||||||||||||
| "encoding/hex" | ||||||||||||||||||||
| "encoding/json" | ||||||||||||||||||||
| "errors" | ||||||||||||||||||||
| "fmt" | ||||||||||||||||||||
| "io" | ||||||||||||||||||||
| "net" | ||||||||||||||||||||
| "net/http" | ||||||||||||||||||||
| "os" | ||||||||||||||||||||
| "path/filepath" | ||||||||||||||||||||
| "strings" | ||||||||||||||||||||
| "sync" | ||||||||||||||||||||
| "syscall" | ||||||||||||||||||||
|
|
@@ -142,6 +145,9 @@ | |||||||||||||||||||
| healthMu sync.Mutex | ||||||||||||||||||||
| restartCount int | ||||||||||||||||||||
| abandoned bool | ||||||||||||||||||||
|
|
||||||||||||||||||||
| shutdownToken string | ||||||||||||||||||||
| shutdownTokenPath string | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // authToken returns the current auth token, preferring the dynamic TokenFunc | ||||||||||||||||||||
|
|
@@ -200,10 +206,6 @@ | |||||||||||||||||||
| s.cancel = cancel | ||||||||||||||||||||
|
|
||||||||||||||||||||
| addr := fmt.Sprintf("127.0.0.1:%d", s.config.Port) | ||||||||||||||||||||
| s.srv = &http.Server{ | ||||||||||||||||||||
| Addr: addr, | ||||||||||||||||||||
| Handler: s.buildMux(), | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ln, err := net.Listen("tcp", addr) | ||||||||||||||||||||
| if err != nil && errors.Is(err, syscall.EADDRINUSE) { | ||||||||||||||||||||
|
|
@@ -241,6 +243,16 @@ | |||||||||||||||||||
| return fmt.Errorf("metadata server listen: %w", err) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if err := s.ensureShutdownToken(); err != nil { | ||||||||||||||||||||
| cancel() | ||||||||||||||||||||
| ln.Close() | ||||||||||||||||||||
| return fmt.Errorf("metadata server shutdown token: %w", err) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| s.srv = &http.Server{ | ||||||||||||||||||||
| Addr: addr, | ||||||||||||||||||||
| Handler: s.buildMux(), | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // Track this server so a future Start() can forcefully close it. | ||||||||||||||||||||
| activeServerMu.Lock() | ||||||||||||||||||||
| activeServer = s | ||||||||||||||||||||
|
|
@@ -352,6 +364,11 @@ | |||||||||||||||||||
| s.srv.Shutdown(shutdownCtx) | ||||||||||||||||||||
| shutdownCancel() | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if s.shutdownTokenPath != "" { | ||||||||||||||||||||
| if err := os.Remove(s.shutdownTokenPath); err != nil && !errors.Is(err, os.ErrNotExist) { | ||||||||||||||||||||
| log.Debug("Failed to remove metadata shutdown token file %s: %v", s.shutdownTokenPath, err) | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if s.cancel != nil { | ||||||||||||||||||||
| s.cancel() | ||||||||||||||||||||
|
|
@@ -369,6 +386,12 @@ | |||||||||||||||||||
| return | ||||||||||||||||||||
| } | ||||||||||||||||||||
| req.Header.Set("Metadata-Flavor", "Google") | ||||||||||||||||||||
| token, err := os.ReadFile(shutdownTokenPath(s.config.Port)) | ||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||
| log.Debug("Could not read metadata shutdown token for port %d: %v", s.config.Port, err) | ||||||||||||||||||||
| return | ||||||||||||||||||||
| } | ||||||||||||||||||||
| req.Header.Set("X-Scion-Shutdown-Token", strings.TrimSpace(string(token))) | ||||||||||||||||||||
| resp, err := client.Do(req) | ||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||
| log.Debug("Could not reach existing metadata server for shutdown: %v", err) | ||||||||||||||||||||
|
|
@@ -385,6 +408,10 @@ | |||||||||||||||||||
| http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | ||||||||||||||||||||
| return | ||||||||||||||||||||
| } | ||||||||||||||||||||
| if s.shutdownToken == "" || r.Header.Get("X-Scion-Shutdown-Token") != s.shutdownToken { | ||||||||||||||||||||
| http.Error(w, "Forbidden", http.StatusForbidden) | ||||||||||||||||||||
| return | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+411
to
+414
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The shutdown token is compared using standard string comparison ( Additionally, we should trim any leading/trailing whitespace from the incoming header token to be robust against trailing newlines (e.g., if the token file was read with a trailing newline). Please use
Suggested change
|
||||||||||||||||||||
| log.Info("Shutdown requested via /_scion/shutdown, stopping metadata server") | ||||||||||||||||||||
| w.WriteHeader(http.StatusOK) | ||||||||||||||||||||
| fmt.Fprint(w, "shutting down") | ||||||||||||||||||||
|
|
@@ -394,6 +421,39 @@ | |||||||||||||||||||
| }() | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| func shutdownTokenPath(port int) string { | ||||||||||||||||||||
| return filepath.Join(os.TempDir(), fmt.Sprintf("scion-metadata-shutdown-%d.token", port)) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+424
to
+426
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using Using func shutdownTokenPath(port int) string {
dir := os.TempDir()
if runtimeDir, err := os.UserRuntimeDir(); err == nil {
dir = runtimeDir
}
return filepath.Join(dir, fmt.Sprintf("scion-metadata-shutdown-%d.token", port))
} |
||||||||||||||||||||
|
|
||||||||||||||||||||
| func (s *Server) ensureShutdownToken() error { | ||||||||||||||||||||
| if s.shutdownToken != "" { | ||||||||||||||||||||
| return nil | ||||||||||||||||||||
| } | ||||||||||||||||||||
| tokenBytes := make([]byte, 32) | ||||||||||||||||||||
| if _, err := rand.Read(tokenBytes); err != nil { | ||||||||||||||||||||
| return err | ||||||||||||||||||||
| } | ||||||||||||||||||||
| s.shutdownToken = hex.EncodeToString(tokenBytes) | ||||||||||||||||||||
| s.shutdownTokenPath = shutdownTokenPath(s.config.Port) | ||||||||||||||||||||
| return writeShutdownToken(s.shutdownTokenPath, s.shutdownToken) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| func writeShutdownToken(path, token string) error { | ||||||||||||||||||||
| if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { | ||||||||||||||||||||
| return err | ||||||||||||||||||||
| } | ||||||||||||||||||||
| f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL|syscall.O_NOFOLLOW, 0600) | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Furthermore,
Suggested change
|
||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||
| return err | ||||||||||||||||||||
| } | ||||||||||||||||||||
| defer f.Close() | ||||||||||||||||||||
| if _, err := f.WriteString(token + "\n"); err != nil { | ||||||||||||||||||||
| _ = os.Remove(path) | ||||||||||||||||||||
| return err | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+450
to
+453
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On Windows, attempting to delete an open file using
Suggested change
|
||||||||||||||||||||
| return nil | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| func (s *Server) probeHealth() bool { | ||||||||||||||||||||
| client := &http.Client{Timeout: healthCheckTimeout} | ||||||||||||||||||||
| resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:%d/", s.config.Port)) | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a race condition between a stopping server instance (
srv1) and a starting server instance (srv2) reclaiming the port.When
srv1.Stop()is called,s.srv.Shutdown(shutdownCtx)immediately closes the listener. This allowssrv2to successfully bind to the port and write its new token file viaensureShutdownToken(). However,srv1'sStop()continues executing and deletes the token file ats.shutdownTokenPathaftersrv2has already written its new token. This leavessrv2running without a token file, preventing any future instances from shutting it down.To fix this, delete the token file before calling
s.srv.Shutdown(shutdownCtx).