-
Notifications
You must be signed in to change notification settings - Fork 583
/
tunnel.go
205 lines (171 loc) · 5.29 KB
/
tunnel.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
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
}