@@ -5,7 +5,10 @@ package localapi
55
66import (
77 "bytes"
8+ "context"
89 "encoding/json"
10+ "errors"
11+ "fmt"
912 "io"
1013 "net/http"
1114 "net/http/httptest"
@@ -17,8 +20,13 @@ import (
1720 "tailscale.com/client/tailscale/apitype"
1821 "tailscale.com/ipn"
1922 "tailscale.com/ipn/ipnlocal"
23+ "tailscale.com/ipn/store/mem"
2024 "tailscale.com/tailcfg"
25+ "tailscale.com/tsd"
2126 "tailscale.com/tstest"
27+ "tailscale.com/types/logger"
28+ "tailscale.com/types/logid"
29+ "tailscale.com/wgengine"
2230)
2331
2432func TestValidHost (t * testing.T ) {
@@ -212,3 +220,92 @@ func TestShouldDenyServeConfigForGOOSAndUserContext(t *testing.T) {
212220 })
213221 }
214222}
223+
224+ func TestServeWatchIPNBus (t * testing.T ) {
225+ tstest .Replace (t , & validLocalHostForTesting , true )
226+
227+ tests := []struct {
228+ desc string
229+ permitRead , permitWrite bool
230+ mask ipn.NotifyWatchOpt // extra bits in addition to ipn.NotifyInitialState
231+ wantStatus int
232+ }{
233+ {
234+ desc : "no-permission" ,
235+ permitRead : false ,
236+ permitWrite : false ,
237+ wantStatus : http .StatusForbidden ,
238+ },
239+ {
240+ desc : "read-initial-state" ,
241+ permitRead : true ,
242+ permitWrite : false ,
243+ wantStatus : http .StatusForbidden ,
244+ },
245+ {
246+ desc : "read-initial-state-no-private-keys" ,
247+ permitRead : true ,
248+ permitWrite : false ,
249+ mask : ipn .NotifyNoPrivateKeys ,
250+ wantStatus : http .StatusOK ,
251+ },
252+ {
253+ desc : "read-initial-state-with-private-keys" ,
254+ permitRead : true ,
255+ permitWrite : true ,
256+ wantStatus : http .StatusOK ,
257+ },
258+ }
259+
260+ for _ , tt := range tests {
261+ t .Run (tt .desc , func (t * testing.T ) {
262+ h := & Handler {
263+ PermitRead : tt .permitRead ,
264+ PermitWrite : tt .permitWrite ,
265+ b : newTestLocalBackend (t ),
266+ }
267+ s := httptest .NewServer (h )
268+ defer s .Close ()
269+ c := s .Client ()
270+
271+ ctx , cancel := context .WithCancel (context .Background ())
272+ req , err := http .NewRequestWithContext (ctx , "GET" , fmt .Sprintf ("%s/localapi/v0/watch-ipn-bus?mask=%d" , s .URL , ipn .NotifyInitialState | tt .mask ), nil )
273+ if err != nil {
274+ t .Fatal (err )
275+ }
276+ res , err := c .Do (req )
277+ if err != nil {
278+ t .Fatal (err )
279+ }
280+ defer res .Body .Close ()
281+ // Cancel the context so that localapi stops streaming IPN bus
282+ // updates.
283+ cancel ()
284+ body , err := io .ReadAll (res .Body )
285+ if err != nil && ! errors .Is (err , context .Canceled ) {
286+ t .Fatal (err )
287+ }
288+ if res .StatusCode != tt .wantStatus {
289+ t .Errorf ("res.StatusCode=%d, want %d. body: %s" , res .StatusCode , tt .wantStatus , body )
290+ }
291+ })
292+ }
293+ }
294+
295+ func newTestLocalBackend (t testing.TB ) * ipnlocal.LocalBackend {
296+ var logf logger.Logf = logger .Discard
297+ sys := new (tsd.System )
298+ store := new (mem.Store )
299+ sys .Set (store )
300+ eng , err := wgengine .NewFakeUserspaceEngine (logf , sys .Set )
301+ if err != nil {
302+ t .Fatalf ("NewFakeUserspaceEngine: %v" , err )
303+ }
304+ t .Cleanup (eng .Close )
305+ sys .Set (eng )
306+ lb , err := ipnlocal .NewLocalBackend (logf , logid.PublicID {}, sys , 0 )
307+ if err != nil {
308+ t .Fatalf ("NewLocalBackend: %v" , err )
309+ }
310+ return lb
311+ }
0 commit comments