-
Notifications
You must be signed in to change notification settings - Fork 17
Implement per-log bastions #35
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
c029f22
4188cd4
df81e69
9bfa804
24af86b
f22d6c1
2b3e803
b323254
071217f
2cc1032
b981aab
271d5d3
805207f
5fef782
b7a0eb6
4e25cd2
695fad0
60f44a9
b2c55b0
94baa7d
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 |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ import ( | |
| "net/http" | ||
| "os" | ||
| "os/signal" | ||
| "slices" | ||
| "strings" | ||
| "syscall" | ||
| "time" | ||
|
|
@@ -39,10 +40,64 @@ var nameFlag = flag.String("name", "", "URL-like (e.g. example.com/foo) name of | |
| var dbFlag = flag.String("db", "litewitness.db", "path to sqlite database") | ||
| var sshAgentFlag = flag.String("ssh-agent", "litewitness.sock", "path to ssh-agent socket") | ||
| var listenFlag = flag.String("listen", "localhost:7380", "address to listen for HTTP requests") | ||
| var noListenFlag = flag.Bool("no-listen", false, "do not open any listening socket, rely exclusively on bastions") | ||
| var keyFlag = flag.String("key", "", "SSH fingerprint (with SHA256: prefix) of the witness key") | ||
| var bastionFlag = flag.String("bastion", "", "address of the bastion(s) to reverse proxy through, comma separated, the first online one is selected") | ||
| var testCertFlag = flag.Bool("testcert", false, "use rootCA.pem for connections to the bastion") | ||
|
|
||
| type ConnectionSet struct { | ||
| connections map[string]func() // connection => cancel func | ||
| connect func(context.Context, string) | ||
| } | ||
|
|
||
| func NewConnectionSet(connect func(context.Context, string)) *ConnectionSet { | ||
| return &ConnectionSet{ | ||
| connections: make(map[string]func()), | ||
| connect: connect, | ||
| } | ||
| } | ||
|
|
||
| func (s *ConnectionSet) Configure(ctx context.Context, addrs []string) { | ||
| slices.Sort(addrs) | ||
|
|
||
| // Disconnect addresses that have disappeared. | ||
| var toDelete []string | ||
| for addr, cancel := range s.connections { | ||
| if _, found := slices.BinarySearch(addrs, addr); !found { | ||
| cancel() | ||
| // Postpone delete, we can't delete while iterating over the map. | ||
| toDelete = append(toDelete, addr) | ||
| } | ||
| } | ||
| for _, addr := range toDelete { | ||
| delete(s.connections, addr) | ||
| } | ||
|
|
||
| // Connect new bastions. | ||
| for _, addr := range addrs { | ||
| if _, found := s.connections[addr]; found { | ||
| continue | ||
| } | ||
| // Quit early on cancel. | ||
| if ctx.Err() != nil { | ||
| break | ||
| } | ||
| connectionCtx, cancel := context.WithCancel(ctx) | ||
| s.connections[addr] = cancel | ||
| go s.connect(connectionCtx, addr) | ||
| } | ||
| } | ||
|
|
||
| func onSignal(signo os.Signal, callback func()) { | ||
| c := make(chan os.Signal, 1) | ||
| signal.Notify(c, signo) | ||
| go func() { | ||
| for range c { | ||
| callback() | ||
| } | ||
| }() | ||
| } | ||
|
|
||
| func main() { | ||
| flag.Parse() | ||
|
|
||
|
|
@@ -52,18 +107,14 @@ func main() { | |
| console.SetFilter(slogconsole.IPAddressFilter) | ||
| slog.SetDefault(slog.New(slogconsole.MultiHandler(h, console))) | ||
|
|
||
| c := make(chan os.Signal, 1) | ||
| signal.Notify(c, syscall.SIGUSR1) | ||
| go func() { | ||
| for range c { | ||
| slog.Info("received USR1 signal, toggling log level") | ||
| if level.Level() == slog.LevelDebug { | ||
| level.Set(slog.LevelInfo) | ||
| } else { | ||
| level.Set(slog.LevelDebug) | ||
| } | ||
| onSignal(syscall.SIGUSR1, func() { | ||
| slog.Info("received USR1 signal, toggling log level") | ||
| if level.Level() == slog.LevelDebug { | ||
| level.Set(slog.LevelInfo) | ||
| } else { | ||
| level.Set(slog.LevelDebug) | ||
| } | ||
| }() | ||
| }) | ||
|
|
||
| signer := connectToSSHAgent() | ||
|
|
||
|
|
@@ -89,10 +140,77 @@ func main() { | |
| BaseContext: func(net.Listener) context.Context { return ctx }, | ||
| } | ||
| e := make(chan error, 1) | ||
|
|
||
| bastionSet := NewConnectionSet(func(ctx context.Context, addr string) { | ||
| var delays = []time.Duration{ | ||
| 100 * time.Millisecond, | ||
| 1 * time.Second, 1 * time.Second, 1 * time.Second, | ||
| 5 * time.Second, 15 * time.Second, 30 * time.Second, | ||
| 1 * time.Minute, | ||
| } | ||
|
|
||
| // If a connection survives for resetRetryDelay, reset the retry delay. | ||
| const resetRetryDelay = 5 * time.Minute | ||
|
|
||
| retry := 0 | ||
| for { | ||
| startTime := time.Now() | ||
| err := connectToBastion(ctx, addr, signer, srv, true) | ||
| duration := time.Since(startTime) | ||
| slog.Warn("bastion connection failed", "bastion", addr, "duration", duration, "err", err) | ||
|
|
||
| // Quit early on cancel. | ||
| if ctx.Err() != nil { | ||
| return | ||
| } | ||
|
|
||
| // If the connection lasted long enough, reset the retry delay. | ||
| if duration >= resetRetryDelay { | ||
| retry = 0 | ||
| } | ||
|
|
||
| // Wait before retrying. | ||
| var delay time.Duration | ||
| if retry < len(delays) { | ||
| delay = delays[retry] | ||
| } else { | ||
| delay = delays[len(delays)-1] | ||
| } | ||
| slog.Info("waiting before reconnecting to bastion", "bastion", addr, "delay", delay) | ||
| timer := time.NewTimer(delay) | ||
| select { | ||
| case <-ctx.Done(): | ||
| timer.Stop() | ||
| return | ||
| case <-timer.C: | ||
| } | ||
| retry++ | ||
| } | ||
| }) | ||
|
|
||
| // Handle log-specific bastions. | ||
| logBastions, err := w.AllBastions() | ||
|
Contributor
Author
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. This only looks up configured bastions at startup, changes made with witnessctl won't take effect until next restart. I wonder if that is good enough, or if we need something more dynamic?
Contributor
Author
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. There seems to be an sqlite3_update_hook function, but unclear to me if that is usable for this purpose.
Contributor
Author
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. I think it's highly desirable to make this a bit more dynamic, in one way or the otherm but I think that can be improved in later PRs. |
||
| if err != nil { | ||
| fatal("failed looking up bastions", "err", err) | ||
| } | ||
| bastionSet.Configure(ctx, logBastions) | ||
|
|
||
| // At this point, ownership of bastionSet belongs with the signal goroutine, | ||
| // and must no longer be accessed by main goroutine. | ||
| onSignal(syscall.SIGHUP, func() { | ||
| slog.Info("received SIGHUP, reconfiguring bastions") | ||
| logBastions, err := w.AllBastions() | ||
| if err != nil { | ||
| slog.Warn("failed looking up bastions", "err", err) | ||
| return | ||
| } | ||
| bastionSet.Configure(ctx, logBastions) | ||
| }) | ||
|
|
||
| if *bastionFlag != "" { | ||
| go func() { | ||
| for _, bastion := range strings.Split(*bastionFlag, ",") { | ||
| err := connectToBastion(ctx, bastion, signer, srv) | ||
| err := connectToBastion(ctx, bastion, signer, srv, false) | ||
| if err == errBastionDisconnected { | ||
| // Connection succeeded and then was interrupted. Restart to | ||
| // let the scheduler apply any backoff, and then retry all bastions. | ||
|
|
@@ -102,11 +220,13 @@ func main() { | |
| } | ||
| e <- errors.New("couldn't connect to any bastion") | ||
| }() | ||
| } else { | ||
| } else if !*noListenFlag { | ||
| go func() { | ||
| slog.Info("listening", "addr", *listenFlag) | ||
| e <- srv.ListenAndServe() | ||
| }() | ||
| } else if len(logBastions) == 0 { | ||
| slog.Warn("configured to not open a listening port, but no bastions configured") | ||
| } | ||
|
|
||
| select { | ||
|
|
@@ -246,7 +366,7 @@ func indexHandler(w *witness.Witness) http.HandlerFunc { | |
|
|
||
| var errBastionDisconnected = errors.New("connection to bastion interrupted") | ||
|
|
||
| func connectToBastion(ctx context.Context, bastion string, signer *signer, srv *http.Server) error { | ||
| func connectToBastion(ctx context.Context, bastion string, signer *signer, srv *http.Server, logSpecific bool) error { | ||
| slog.Info("connecting to bastion", "bastion", bastion) | ||
| cert, err := selfSignedCertificate(signer) | ||
| if err != nil { | ||
|
|
@@ -279,7 +399,19 @@ func connectToBastion(ctx context.Context, bastion string, signer *signer, srv * | |
| slog.Info("connecting to bastion failed", "bastion", bastion, "err", err) | ||
| return fmt.Errorf("connecting to bastion: %v", err) | ||
| } | ||
| // Ensure that the connection is closed when our context is cancelled. | ||
| ctx, cancel = context.WithCancel(ctx) | ||
| defer cancel() | ||
| go func(ctx context.Context) { | ||
| // TODO: gracefully complete in-flight requests. | ||
| <-ctx.Done() | ||
| conn.Close() | ||
| }(ctx) | ||
|
|
||
| slog.Info("connected to bastion", "bastion", bastion) | ||
| if logSpecific { | ||
| ctx = witness.ContextWithBastion(ctx, bastion) | ||
|
Owner
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. This write to the ctx variable races with using ctx in the goroutine above.
Comment on lines
+402
to
+413
Contributor
Author
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. Note that it is unclear to me why this is needed; it would make sense to me if one could have ServeConn close the connection when the context passed down to that machinery is cancelled. I also first tried to move creation of this goroutine later, after the |
||
| } | ||
| // TODO: find a way to surface the fatal error, especially since with | ||
| // TLS 1.3 it might be that the bastion rejected the client certificate. | ||
| (&http2.Server{ | ||
|
|
||
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.
I have reworked this logic to