mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
This change will improve over CLI performance and "snappiness" as well as substantially reduce our test times. Preliminary benchmarks show `coder server --help` times cut from 300ms to 120ms on my dogfood instance. The inefficiency of lipgloss disproportionately impacts our system, as all help text for every command is generated whenever any command is invoked. The `pretty` API could clean up a lot of the code (e.g., by replacing complex string concatenations with Printf), but this commit is too expansive as is so that work will be done in a follow up.
206 lines
5.3 KiB
Go
206 lines
5.3 KiB
Go
package devtunnel
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/briandowns/spinner"
|
|
"golang.org/x/xerrors"
|
|
"golang.zx2c4.com/wireguard/device"
|
|
|
|
"cdr.dev/slog"
|
|
"github.com/coder/coder/v2/cli/cliui"
|
|
"github.com/coder/coder/v2/cryptorand"
|
|
"github.com/coder/pretty"
|
|
"github.com/coder/wgtunnel/tunnelsdk"
|
|
)
|
|
|
|
type Config struct {
|
|
Version tunnelsdk.TunnelVersion `json:"version"`
|
|
PrivateKey device.NoisePrivateKey `json:"private_key"`
|
|
PublicKey device.NoisePublicKey `json:"public_key"`
|
|
|
|
Tunnel Node `json:"tunnel"`
|
|
|
|
// Used in testing. Normally this is nil, indicating to use DefaultClient.
|
|
HTTPClient *http.Client `json:"-"`
|
|
}
|
|
|
|
// NewWithConfig calls New with the given config. For documentation, see New.
|
|
func NewWithConfig(ctx context.Context, logger slog.Logger, cfg Config) (*tunnelsdk.Tunnel, error) {
|
|
u := &url.URL{
|
|
Scheme: "https",
|
|
Host: cfg.Tunnel.HostnameHTTPS,
|
|
}
|
|
|
|
c := tunnelsdk.New(u)
|
|
if cfg.HTTPClient != nil {
|
|
c.HTTPClient = cfg.HTTPClient
|
|
}
|
|
return c.LaunchTunnel(ctx, tunnelsdk.TunnelConfig{
|
|
Log: logger,
|
|
Version: cfg.Version,
|
|
PrivateKey: tunnelsdk.FromNoisePrivateKey(cfg.PrivateKey),
|
|
})
|
|
}
|
|
|
|
// New creates a tunnel with a public URL and returns a listener for incoming
|
|
// connections on that URL. Connections are made over the wireguard protocol.
|
|
// Tunnel configuration is cached in the user's config directory. Successive
|
|
// calls to New will always use the same URL. If multiple public URLs in
|
|
// parallel are required, use NewWithConfig.
|
|
//
|
|
// This uses https://github.com/coder/wgtunnel as the server and client
|
|
// implementation.
|
|
func New(ctx context.Context, logger slog.Logger, customTunnelHost string) (*tunnelsdk.Tunnel, error) {
|
|
cfg, err := readOrGenerateConfig(customTunnelHost)
|
|
if err != nil {
|
|
return nil, xerrors.Errorf("read or generate config: %w", err)
|
|
}
|
|
|
|
return NewWithConfig(ctx, logger, cfg)
|
|
}
|
|
|
|
func cfgPath() (string, error) {
|
|
cfgDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
return "", xerrors.Errorf("get user config dir: %w", err)
|
|
}
|
|
|
|
cfgDir = filepath.Join(cfgDir, "coderv2")
|
|
err = os.MkdirAll(cfgDir, 0o750)
|
|
if err != nil {
|
|
return "", xerrors.Errorf("mkdirall config dir %q: %w", cfgDir, err)
|
|
}
|
|
|
|
return filepath.Join(cfgDir, "devtunnel"), nil
|
|
}
|
|
|
|
func readOrGenerateConfig(customTunnelHost string) (Config, error) {
|
|
cfgFi, err := cfgPath()
|
|
if err != nil {
|
|
return Config{}, xerrors.Errorf("get config path: %w", err)
|
|
}
|
|
|
|
fi, err := os.ReadFile(cfgFi)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
cfg, err := GenerateConfig(customTunnelHost)
|
|
if err != nil {
|
|
return Config{}, xerrors.Errorf("generate config: %w", err)
|
|
}
|
|
|
|
err = writeConfig(cfg)
|
|
if err != nil {
|
|
return Config{}, xerrors.Errorf("write config: %w", err)
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
return Config{}, xerrors.Errorf("read config: %w", err)
|
|
}
|
|
|
|
cfg := Config{}
|
|
err = json.Unmarshal(fi, &cfg)
|
|
if err != nil {
|
|
return Config{}, xerrors.Errorf("unmarshal config: %w", err)
|
|
}
|
|
|
|
if cfg.Version == 0 {
|
|
_, _ = fmt.Println()
|
|
pretty.Printf(cliui.DefaultStyles.Error, "You're running a deprecated tunnel version.\n")
|
|
pretty.Printf(cliui.DefaultStyles.Error, "Upgrading you to the new version now. You will need to rebuild running workspaces.")
|
|
_, _ = fmt.Println()
|
|
|
|
cfg, err := GenerateConfig(customTunnelHost)
|
|
if err != nil {
|
|
return Config{}, xerrors.Errorf("generate config: %w", err)
|
|
}
|
|
|
|
err = writeConfig(cfg)
|
|
if err != nil {
|
|
return Config{}, xerrors.Errorf("write config: %w", err)
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func GenerateConfig(customTunnelHost string) (Config, error) {
|
|
priv, err := tunnelsdk.GeneratePrivateKey()
|
|
if err != nil {
|
|
return Config{}, xerrors.Errorf("generate private key: %w", err)
|
|
}
|
|
privNoisePublicKey, err := priv.NoisePrivateKey()
|
|
if err != nil {
|
|
return Config{}, xerrors.Errorf("generate noise private key: %w", err)
|
|
}
|
|
pubNoisePublicKey := priv.NoisePublicKey()
|
|
|
|
spin := spinner.New(spinner.CharSets[39], 350*time.Millisecond)
|
|
spin.Suffix = " Finding the closest tunnel region..."
|
|
spin.Start()
|
|
|
|
nodes, err := Nodes(customTunnelHost)
|
|
if err != nil {
|
|
return Config{}, xerrors.Errorf("get nodes: %w", err)
|
|
}
|
|
node, err := FindClosestNode(nodes)
|
|
if err != nil {
|
|
// If we fail to find the closest node, default to a random node from
|
|
// the first region.
|
|
region := Regions[0]
|
|
n, _ := cryptorand.Intn(len(region.Nodes))
|
|
node = region.Nodes[n]
|
|
spin.Stop()
|
|
_, _ = fmt.Println("Error picking closest dev tunnel:", err)
|
|
_, _ = fmt.Println("Defaulting to", Regions[0].LocationName)
|
|
}
|
|
|
|
locationName := "Unknown"
|
|
if node.RegionID < len(Regions) {
|
|
locationName = Regions[node.RegionID].LocationName
|
|
}
|
|
|
|
spin.Stop()
|
|
_, _ = fmt.Printf("Using tunnel in %s with latency %s.\n",
|
|
cliui.Keyword(locationName),
|
|
cliui.Code(node.AvgLatency.String()),
|
|
)
|
|
|
|
return Config{
|
|
Version: tunnelsdk.TunnelVersion2,
|
|
PrivateKey: privNoisePublicKey,
|
|
PublicKey: pubNoisePublicKey,
|
|
Tunnel: node,
|
|
}, nil
|
|
}
|
|
|
|
func writeConfig(cfg Config) error {
|
|
cfgFi, err := cfgPath()
|
|
if err != nil {
|
|
return xerrors.Errorf("get config path: %w", err)
|
|
}
|
|
|
|
raw, err := json.Marshal(cfg)
|
|
if err != nil {
|
|
return xerrors.Errorf("marshal config: %w", err)
|
|
}
|
|
|
|
err = os.WriteFile(cfgFi, raw, 0o600)
|
|
if err != nil {
|
|
return xerrors.Errorf("write file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|