Skip to content

Commit 159ae8b

Browse files
committed
Merge branch 'main' into rmwebrtc
2 parents 2403c94 + e3bbc77 commit 159ae8b

File tree

9 files changed

+200
-11
lines changed

9 files changed

+200
-11
lines changed

agent/agent.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"go.uber.org/atomic"
3030
gossh "golang.org/x/crypto/ssh"
3131
"golang.org/x/xerrors"
32+
"tailscale.com/net/speedtest"
3233
"tailscale.com/tailcfg"
3334

3435
"cdr.dev/slog"
@@ -56,6 +57,7 @@ var (
5657
tailnetIP = netip.MustParseAddr("fd7a:115c:a1e0:49d6:b259:b7ac:b1b2:48f4")
5758
tailnetSSHPort = 1
5859
tailnetReconnectingPTYPort = 2
60+
tailnetSpeedtestPort = 3
5961
)
6062

6163
type Options struct {
@@ -246,6 +248,27 @@ func (a *agent) runTailnet(ctx context.Context, derpMap *tailcfg.DERPMap) {
246248
go a.handleReconnectingPTY(ctx, msg, conn)
247249
}
248250
}()
251+
speedtestListener, err := a.network.Listen("tcp", ":"+strconv.Itoa(tailnetSpeedtestPort))
252+
if err != nil {
253+
a.logger.Critical(ctx, "listen for speedtest", slog.Error(err))
254+
return
255+
}
256+
go func() {
257+
for {
258+
conn, err := speedtestListener.Accept()
259+
if err != nil {
260+
a.logger.Debug(ctx, "speedtest listener failed", slog.Error(err))
261+
return
262+
}
263+
a.closeMutex.Lock()
264+
a.connCloseWait.Add(1)
265+
a.closeMutex.Unlock()
266+
go func() {
267+
a.connCloseWait.Done()
268+
_ = speedtest.ServeConn(conn)
269+
}()
270+
}
271+
}()
249272
}
250273

251274
// runCoordinator listens for nodes and updates the self-node as it changes.

agent/agent_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"time"
2020

2121
"golang.org/x/xerrors"
22+
"tailscale.com/net/speedtest"
2223

2324
scp "github.com/bramvdbogaerde/go-scp"
2425
"github.com/google/uuid"
@@ -491,6 +492,21 @@ func TestAgent(t *testing.T) {
491492
return err == nil
492493
}, testutil.WaitMedium, testutil.IntervalFast)
493494
})
495+
496+
t.Run("Speedtest", func(t *testing.T) {
497+
t.Parallel()
498+
if testing.Short() {
499+
t.Skip("The minimum duration for a speedtest is hardcoded in Tailscale to 5s!")
500+
}
501+
derpMap := tailnettest.RunDERPAndSTUN(t)
502+
conn, _ := setupAgent(t, agent.Metadata{
503+
DERPMap: derpMap,
504+
}, 0)
505+
defer conn.Close()
506+
res, err := conn.Speedtest(speedtest.Upload, speedtest.MinDuration)
507+
require.NoError(t, err)
508+
t.Logf("%.2f MBits/s", res[len(res)-1].MBitsPerSecond())
509+
})
494510
}
495511

496512
func setupSSHCommand(t *testing.T, beforeArgs []string, afterArgs []string) *exec.Cmd {

agent/conn.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"golang.org/x/crypto/ssh"
1313
"golang.org/x/xerrors"
1414
"tailscale.com/ipn/ipnstate"
15+
"tailscale.com/net/speedtest"
1516
"tailscale.com/tailcfg"
1617

1718
"github.com/coder/coder/tailnet"
@@ -115,6 +116,18 @@ func (c *Conn) SSHClient() (*ssh.Client, error) {
115116
return ssh.NewClient(sshConn, channels, requests), nil
116117
}
117118

119+
func (c *Conn) Speedtest(direction speedtest.Direction, duration time.Duration) ([]speedtest.Result, error) {
120+
speedConn, err := c.DialContextTCP(context.Background(), netip.AddrPortFrom(tailnetIP, uint16(tailnetSpeedtestPort)))
121+
if err != nil {
122+
return nil, xerrors.Errorf("dial speedtest: %w", err)
123+
}
124+
results, err := speedtest.RunClientWithConn(direction, duration, speedConn)
125+
if err != nil {
126+
return nil, xerrors.Errorf("run speedtest: %w", err)
127+
}
128+
return results, err
129+
}
130+
118131
func (c *Conn) DialContext(ctx context.Context, network string, addr string) (net.Conn, error) {
119132
if network == "unix" {
120133
return nil, xerrors.New("network must be tcp or udp")

cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func Core() []*cobra.Command {
7878
schedules(),
7979
show(),
8080
ssh(),
81+
speedtest(),
8182
start(),
8283
state(),
8384
stop(),

cli/speedtest.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"cdr.dev/slog"
9+
"github.com/coder/coder/cli/cliflag"
10+
"github.com/coder/coder/cli/cliui"
11+
"github.com/coder/coder/codersdk"
12+
"github.com/jedib0t/go-pretty/v6/table"
13+
"github.com/spf13/cobra"
14+
"golang.org/x/xerrors"
15+
tsspeedtest "tailscale.com/net/speedtest"
16+
)
17+
18+
func speedtest() *cobra.Command {
19+
var (
20+
reverse bool
21+
timeStr string
22+
)
23+
cmd := &cobra.Command{
24+
Annotations: workspaceCommand,
25+
Use: "speedtest <workspace>",
26+
Short: "Run a speed test from your machine to the workspace.",
27+
RunE: func(cmd *cobra.Command, args []string) error {
28+
ctx, cancel := context.WithCancel(cmd.Context())
29+
defer cancel()
30+
31+
dur, err := time.ParseDuration(timeStr)
32+
if err != nil {
33+
return err
34+
}
35+
36+
client, err := CreateClient(cmd)
37+
if err != nil {
38+
return xerrors.Errorf("create codersdk client: %w", err)
39+
}
40+
41+
workspace, workspaceAgent, err := getWorkspaceAndAgent(ctx, cmd, client, codersdk.Me, args[0], false)
42+
if err != nil {
43+
return err
44+
}
45+
46+
err = cliui.Agent(ctx, cmd.ErrOrStderr(), cliui.AgentOptions{
47+
WorkspaceName: workspace.Name,
48+
Fetch: func(ctx context.Context) (codersdk.WorkspaceAgent, error) {
49+
return client.WorkspaceAgent(ctx, workspaceAgent.ID)
50+
},
51+
})
52+
if err != nil {
53+
return xerrors.Errorf("await agent: %w", err)
54+
}
55+
conn, err := client.DialWorkspaceAgentTailnet(ctx, slog.Logger{}, workspaceAgent.ID)
56+
if err != nil {
57+
return err
58+
}
59+
defer conn.Close()
60+
_, _ = conn.Ping()
61+
dir := tsspeedtest.Download
62+
if reverse {
63+
dir = tsspeedtest.Upload
64+
}
65+
cmd.Printf("Starting a %ds %s test...\n", int(dur.Seconds()), dir)
66+
results, err := conn.Speedtest(dir, dur)
67+
if err != nil {
68+
return err
69+
}
70+
tableWriter := cliui.Table()
71+
tableWriter.AppendHeader(table.Row{"Interval", "Transfer", "Bandwidth"})
72+
for _, r := range results {
73+
if r.Total {
74+
tableWriter.AppendSeparator()
75+
}
76+
tableWriter.AppendRow(table.Row{
77+
fmt.Sprintf("%.2f-%.2f sec", r.IntervalStart.Seconds(), r.IntervalEnd.Seconds()),
78+
fmt.Sprintf("%.4f MBits", r.MegaBits()),
79+
fmt.Sprintf("%.4f Mbits/sec", r.MBitsPerSecond()),
80+
})
81+
}
82+
_, err = fmt.Fprintln(cmd.OutOrStdout(), tableWriter.Render())
83+
return err
84+
},
85+
}
86+
cliflag.BoolVarP(cmd.Flags(), &reverse, "reverse", "r", "", false,
87+
"Specifies whether to run in reverse mode where the client receives and the server sends.")
88+
cliflag.StringVarP(cmd.Flags(), &timeStr, "time", "t", "", "5s",
89+
"Specifies the duration to monitor traffic.")
90+
return cmd
91+
}

cli/speedtest_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package cli_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"cdr.dev/slog/sloggers/slogtest"
8+
"github.com/coder/coder/agent"
9+
"github.com/coder/coder/cli/clitest"
10+
"github.com/coder/coder/coderd/coderdtest"
11+
"github.com/coder/coder/codersdk"
12+
"github.com/coder/coder/pty/ptytest"
13+
"github.com/coder/coder/testutil"
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func TestSpeedtest(t *testing.T) {
18+
t.Parallel()
19+
if testing.Short() {
20+
t.Skip("This test takes a minimum of 5ms per a hardcoded value in Tailscale!")
21+
}
22+
client, workspace, agentToken := setupWorkspaceForAgent(t)
23+
agentClient := codersdk.New(client.URL)
24+
agentClient.SessionToken = agentToken
25+
agentCloser := agent.New(agent.Options{
26+
FetchMetadata: agentClient.WorkspaceAgentMetadata,
27+
CoordinatorDialer: agentClient.ListenWorkspaceAgentTailnet,
28+
Logger: slogtest.Make(t, nil).Named("agent"),
29+
})
30+
defer agentCloser.Close()
31+
coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID)
32+
33+
cmd, root := clitest.New(t, "speedtest", workspace.Name)
34+
clitest.SetupConfig(t, client, root)
35+
pty := ptytest.New(t)
36+
cmd.SetOut(pty.Output())
37+
38+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
39+
defer cancel()
40+
cmdDone := tGo(t, func() {
41+
err := cmd.ExecuteContext(ctx)
42+
assert.NoError(t, err)
43+
})
44+
<-cmdDone
45+
}

cli/ssh_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import (
3131
"github.com/coder/coder/testutil"
3232
)
3333

34-
func setupWorkspaceForSSH(t *testing.T) (*codersdk.Client, codersdk.Workspace, string) {
34+
func setupWorkspaceForAgent(t *testing.T) (*codersdk.Client, codersdk.Workspace, string) {
3535
t.Helper()
3636
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
3737
user := coderdtest.CreateFirstUser(t, client)
@@ -69,7 +69,7 @@ func TestSSH(t *testing.T) {
6969
t.Run("ImmediateExit", func(t *testing.T) {
7070
t.Parallel()
7171

72-
client, workspace, agentToken := setupWorkspaceForSSH(t)
72+
client, workspace, agentToken := setupWorkspaceForAgent(t)
7373
cmd, root := clitest.New(t, "ssh", workspace.Name)
7474
clitest.SetupConfig(t, client, root)
7575
pty := ptytest.New(t)
@@ -103,7 +103,7 @@ func TestSSH(t *testing.T) {
103103
})
104104
t.Run("Stdio", func(t *testing.T) {
105105
t.Parallel()
106-
client, workspace, agentToken := setupWorkspaceForSSH(t)
106+
client, workspace, agentToken := setupWorkspaceForAgent(t)
107107
_, _ = tGoContext(t, func(ctx context.Context) {
108108
// Run this async so the SSH command has to wait for
109109
// the build and agent to connect!
@@ -173,7 +173,7 @@ func TestSSH(t *testing.T) {
173173

174174
t.Parallel()
175175

176-
client, workspace, agentToken := setupWorkspaceForSSH(t)
176+
client, workspace, agentToken := setupWorkspaceForAgent(t)
177177

178178
agentClient := codersdk.New(client.URL)
179179
agentClient.SessionToken = agentToken

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ require (
133133
golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167
134134
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
135135
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4
136-
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2
136+
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094
137137
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
138138
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8
139139
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
@@ -143,15 +143,15 @@ require (
143143
golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478
144144
golang.zx2c4.com/wireguard/tun/netstack v0.0.0-00010101000000-000000000000
145145
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220504211119-3d4a969bb56b
146-
google.golang.org/api v0.90.0
146+
google.golang.org/api v0.94.0
147147
google.golang.org/protobuf v1.28.0
148148
gopkg.in/natefinch/lumberjack.v2 v2.0.0
149149
gopkg.in/yaml.v3 v3.0.1
150150
gvisor.dev/gvisor v0.0.0-20220801230058-850e42eb4444
151151
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9
152152
nhooyr.io/websocket v1.8.7
153153
storj.io/drpc v0.0.33-0.20220622181519-9206537a4db7
154-
tailscale.com v1.26.2
154+
tailscale.com v1.30.0
155155
)
156156

157157
require (

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2170,8 +2170,8 @@ golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j
21702170
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
21712171
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
21722172
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
2173-
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2 h1:+jnHzr9VPj32ykQVai5DNahi9+NSp7yYuCsl5eAQtL0=
2174-
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
2173+
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 h1:2o1E+E8TpNLklK9nHiPiK1uzIYrIHt+cQx3ynCwq9V8=
2174+
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
21752175
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
21762176
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
21772177
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -2563,8 +2563,8 @@ google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69
25632563
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
25642564
google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
25652565
google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
2566-
google.golang.org/api v0.90.0 h1:WMnUWAvihIClUYFNeFA69VTuR3duKS3IalMGDQcLvq8=
2567-
google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
2566+
google.golang.org/api v0.94.0 h1:KtKM9ru3nzQioV1HLlUf1cR7vMYJIpgls5VhAYQXIwA=
2567+
google.golang.org/api v0.94.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI=
25682568
google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
25692569
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
25702570
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy