Skip to content

Commit f784a5d

Browse files
author
Niels Möller
committed
Improve bastion retry logic
1 parent 188beea commit f784a5d

1 file changed

Lines changed: 102 additions & 27 deletions

File tree

cmd/litewitness/litewitness.go

Lines changed: 102 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"net/http"
2222
"os"
2323
"os/signal"
24+
"slices"
2425
"strings"
2526
"syscall"
2627
"time"
@@ -44,6 +45,60 @@ var keyFlag = flag.String("key", "", "SSH fingerprint (with SHA256: prefix) of t
4445
var bastionFlag = flag.String("bastion", "", "address of the bastion(s) to reverse proxy through, comma separated, the first online one is selected")
4546
var testCertFlag = flag.Bool("testcert", false, "use rootCA.pem for connections to the bastion")
4647

48+
type ConnectionSet struct {
49+
// Map of active connections, and cancel functions for each.
50+
connections map[string]func()
51+
connect func(context.Context, string)
52+
}
53+
54+
func NewConnectionSet(connect func(context.Context, string)) *ConnectionSet {
55+
return &ConnectionSet{
56+
connections: make(map[string]func()),
57+
connect: connect,
58+
}
59+
}
60+
61+
func (s *ConnectionSet) Configure(ctx context.Context, addrs []string) {
62+
slices.Sort(addrs)
63+
64+
// Disconnect addresses that have disappeared.
65+
var toDelete []string
66+
for addr, cancel := range s.connections {
67+
if _, found := slices.BinarySearch(addrs, addr); !found {
68+
cancel()
69+
// Postpone delete, we can't delete while iterating over the map.
70+
toDelete = append(toDelete, addr)
71+
}
72+
}
73+
for _, addr := range toDelete {
74+
delete(s.connections, addr)
75+
}
76+
77+
// Connect new bastions.
78+
for _, addr := range addrs {
79+
if _, found := s.connections[addr]; found {
80+
continue
81+
}
82+
// Quit early on cancel
83+
if ctx.Err() != nil {
84+
break
85+
}
86+
connectionCtx, cancel := context.WithCancel(ctx)
87+
s.connections[addr] = cancel
88+
go s.connect(connectionCtx, addr)
89+
}
90+
}
91+
92+
func onSignal(signo os.Signal, callback func()) {
93+
c := make(chan os.Signal, 1)
94+
signal.Notify(c, signo)
95+
go func() {
96+
for range c {
97+
callback()
98+
}
99+
}()
100+
}
101+
47102
func main() {
48103
flag.Parse()
49104

@@ -53,18 +108,14 @@ func main() {
53108
console.SetFilter(slogconsole.IPAddressFilter)
54109
slog.SetDefault(slog.New(slogconsole.MultiHandler(h, console)))
55110

56-
c := make(chan os.Signal, 1)
57-
signal.Notify(c, syscall.SIGUSR1)
58-
go func() {
59-
for range c {
60-
slog.Info("received USR1 signal, toggling log level")
61-
if level.Level() == slog.LevelDebug {
62-
level.Set(slog.LevelInfo)
63-
} else {
64-
level.Set(slog.LevelDebug)
65-
}
111+
onSignal(syscall.SIGUSR1, func() {
112+
slog.Info("received USR1 signal, toggling log level")
113+
if level.Level() == slog.LevelDebug {
114+
level.Set(slog.LevelInfo)
115+
} else {
116+
level.Set(slog.LevelDebug)
66117
}
67-
}()
118+
})
68119

69120
signer := connectToSSHAgent()
70121

@@ -90,28 +141,52 @@ func main() {
90141
BaseContext: func(net.Listener) context.Context { return ctx },
91142
}
92143
e := make(chan error, 1)
93-
// Handle log-specific bastions.
94-
logBastions, err := w.AllBastions()
95-
if err != nil {
96-
fatal("failed looking up bastions", "err", err)
97-
}
98-
const bastionInitialRetryDelay = 30 * time.Second
99-
const bastionMaxRetryDelay = time.Hour
100-
for _, bastion := range logBastions {
101-
go func(bastion string) {
102-
for delay := bastionInitialRetryDelay; ; delay *= 2 {
103-
err := connectToBastion(ctx, bastion, signer, srv, true)
104-
slog.Warn("connection failed", "bastion", err)
144+
145+
bastionSet := NewConnectionSet(func(ctx context.Context, addr string) {
146+
const bastionInitialRetryDelay = 10 * time.Second
147+
const bastionMaxRetryDelay = time.Hour
148+
149+
delay := bastionInitialRetryDelay
150+
for {
151+
startTime := time.Now()
152+
err := connectToBastion(ctx, addr, signer, srv, true)
153+
duration := time.Since(startTime)
154+
slog.Warn("bastion connection failed", "duration", duration, "err", err)
155+
// Reset retry delay if bastion connection succeeded, and went down
156+
// later due to restart or network issues.
157+
if err == errBastionDisconnected && duration > delay {
158+
delay = bastionInitialRetryDelay
159+
} else {
160+
delay *= 2
105161
if delay > bastionMaxRetryDelay {
106-
// Give up, restart to let the scheduler apply any backoff,
107-
// and then retry all bastions.
162+
// Give up, restart to let the scheduler apply any
163+
// backoff, and then retry all bastions.
108164
e <- err
109165
return
110166
}
111-
time.Sleep(delay)
112167
}
113-
}(bastion)
168+
time.Sleep(delay)
169+
}
170+
})
171+
172+
// Handle log-specific bastions.
173+
logBastions, err := w.AllBastions()
174+
if err != nil {
175+
fatal("failed looking up bastions", "err", err)
114176
}
177+
bastionSet.Configure(ctx, logBastions)
178+
179+
// At this point, ownership of bastionSet belongs with the signal goroutine, and must no
180+
// longer be accessed by main goroutine.
181+
onSignal(syscall.SIGHUP, func() {
182+
logBastions, err := w.AllBastions()
183+
if err != nil {
184+
slog.Warn("failed looking up bastions", "err", err)
185+
return
186+
}
187+
bastionSet.Configure(ctx, logBastions)
188+
})
189+
115190
if *bastionFlag != "" {
116191
go func() {
117192
for _, bastion := range strings.Split(*bastionFlag, ",") {

0 commit comments

Comments
 (0)