Skip to content

Commit 535b925

Browse files
committed
derp/derphttp: add Client.Ping, SendPing methods
Continuing work in 434af15, to make it possible for magicsock to probe whether a DERP server is still there. Updates tailscale#3619 Change-Id: I366a77c27e93b876734e64f445b85ef01eb590f2 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
1 parent 434af15 commit 535b925

File tree

2 files changed

+129
-7
lines changed

2 files changed

+129
-7
lines changed

derp/derphttp/derphttp_client.go

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ package derphttp
1313
import (
1414
"bufio"
1515
"context"
16+
"crypto/rand"
1617
"crypto/tls"
1718
"crypto/x509"
1819
"errors"
@@ -72,6 +73,7 @@ type Client struct {
7273
client *derp.Client
7374
connGen int // incremented once per new connection; valid values are >0
7475
serverPubKey key.NodePublic
76+
pingOut map[derp.PingMessage]chan<- bool // chan to send to on pong
7577
}
7678

7779
// NewRegionClient returns a new DERP-over-HTTP client. It connects lazily.
@@ -698,7 +700,67 @@ func (c *Client) Send(dstKey key.NodePublic, b []byte) error {
698700
return err
699701
}
700702

701-
// SendPing sends a ping message, without any implicit connect or reconnect.
703+
func (c *Client) registerPing(m derp.PingMessage, ch chan<- bool) {
704+
c.mu.Lock()
705+
defer c.mu.Unlock()
706+
if c.pingOut == nil {
707+
c.pingOut = map[derp.PingMessage]chan<- bool{}
708+
}
709+
c.pingOut[m] = ch
710+
}
711+
712+
func (c *Client) unregisterPing(m derp.PingMessage) {
713+
c.mu.Lock()
714+
defer c.mu.Unlock()
715+
delete(c.pingOut, m)
716+
}
717+
718+
func (c *Client) handledPong(m derp.PongMessage) bool {
719+
c.mu.Lock()
720+
defer c.mu.Unlock()
721+
k := derp.PingMessage(m)
722+
if ch, ok := c.pingOut[k]; ok {
723+
ch <- true
724+
delete(c.pingOut, k)
725+
return true
726+
}
727+
return false
728+
}
729+
730+
// Ping sends a ping to the peer and waits for it either to be
731+
// acknowledged (in which case Ping returns nil) or waits for ctx to
732+
// be over and returns an error. It will wait at most 5 seconds
733+
// before returning an error.
734+
//
735+
// Another goroutine must be in a loop calling Recv or
736+
// RecvDetail or ping responses won't be handled.
737+
func (c *Client) Ping(ctx context.Context) error {
738+
maxDL := time.Now().Add(5 * time.Second)
739+
if dl, ok := ctx.Deadline(); !ok || dl.After(maxDL) {
740+
var cancel context.CancelFunc
741+
ctx, cancel = context.WithDeadline(ctx, maxDL)
742+
defer cancel()
743+
}
744+
var data derp.PingMessage
745+
rand.Read(data[:])
746+
gotPing := make(chan bool, 1)
747+
c.registerPing(data, gotPing)
748+
defer c.unregisterPing(data)
749+
if err := c.SendPing(data); err != nil {
750+
return err
751+
}
752+
select {
753+
case <-gotPing:
754+
return nil
755+
case <-ctx.Done():
756+
return ctx.Err()
757+
}
758+
}
759+
760+
// SendPing writes a ping message, without any implicit connect or
761+
// reconnect. This is a lower-level interface that writes a frame
762+
// without any implicit handling of the response pong, if any. For a
763+
// higher-level interface, use Ping.
702764
func (c *Client) SendPing(data [8]byte) error {
703765
c.mu.Lock()
704766
closed, client := c.closed, c.client
@@ -819,14 +881,22 @@ func (c *Client) RecvDetail() (m derp.ReceivedMessage, connGen int, err error) {
819881
if err != nil {
820882
return nil, 0, err
821883
}
822-
m, err = client.Recv()
823-
if err != nil {
824-
c.closeForReconnect(client)
825-
if c.isClosed() {
826-
err = ErrClientClosed
884+
for {
885+
m, err = client.Recv()
886+
switch m := m.(type) {
887+
case derp.PongMessage:
888+
if c.handledPong(m) {
889+
continue
890+
}
891+
}
892+
if err != nil {
893+
c.closeForReconnect(client)
894+
if c.isClosed() {
895+
err = ErrClientClosed
896+
}
827897
}
898+
return m, connGen, err
828899
}
829-
return m, connGen, err
830900
}
831901

832902
func (c *Client) isClosed() bool {

derp/derphttp/derphttp_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,55 @@ func waitConnect(t testing.TB, c *Client) {
154154
t.Fatalf("client first Recv was unexpected type %T", v)
155155
}
156156
}
157+
158+
func TestPing(t *testing.T) {
159+
serverPrivateKey := key.NewNode()
160+
s := derp.NewServer(serverPrivateKey, t.Logf)
161+
defer s.Close()
162+
163+
httpsrv := &http.Server{
164+
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
165+
Handler: Handler(s),
166+
}
167+
168+
ln, err := net.Listen("tcp4", "localhost:0")
169+
if err != nil {
170+
t.Fatal(err)
171+
}
172+
serverURL := "http://" + ln.Addr().String()
173+
t.Logf("server URL: %s", serverURL)
174+
175+
go func() {
176+
if err := httpsrv.Serve(ln); err != nil {
177+
if err == http.ErrServerClosed {
178+
return
179+
}
180+
panic(err)
181+
}
182+
}()
183+
184+
c, err := NewClient(key.NewNode(), serverURL, t.Logf)
185+
if err != nil {
186+
t.Fatalf("NewClient: %v", err)
187+
}
188+
defer c.Close()
189+
if err := c.Connect(context.Background()); err != nil {
190+
t.Fatalf("client Connect: %v", err)
191+
}
192+
193+
errc := make(chan error, 1)
194+
go func() {
195+
for {
196+
m, err := c.Recv()
197+
if err != nil {
198+
errc <- err
199+
return
200+
}
201+
t.Logf("Recv: %T", m)
202+
}
203+
}()
204+
err = c.Ping(context.Background())
205+
if err != nil {
206+
t.Fatalf("Ping: %v", err)
207+
}
208+
}

0 commit comments

Comments
 (0)