Skip to content
This repository has been archived by the owner on Aug 30, 2024. It is now read-only.

Commit

Permalink
Add coder tunnel command (#313)
Browse files Browse the repository at this point in the history
* Add coder tunnel command

* Fix auth for p2p
  • Loading branch information
f0ssel authored Apr 8, 2021
1 parent 739c8e4 commit c1a9994
Show file tree
Hide file tree
Showing 5 changed files with 330 additions and 20 deletions.
1 change: 1 addition & 0 deletions docs/coder_config-ssh.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ coder config-ssh [flags]
```
--filepath string override the default path of your ssh config file (default "~/.ssh/config")
-h, --help help for config-ssh
--p2p (experimental) uses coder tunnel to proxy ssh connection
--remove remove the auto-generated Coder ssh config
```

Expand Down
48 changes: 34 additions & 14 deletions internal/cmd/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"golang.org/x/xerrors"
"nhooyr.io/websocket"

"cdr.dev/coder-cli/internal/x/xcobra"
"cdr.dev/coder-cli/internal/x/xwebrtc"
"cdr.dev/coder-cli/pkg/proto"
)
Expand All @@ -40,31 +39,42 @@ func agentCmd() *cobra.Command {

func startCmd() *cobra.Command {
var (
token string
token string
coderURL string
)
cmd := &cobra.Command{
Use: "start [coderURL] --token=[token]",
Args: xcobra.ExactArgs(1),
Use: "start --coder-url=[coder_url] --token=[token]",
Short: "starts the coder agent",
Long: "starts the coder agent",
Example: `# start the agent and connect with a Coder agent token
Example: `# start the agent and use CODER_URL and CODER_AGENT_TOKEN env vars
coder agent start https://my-coder.com --token xxxx-xxxx
coder agent start
# start the agent and use CODER_AGENT_TOKEN env var for auth token
# start the agent and connect with a specified url and agent token
coder agent start https://my-coder.com
coder agent start --coder-url https://my-coder.com --token xxxx-xxxx
`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
log := slog.Make(sloghuman.Sink(cmd.OutOrStdout()))

// Pull the URL from the args and do some sanity check.
rawURL := args[0]
if rawURL == "" || !strings.HasPrefix(rawURL, "http") {
if coderURL == "" {
var ok bool
token, ok = os.LookupEnv("CODER_URL")
if !ok {
client, err := newClient(ctx)
if err != nil {
return xerrors.New("must login, pass --coder-url flag, or set the CODER_URL env variable")
}
burl := client.BaseURL()
coderURL = burl.String()
}
}

if !strings.HasPrefix(coderURL, "http") {
return xerrors.Errorf("invalid URL")
}
u, err := url.Parse(rawURL)
u, err := url.Parse(coderURL)
if err != nil {
return xerrors.Errorf("parse url: %w", err)
}
Expand All @@ -79,18 +89,26 @@ coder agent start https://my-coder.com
}
}

if token == "" {
var ok bool
token, ok = os.LookupEnv("CODER_AGENT_TOKEN")
if !ok {
return xerrors.New("must pass --token or set the CODER_AGENT_TOKEN env variable")
}
}

q := u.Query()
q.Set("service_token", token)
u.RawQuery = q.Encode()

ctx, cancelFunc := context.WithTimeout(ctx, time.Second*15)
defer cancelFunc()
log.Info(ctx, "connecting to broker", slog.F("url", u.String()))
conn, res, err := websocket.Dial(ctx, u.String(), nil)
// nolint: bodyclose
conn, _, err := websocket.Dial(ctx, u.String(), nil)
if err != nil {
return fmt.Errorf("dial: %w", err)
}
_ = res.Body.Close()
nc := websocket.NetConn(context.Background(), conn, websocket.MessageBinary)
session, err := yamux.Server(nc, nil)
if err != nil {
Expand All @@ -112,6 +130,8 @@ coder agent start https://my-coder.com
}

cmd.Flags().StringVar(&token, "token", "", "coder agent token")
cmd.Flags().StringVar(&coderURL, "coder-url", "", "coder access url")

return cmd
}

Expand Down
1 change: 1 addition & 0 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func Make() *cobra.Command {
providersCmd(),
genDocsCmd(app),
agentCmd(),
tunnelCmd(),
)
app.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "show verbose output")
return app
Expand Down
27 changes: 21 additions & 6 deletions internal/cmd/configssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,23 @@ func configSSHCmd() *cobra.Command {
var (
configpath string
remove = false
p2p = false
)

cmd := &cobra.Command{
Use: "config-ssh",
Short: "Configure SSH to access Coder environments",
Long: "Inject the proper OpenSSH configuration into your local SSH config file.",
RunE: configSSH(&configpath, &remove),
RunE: configSSH(&configpath, &remove, &p2p),
}
cmd.Flags().StringVar(&configpath, "filepath", filepath.Join("~", ".ssh", "config"), "override the default path of your ssh config file")
cmd.Flags().BoolVar(&remove, "remove", false, "remove the auto-generated Coder ssh config")
cmd.Flags().BoolVar(&p2p, "p2p", false, "(experimental) uses coder tunnel to proxy ssh connection")

return cmd
}

func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []string) error {
func configSSH(configpath *string, remove *bool, p2p *bool) func(cmd *cobra.Command, _ []string) error {
return func(cmd *cobra.Command, _ []string) error {
ctx := cmd.Context()
usr, err := user.Current()
Expand Down Expand Up @@ -113,7 +115,7 @@ func configSSH(configpath *string, remove *bool) func(cmd *cobra.Command, _ []st
return xerrors.New("SSH is disabled or not available for any environments in your Coder deployment.")
}

newConfig := makeNewConfigs(user.Username, envsWithProviders, privateKeyFilepath)
newConfig := makeNewConfigs(user.Username, envsWithProviders, privateKeyFilepath, *p2p)

err = os.MkdirAll(filepath.Dir(*configpath), os.ModePerm)
if err != nil {
Expand Down Expand Up @@ -174,7 +176,7 @@ func writeSSHKey(ctx context.Context, client coder.Client, privateKeyPath string
return ioutil.WriteFile(privateKeyPath, []byte(key.PrivateKey), 0600)
}

func makeNewConfigs(userName string, envs []coderutil.EnvWithWorkspaceProvider, privateKeyFilepath string) string {
func makeNewConfigs(userName string, envs []coderutil.EnvWithWorkspaceProvider, privateKeyFilepath string, p2p bool) string {
newConfig := fmt.Sprintf("\n%s\n%s\n\n", sshStartToken, sshStartMessage)

sort.Slice(envs, func(i, j int) bool { return envs[i].Env.Name < envs[j].Env.Name })
Expand All @@ -192,14 +194,27 @@ func makeNewConfigs(userName string, envs []coderutil.EnvWithWorkspaceProvider,
clog.LogWarn("invalid access url", clog.Causef("malformed url: %q", env.WorkspaceProvider.EnvproxyAccessURL))
continue
}
newConfig += makeSSHConfig(u.Host, userName, env.Env.Name, privateKeyFilepath)
newConfig += makeSSHConfig(u.Host, userName, env.Env.Name, privateKeyFilepath, p2p)
}
newConfig += fmt.Sprintf("\n%s\n", sshEndToken)

return newConfig
}

func makeSSHConfig(host, userName, envName, privateKeyFilepath string) string {
func makeSSHConfig(host, userName, envName, privateKeyFilepath string, p2p bool) string {
if p2p {
return fmt.Sprintf(
`Host coder.%s
HostName localhost
ProxyCommand coder tunnel %s 22 stdio
StrictHostKeyChecking no
ConnectTimeout=0
IdentityFile="%s"
ServerAliveInterval 60
ServerAliveCountMax 3
`, envName, envName, privateKeyFilepath)
}

return fmt.Sprintf(
`Host coder.%s
HostName %s
Expand Down
Loading

0 comments on commit c1a9994

Please sign in to comment.