@@ -21,6 +21,7 @@ import (
2121 "net/http"
2222 "os"
2323 "os/signal"
24+ "slices"
2425 "strings"
2526 "syscall"
2627 "time"
@@ -39,10 +40,64 @@ var nameFlag = flag.String("name", "", "URL-like (e.g. example.com/foo) name of
3940var dbFlag = flag .String ("db" , "litewitness.db" , "path to sqlite database" )
4041var sshAgentFlag = flag .String ("ssh-agent" , "litewitness.sock" , "path to ssh-agent socket" )
4142var listenFlag = flag .String ("listen" , "localhost:7380" , "address to listen for HTTP requests" )
43+ var noListenFlag = flag .Bool ("no-listen" , false , "do not open any listening socket, rely exclusively on bastions" )
4244var keyFlag = flag .String ("key" , "" , "SSH fingerprint (with SHA256: prefix) of the witness key" )
4345var bastionFlag = flag .String ("bastion" , "" , "address of the bastion(s) to reverse proxy through, comma separated, the first online one is selected" )
4446var testCertFlag = flag .Bool ("testcert" , false , "use rootCA.pem for connections to the bastion" )
4547
48+ type ConnectionSet struct {
49+ connections map [string ]func () // connection => cancel func
50+ connect func (context.Context , string )
51+ }
52+
53+ func NewConnectionSet (connect func (context.Context , string )) * ConnectionSet {
54+ return & ConnectionSet {
55+ connections : make (map [string ]func ()),
56+ connect : connect ,
57+ }
58+ }
59+
60+ func (s * ConnectionSet ) Configure (ctx context.Context , addrs []string ) {
61+ slices .Sort (addrs )
62+
63+ // Disconnect addresses that have disappeared.
64+ var toDelete []string
65+ for addr , cancel := range s .connections {
66+ if _ , found := slices .BinarySearch (addrs , addr ); ! found {
67+ cancel ()
68+ // Postpone delete, we can't delete while iterating over the map.
69+ toDelete = append (toDelete , addr )
70+ }
71+ }
72+ for _ , addr := range toDelete {
73+ delete (s .connections , addr )
74+ }
75+
76+ // Connect new bastions.
77+ for _ , addr := range addrs {
78+ if _ , found := s .connections [addr ]; found {
79+ continue
80+ }
81+ // Quit early on cancel.
82+ if ctx .Err () != nil {
83+ break
84+ }
85+ connectionCtx , cancel := context .WithCancel (ctx )
86+ s .connections [addr ] = cancel
87+ go s .connect (connectionCtx , addr )
88+ }
89+ }
90+
91+ func onSignal (signo os.Signal , callback func ()) {
92+ c := make (chan os.Signal , 1 )
93+ signal .Notify (c , signo )
94+ go func () {
95+ for range c {
96+ callback ()
97+ }
98+ }()
99+ }
100+
46101func main () {
47102 flag .Parse ()
48103
@@ -52,18 +107,14 @@ func main() {
52107 console .SetFilter (slogconsole .IPAddressFilter )
53108 slog .SetDefault (slog .New (slogconsole .MultiHandler (h , console )))
54109
55- c := make (chan os.Signal , 1 )
56- signal .Notify (c , syscall .SIGUSR1 )
57- go func () {
58- for range c {
59- slog .Info ("received USR1 signal, toggling log level" )
60- if level .Level () == slog .LevelDebug {
61- level .Set (slog .LevelInfo )
62- } else {
63- level .Set (slog .LevelDebug )
64- }
110+ onSignal (syscall .SIGUSR1 , func () {
111+ slog .Info ("received USR1 signal, toggling log level" )
112+ if level .Level () == slog .LevelDebug {
113+ level .Set (slog .LevelInfo )
114+ } else {
115+ level .Set (slog .LevelDebug )
65116 }
66- }( )
117+ })
67118
68119 signer := connectToSSHAgent ()
69120
@@ -89,10 +140,77 @@ func main() {
89140 BaseContext : func (net.Listener ) context.Context { return ctx },
90141 }
91142 e := make (chan error , 1 )
143+
144+ bastionSet := NewConnectionSet (func (ctx context.Context , addr string ) {
145+ var delays = []time.Duration {
146+ 100 * time .Millisecond ,
147+ 1 * time .Second , 1 * time .Second , 1 * time .Second ,
148+ 5 * time .Second , 15 * time .Second , 30 * time .Second ,
149+ 1 * time .Minute ,
150+ }
151+
152+ // If a connection survives for resetRetryDelay, reset the retry delay.
153+ const resetRetryDelay = 5 * time .Minute
154+
155+ retry := 0
156+ for {
157+ startTime := time .Now ()
158+ err := connectToBastion (ctx , addr , signer , srv , true )
159+ duration := time .Since (startTime )
160+ slog .Warn ("bastion connection failed" , "bastion" , addr , "duration" , duration , "err" , err )
161+
162+ // Quit early on cancel.
163+ if ctx .Err () != nil {
164+ return
165+ }
166+
167+ // If the connection lasted long enough, reset the retry delay.
168+ if duration >= resetRetryDelay {
169+ retry = 0
170+ }
171+
172+ // Wait before retrying.
173+ var delay time.Duration
174+ if retry < len (delays ) {
175+ delay = delays [retry ]
176+ } else {
177+ delay = delays [len (delays )- 1 ]
178+ }
179+ slog .Info ("waiting before reconnecting to bastion" , "bastion" , addr , "delay" , delay )
180+ timer := time .NewTimer (delay )
181+ select {
182+ case <- ctx .Done ():
183+ timer .Stop ()
184+ return
185+ case <- timer .C :
186+ }
187+ retry ++
188+ }
189+ })
190+
191+ // Handle log-specific bastions.
192+ logBastions , err := w .AllBastions ()
193+ if err != nil {
194+ fatal ("failed looking up bastions" , "err" , err )
195+ }
196+ bastionSet .Configure (ctx , logBastions )
197+
198+ // At this point, ownership of bastionSet belongs with the signal goroutine,
199+ // and must no longer be accessed by main goroutine.
200+ onSignal (syscall .SIGHUP , func () {
201+ slog .Info ("received SIGHUP, reconfiguring bastions" )
202+ logBastions , err := w .AllBastions ()
203+ if err != nil {
204+ slog .Warn ("failed looking up bastions" , "err" , err )
205+ return
206+ }
207+ bastionSet .Configure (ctx , logBastions )
208+ })
209+
92210 if * bastionFlag != "" {
93211 go func () {
94212 for _ , bastion := range strings .Split (* bastionFlag , "," ) {
95- err := connectToBastion (ctx , bastion , signer , srv )
213+ err := connectToBastion (ctx , bastion , signer , srv , false )
96214 if err == errBastionDisconnected {
97215 // Connection succeeded and then was interrupted. Restart to
98216 // let the scheduler apply any backoff, and then retry all bastions.
@@ -102,11 +220,13 @@ func main() {
102220 }
103221 e <- errors .New ("couldn't connect to any bastion" )
104222 }()
105- } else {
223+ } else if ! * noListenFlag {
106224 go func () {
107225 slog .Info ("listening" , "addr" , * listenFlag )
108226 e <- srv .ListenAndServe ()
109227 }()
228+ } else if len (logBastions ) == 0 {
229+ slog .Warn ("configured to not open a listening port, but no bastions configured" )
110230 }
111231
112232 select {
@@ -246,7 +366,7 @@ func indexHandler(w *witness.Witness) http.HandlerFunc {
246366
247367var errBastionDisconnected = errors .New ("connection to bastion interrupted" )
248368
249- func connectToBastion (ctx context.Context , bastion string , signer * signer , srv * http.Server ) error {
369+ func connectToBastion (ctx context.Context , bastion string , signer * signer , srv * http.Server , logSpecific bool ) error {
250370 slog .Info ("connecting to bastion" , "bastion" , bastion )
251371 cert , err := selfSignedCertificate (signer )
252372 if err != nil {
@@ -279,7 +399,19 @@ func connectToBastion(ctx context.Context, bastion string, signer *signer, srv *
279399 slog .Info ("connecting to bastion failed" , "bastion" , bastion , "err" , err )
280400 return fmt .Errorf ("connecting to bastion: %v" , err )
281401 }
402+ // Ensure that the connection is closed when our context is cancelled.
403+ ctx , cancel = context .WithCancel (ctx )
404+ defer cancel ()
405+ go func (ctx context.Context ) {
406+ // TODO: gracefully complete in-flight requests.
407+ <- ctx .Done ()
408+ conn .Close ()
409+ }(ctx )
410+
282411 slog .Info ("connected to bastion" , "bastion" , bastion )
412+ if logSpecific {
413+ ctx = witness .ContextWithBastion (ctx , bastion )
414+ }
283415 // TODO: find a way to surface the fatal error, especially since with
284416 // TLS 1.3 it might be that the bastion rejected the client certificate.
285417 (& http2.Server {
0 commit comments