Skip to content

feat: force legacy tunnels to new version #2914

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion coderd/devtunnel/servers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Region struct {

type Node struct {
ID int `json:"id"`
RegionID int `json:"region_id"`
HostnameHTTPS string `json:"hostname_https"`
HostnameWireguard string `json:"hostname_wireguard"`
WireguardPort uint16 `json:"wireguard_port"`
Expand All @@ -29,11 +30,12 @@ type Node struct {

var Regions = []Region{
{
ID: 1,
ID: 0,
LocationName: "US East Pittsburgh",
Nodes: []Node{
{
ID: 1,
RegionID: 0,
HostnameHTTPS: "pit-1.try.coder.app",
HostnameWireguard: "pit-1.try.coder.app",
WireguardPort: 55551,
Expand Down
100 changes: 36 additions & 64 deletions coderd/devtunnel/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,52 +3,42 @@ package devtunnel
import (
"bytes"
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"os"
"path/filepath"
"time"

"github.com/google/uuid"
"github.com/briandowns/spinner"
"golang.org/x/xerrors"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"golang.zx2c4.com/wireguard/tun/netstack"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"

"cdr.dev/slog"
"github.com/coder/coder/cli/cliui"
"github.com/coder/coder/cryptorand"
)

var (
v0EndpointHTTPS = "wg-tunnel.coder.app"

v0ServerPublicKey = "+KNSMwed/IlqoesvTMSBNsHFaKVLrmmaCkn0bxIhUg0="
v0ServerIP = netip.AddrFrom16(uuid.MustParse("fcad0000-0000-4000-8000-000000000001"))
)

type Tunnel struct {
URL string
Listener net.Listener
}

type Config struct {
Version int `json:"version"`
ID uuid.UUID `json:"id"`
PrivateKey device.NoisePrivateKey `json:"private_key"`
PublicKey device.NoisePublicKey `json:"public_key"`

Tunnel Node `json:"tunnel"`
}
type configExt struct {
Version int `json:"-"`
ID uuid.UUID `json:"id"`
PrivateKey device.NoisePrivateKey `json:"-"`
PublicKey device.NoisePublicKey `json:"public_key"`

Expand Down Expand Up @@ -146,7 +136,7 @@ func startUpdateRoutine(ctx context.Context, logger slog.Logger, cfg Config) (Se
endCh := make(chan struct{})
go func() {
defer close(endCh)
ticker := time.NewTicker(30 * time.Second)
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()

for {
Expand Down Expand Up @@ -179,22 +169,9 @@ func sendConfigToServer(ctx context.Context, cfg Config) (ServerResponse, error)
return ServerResponse{}, xerrors.Errorf("marshal config: %w", err)
}

var req *http.Request
switch cfg.Version {
case 0:
req, err = http.NewRequestWithContext(ctx, "POST", "https://"+v0EndpointHTTPS+"/tun", bytes.NewReader(raw))
if err != nil {
return ServerResponse{}, xerrors.Errorf("new request: %w", err)
}

case 1:
req, err = http.NewRequestWithContext(ctx, "POST", "https://"+cfg.Tunnel.HostnameHTTPS+"/tun", bytes.NewReader(raw))
if err != nil {
return ServerResponse{}, xerrors.Errorf("new request: %w", err)
}

default:
return ServerResponse{}, xerrors.Errorf("unknown config version: %d", cfg.Version)
req, err := http.NewRequestWithContext(ctx, "POST", "https://"+cfg.Tunnel.HostnameHTTPS+"/tun", bytes.NewReader(raw))
if err != nil {
return ServerResponse{}, xerrors.Errorf("new request: %w", err)
}

res, err := http.DefaultClient.Do(req)
Expand All @@ -204,23 +181,9 @@ func sendConfigToServer(ctx context.Context, cfg Config) (ServerResponse, error)
defer res.Body.Close()

var resp ServerResponse
switch cfg.Version {
case 0:
_, _ = io.Copy(io.Discard, res.Body)
resp.Hostname = fmt.Sprintf("%s.%s", cfg.ID, v0EndpointHTTPS)
resp.ServerIP = v0ServerIP
resp.ServerPublicKey = encodeBase64ToHex(v0ServerPublicKey)
resp.ClientIP = netip.AddrFrom16(cfg.ID)

case 1:
err := json.NewDecoder(res.Body).Decode(&resp)
if err != nil {
return ServerResponse{}, xerrors.Errorf("decode response: %w", err)
}

default:
_, _ = io.Copy(io.Discard, res.Body)
return ServerResponse{}, xerrors.Errorf("unknown config version: %d", cfg.Version)
err = json.NewDecoder(res.Body).Decode(&resp)
if err != nil {
return ServerResponse{}, xerrors.Errorf("decode response: %w", err)
}

return resp, nil
Expand Down Expand Up @@ -273,12 +236,22 @@ func readOrGenerateConfig() (Config, error) {
}

if cfg.Version == 0 {
cfg.Tunnel = Node{
ID: 0,
HostnameHTTPS: "wg-tunnel.coder.app",
HostnameWireguard: "wg-tunnel-udp.coder.app",
WireguardPort: 55555,
_, _ = fmt.Println()
_, _ = fmt.Println(cliui.Styles.Error.Render("You're running a deprecated tunnel version!"))
_, _ = fmt.Println(cliui.Styles.Error.Render("Upgrading you to the new version now. You will need to rebuild running workspaces."))
_, _ = fmt.Println()

cfg, err := GenerateConfig()
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
Expand All @@ -291,15 +264,27 @@ func GenerateConfig() (Config, error) {
}
pub := priv.PublicKey()

spin := spinner.New(spinner.CharSets[39], 350*time.Millisecond)
spin.Suffix = " Finding the closest tunnel region..."
spin.Start()

node, err := FindClosestNode()
if err != nil {
// If we fail to find the closest node, default to US East.
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)
}

spin.Stop()
_, _ = fmt.Printf("Using tunnel in %s with latency %s.\n",
cliui.Styles.Keyword.Render(Regions[node.RegionID].LocationName),
cliui.Styles.Code.Render(node.AvgLatency.String()),
)

return Config{
Version: 1,
PrivateKey: device.NoisePrivateKey(priv),
Expand All @@ -326,16 +311,3 @@ func writeConfig(cfg Config) error {

return nil
}

func encodeBase64ToHex(key string) string {
decoded, err := base64.StdEncoding.DecodeString(key)
if err != nil {
panic(err)
}

if len(decoded) != 32 {
panic((xerrors.New("key should be 32 bytes: " + key)))
}

return hex.EncodeToString(decoded)
}