@@ -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
4445var bastionFlag = flag .String ("bastion" , "" , "address of the bastion(s) to reverse proxy through, comma separated, the first online one is selected" )
4546var 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+
47102func 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