blob: 3696342022c9463b3cd714537f5c13ea0fc51d99 [file] [log] [blame]
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +04001package main
2
3import (
gio721c0042025-04-03 11:56:36 +04004 "bytes"
giob1c4e542024-07-15 12:10:52 +04005 "encoding/base64"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +04006 "encoding/json"
gioc76baed2024-08-19 22:04:57 +04007 "errors"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +04008 "flag"
9 "fmt"
10 "io"
gioc76baed2024-08-19 22:04:57 +040011 "io/fs"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040012 "log"
gioc76baed2024-08-19 22:04:57 +040013 "math/rand"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040014 "net/http"
15 "os"
gio721c0042025-04-03 11:56:36 +040016 "path/filepath"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040017 "strconv"
18 "strings"
gioefa0ed42024-06-13 12:31:43 +040019 "sync"
20 "time"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040021
gio721c0042025-04-03 11:56:36 +040022 "github.com/giolekva/pcloud/core/installer"
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040023 "github.com/giolekva/pcloud/core/installer/soft"
24
25 "golang.org/x/crypto/ssh"
26)
27
Davit Tabidze6bf29832024-06-17 16:51:54 +040028const (
29 secretLength = 20
gioc76baed2024-08-19 22:04:57 +040030 start = 49152
31 end = 65535
Davit Tabidze6bf29832024-06-17 16:51:54 +040032)
33
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040034var port = flag.Int("port", 8080, "Port to listen on")
35var repoAddr = flag.String("repo-addr", "", "Git repository address where Helm releases are stored")
36var sshKey = flag.String("ssh-key", "", "Path to SHH key used to connect with Git repository")
37var ingressNginxPath = flag.String("ingress-nginx-path", "", "Path to the ingress-nginx Helm release")
giod28f83c2024-08-15 10:53:40 +040038var minPreOpenPorts = flag.Int("min-pre-open-ports", 5, "Minimum number of pre-open ports to keep in reserve")
39var preOpenPortsBatchSize = flag.Int("pre-open-ports-batch-size", 10, "Number of new ports to open at a time")
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040040
41type client interface {
gio721c0042025-04-03 11:56:36 +040042 ReservePort(remoteProxy bool) (int, string, error)
gioc76baed2024-08-19 22:04:57 +040043 ReleaseReservedPort(port ...int)
giod28f83c2024-08-15 10:53:40 +040044 AddPortForwarding(protocol string, port int, secret, dest string) error
45 RemovePortForwarding(protocol string, port int) error
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040046}
47
gio721c0042025-04-03 11:56:36 +040048type Reservation struct {
49 Secret string `json:"secret"`
50 IsRemoteProxy bool `json:"isRemoteProxy"`
51}
52
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040053type repoClient struct {
giod28f83c2024-08-15 10:53:40 +040054 l sync.Locker
55 repo soft.RepoIO
56 path string
gioc76baed2024-08-19 22:04:57 +040057 secretGenerator SecretGenerator
gio721c0042025-04-03 11:56:36 +040058 proxyCfg *installer.NginxProxyConfigurator
giod28f83c2024-08-15 10:53:40 +040059 minPreOpenPorts int
60 preOpenPortsBatchSize int
61 preOpenPorts []int
gio721c0042025-04-03 11:56:36 +040062 proxyPreOpenPorts []int
giod28f83c2024-08-15 10:53:40 +040063 blocklist map[int]struct{}
gio721c0042025-04-03 11:56:36 +040064 reserve map[int]Reservation
gioc76baed2024-08-19 22:04:57 +040065 availablePorts []int
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +040066}
67
gio721c0042025-04-03 11:56:36 +040068func getProxyBackendConfigPath(repo soft.RepoIO, path string) (string, error) {
69 cfgPath := filepath.Join(filepath.Dir(path), "proxy-backend-config.yaml")
70 inp, err := repo.Reader(cfgPath)
71 if err != nil {
72 return "", nil
73 }
74 defer inp.Close()
75 return cfgPath, nil
76}
77
giod28f83c2024-08-15 10:53:40 +040078func newRepoClient(
79 repo soft.RepoIO,
80 path string,
81 minPreOpenPorts int,
82 preOpenPortsBatchSize int,
gioc76baed2024-08-19 22:04:57 +040083 secretGenerator SecretGenerator,
giod28f83c2024-08-15 10:53:40 +040084) (client, error) {
gio721c0042025-04-03 11:56:36 +040085 proxyCfg, err := getProxyBackendConfigPath(repo, path)
86 if err != nil {
87 return nil, err
88 }
89 var cnc *installer.NginxProxyConfigurator
90 if proxyCfg != "" {
91 cnc = &installer.NginxProxyConfigurator{
92 Repo: repo,
93 ConfigPath: proxyCfg,
94 ServicePath: filepath.Join(filepath.Dir(proxyCfg), "proxy-backend-service.yaml"),
95 }
96 }
giod28f83c2024-08-15 10:53:40 +040097 ret := &repoClient{
98 l: &sync.Mutex{},
99 repo: repo,
100 path: path,
gioc76baed2024-08-19 22:04:57 +0400101 secretGenerator: secretGenerator,
gio721c0042025-04-03 11:56:36 +0400102 proxyCfg: cnc,
giod28f83c2024-08-15 10:53:40 +0400103 minPreOpenPorts: minPreOpenPorts,
104 preOpenPortsBatchSize: preOpenPortsBatchSize,
gioc76baed2024-08-19 22:04:57 +0400105 preOpenPorts: []int{},
106 blocklist: map[int]struct{}{},
gio721c0042025-04-03 11:56:36 +0400107 reserve: map[int]Reservation{},
gioc76baed2024-08-19 22:04:57 +0400108 availablePorts: []int{},
giod28f83c2024-08-15 10:53:40 +0400109 }
gioc76baed2024-08-19 22:04:57 +0400110 st, err := ret.readState(repo)
giod28f83c2024-08-15 10:53:40 +0400111 if err != nil {
gioc76baed2024-08-19 22:04:57 +0400112 if !errors.Is(err, fs.ErrNotExist) {
giod28f83c2024-08-15 10:53:40 +0400113 return nil, err
114 }
gioc76baed2024-08-19 22:04:57 +0400115 } else {
116 ret.preOpenPorts = st.PreOpenPorts
117 ret.blocklist = st.Blocklist
118 ret.reserve = st.Reserve
giod28f83c2024-08-15 10:53:40 +0400119 }
gioc76baed2024-08-19 22:04:57 +0400120 for i := start; i < end; i++ {
121 if _, ok := ret.blocklist[i]; !ok {
122 ret.availablePorts = append(ret.availablePorts, i)
123 }
124 }
125 if err := ret.preOpenNewPorts(); err != nil {
126 return nil, err
127 }
128 var reservedPorts []int
129 for k := range ret.reserve {
130 reservedPorts = append(reservedPorts, k)
131 }
132 go func() {
133 time.Sleep(30 * time.Minute)
134 ret.ReleaseReservedPort(reservedPorts...)
135 }()
giod28f83c2024-08-15 10:53:40 +0400136 return ret, nil
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400137}
138
gio721c0042025-04-03 11:56:36 +0400139func (c *repoClient) ReservePort(remoteProxy bool) (int, string, error) {
giod28f83c2024-08-15 10:53:40 +0400140 c.l.Lock()
141 defer c.l.Unlock()
gio721c0042025-04-03 11:56:36 +0400142 var port int
143 if !remoteProxy {
144 if len(c.preOpenPorts) == 0 {
145 return -1, "", fmt.Errorf("no pre-open ports are available")
146 }
147 port = c.preOpenPorts[0]
148 c.preOpenPorts = c.preOpenPorts[1:]
149 } else {
150 if c.proxyCfg == nil {
151 return -1, "", fmt.Errorf("does not support TCP/UDP proxy")
152 }
153 if len(c.proxyPreOpenPorts) == 0 {
154 return -1, "", fmt.Errorf("no proxy pre-open ports are available")
155 }
156 port = c.proxyPreOpenPorts[0]
157 c.proxyPreOpenPorts = c.proxyPreOpenPorts[1:]
giod28f83c2024-08-15 10:53:40 +0400158 }
gioc76baed2024-08-19 22:04:57 +0400159 secret, err := c.secretGenerator()
giod28f83c2024-08-15 10:53:40 +0400160 if err != nil {
161 return -1, "", err
162 }
gio721c0042025-04-03 11:56:36 +0400163 c.reserve[port] = Reservation{secret, remoteProxy}
giod28f83c2024-08-15 10:53:40 +0400164 return port, secret, nil
165}
166
gioc76baed2024-08-19 22:04:57 +0400167func (c *repoClient) ReleaseReservedPort(port ...int) {
168 if len(port) == 0 {
169 return
170 }
giod28f83c2024-08-15 10:53:40 +0400171 c.l.Lock()
172 defer c.l.Unlock()
gioc76baed2024-08-19 22:04:57 +0400173 if _, err := c.repo.Do(func(fs soft.RepoFS) (string, error) {
174 for _, p := range port {
gio721c0042025-04-03 11:56:36 +0400175 r, ok := c.reserve[p]
176 if !ok {
177 continue
178 }
gioc76baed2024-08-19 22:04:57 +0400179 delete(c.reserve, p)
gio721c0042025-04-03 11:56:36 +0400180 if r.IsRemoteProxy {
181 c.proxyPreOpenPorts = append(c.proxyPreOpenPorts, p)
182 } else {
183 c.preOpenPorts = append(c.preOpenPorts, p)
184 }
gioc76baed2024-08-19 22:04:57 +0400185 }
186 if err := c.writeState(fs); err != nil {
187 return "", err
188 }
189 return fmt.Sprintf("Released port reservations: %+v", port), nil
190 }); err != nil {
191 panic(err)
192 }
giod28f83c2024-08-15 10:53:40 +0400193}
194
gio721c0042025-04-03 11:56:36 +0400195type oldState struct {
196 PreOpenPorts []int `json:"preOpenPorts"`
197 ProxyPreOpenPorts []int `json:"proxyPreOpenPorts"`
198 Blocklist map[int]struct{} `json:"blocklist"`
199 Reserve map[int]string `json:"reserve"`
200}
201
giod28f83c2024-08-15 10:53:40 +0400202type state struct {
gio721c0042025-04-03 11:56:36 +0400203 PreOpenPorts []int `json:"preOpenPorts"`
204 ProxyPreOpenPorts []int `json:"proxyPreOpenPorts"`
205 Blocklist map[int]struct{} `json:"blocklist"`
206 Reserve map[int]Reservation `json:"reserve"`
giod28f83c2024-08-15 10:53:40 +0400207}
208
209func (c *repoClient) preOpenNewPorts() error {
210 c.l.Lock()
211 defer c.l.Unlock()
gio721c0042025-04-03 11:56:36 +0400212 var ports []int
213 if len(c.preOpenPorts) < c.minPreOpenPorts {
214 for count := c.preOpenPortsBatchSize; count > 0; count-- {
215 if len(c.availablePorts) == 0 {
216 return fmt.Errorf("could not open new port")
217 }
218 r := rand.Intn(len(c.availablePorts))
219 p := c.availablePorts[r]
220 c.availablePorts[r] = c.availablePorts[len(c.availablePorts)-1]
221 c.availablePorts = c.availablePorts[:len(c.availablePorts)-1]
222 ports = append(ports, p)
223 c.preOpenPorts = append(c.preOpenPorts, p)
224 c.blocklist[p] = struct{}{}
225 }
giod28f83c2024-08-15 10:53:40 +0400226 }
gio721c0042025-04-03 11:56:36 +0400227 if c.proxyCfg != nil && len(c.proxyPreOpenPorts) < c.minPreOpenPorts {
228 for count := c.preOpenPortsBatchSize; count > 0; count-- {
229 if len(c.availablePorts) == 0 {
230 return fmt.Errorf("could not open new port")
231 }
232 r := rand.Intn(len(c.availablePorts))
233 p := c.availablePorts[r]
234 c.availablePorts[r] = c.availablePorts[len(c.availablePorts)-1]
235 c.availablePorts = c.availablePorts[:len(c.availablePorts)-1]
236 ports = append(ports, p)
237 c.proxyPreOpenPorts = append(c.proxyPreOpenPorts, p)
238 c.blocklist[p] = struct{}{}
giod28f83c2024-08-15 10:53:40 +0400239 }
gio721c0042025-04-03 11:56:36 +0400240 }
241 if len(ports) == 0 {
242 return nil
giod28f83c2024-08-15 10:53:40 +0400243 }
gioc76baed2024-08-19 22:04:57 +0400244 _, err := c.repo.Do(func(fs soft.RepoFS) (string, error) {
giod28f83c2024-08-15 10:53:40 +0400245 if err := c.writeState(fs); err != nil {
246 return "", err
247 }
248 rel, err := c.readRelease(fs)
249 if err != nil {
250 return "", err
251 }
gio721c0042025-04-03 11:56:36 +0400252 svcType := ""
253 svcEnabled, err := extractBool(rel, "spec.values.controller.service.enabled")
giod28f83c2024-08-15 10:53:40 +0400254 if err != nil {
255 return "", err
256 }
gio721c0042025-04-03 11:56:36 +0400257 if svcEnabled {
258 svcType, err = extractString(rel, "spec.values.controller.service.type")
259 if err != nil {
260 return "", err
261 }
262 }
gioa344a2a2024-08-16 17:13:48 +0400263 if svcType == "NodePort" {
264 tcp, err := extractPorts(rel, "spec.values.controller.service.nodePorts.tcp")
265 if err != nil {
266 return "", err
267 }
268 udp, err := extractPorts(rel, "spec.values.controller.service.nodePorts.udp")
269 if err != nil {
270 return "", err
271 }
272 for _, p := range ports {
273 ps := strconv.Itoa(p)
274 tcp[ps] = p
275 udp[ps] = p
276 }
277 if err := c.writeRelease(fs, rel); err != nil {
278 return "", err
279 }
giod28f83c2024-08-15 10:53:40 +0400280 }
gioa344a2a2024-08-16 17:13:48 +0400281 fmt.Printf("Pre opened new ports: %+v\n", ports)
giod28f83c2024-08-15 10:53:40 +0400282 return "preopen new ports", nil
283 })
gioc76baed2024-08-19 22:04:57 +0400284 return err
giod28f83c2024-08-15 10:53:40 +0400285}
286
287func (c *repoClient) AddPortForwarding(protocol string, port int, secret, dest string) error {
gioc76baed2024-08-19 22:04:57 +0400288 protocol = strings.ToLower(protocol)
giod28f83c2024-08-15 10:53:40 +0400289 defer func() {
gioc76baed2024-08-19 22:04:57 +0400290 if err := c.preOpenNewPorts(); err != nil {
291 panic(err)
292 }
giod28f83c2024-08-15 10:53:40 +0400293 }()
294 c.l.Lock()
295 defer c.l.Unlock()
gio721c0042025-04-03 11:56:36 +0400296 r, ok := c.reserve[port]
297 if !ok || r.Secret != secret {
giod28f83c2024-08-15 10:53:40 +0400298 return fmt.Errorf("wrong secret")
299 }
300 delete(c.reserve, port)
gio721c0042025-04-03 11:56:36 +0400301 if r.IsRemoteProxy {
302 if c.proxyCfg == nil {
303 return fmt.Errorf("does not support TCP/UDP proxy")
304 }
gio721c0042025-04-03 11:56:36 +0400305 switch strings.ToLower(protocol) {
306 case "tcp":
giof55ab362025-04-11 17:48:17 +0400307 if _, err := c.proxyCfg.AddProxy(port, dest, installer.ProtocolTCP); err != nil {
gio721c0042025-04-03 11:56:36 +0400308 return err
309 }
310 case "udp":
giof55ab362025-04-11 17:48:17 +0400311 if _, err := c.proxyCfg.AddProxy(port, dest, installer.ProtocolUDP); err != nil {
gio721c0042025-04-03 11:56:36 +0400312 return err
313 }
314 default:
315 return fmt.Errorf("unknown protocol: %s", protocol)
316 }
gio721c0042025-04-03 11:56:36 +0400317 }
gioc76baed2024-08-19 22:04:57 +0400318 _, err := c.repo.Do(func(fs soft.RepoFS) (string, error) {
giod28f83c2024-08-15 10:53:40 +0400319 if err := c.writeState(fs); err != nil {
320 return "", err
321 }
322 rel, err := c.readRelease(fs)
323 if err != nil {
324 return "", err
325 }
326 portStr := strconv.Itoa(port)
giof55ab362025-04-11 17:48:17 +0400327 var portMap map[string]any
328 base := "spec.values"
329 if r.IsRemoteProxy {
330 base = "spec.values.controller.service.extraPorts"
331 dest = portStr
332 }
giod28f83c2024-08-15 10:53:40 +0400333 switch protocol {
334 case "tcp":
giof55ab362025-04-11 17:48:17 +0400335 portMap, err = extractPorts(rel, fmt.Sprintf("%s.tcp", base))
giod28f83c2024-08-15 10:53:40 +0400336 if err != nil {
337 return "", err
338 }
giod28f83c2024-08-15 10:53:40 +0400339 case "udp":
giof55ab362025-04-11 17:48:17 +0400340 portMap, err = extractPorts(rel, fmt.Sprintf("%s.udp", base))
giod28f83c2024-08-15 10:53:40 +0400341 if err != nil {
342 return "", err
343 }
giod28f83c2024-08-15 10:53:40 +0400344 default:
345 panic("MUST NOT REACH")
346 }
giof55ab362025-04-11 17:48:17 +0400347 portMap[portStr] = dest
giod28f83c2024-08-15 10:53:40 +0400348 if err := c.writeRelease(fs, rel); err != nil {
349 return "", err
350 }
351 return fmt.Sprintf("ingress: port %s map %d %s", protocol, port, dest), nil
352 })
gioc76baed2024-08-19 22:04:57 +0400353 return err
giod28f83c2024-08-15 10:53:40 +0400354}
355
356func (c *repoClient) RemovePortForwarding(protocol string, port int) error {
gioc76baed2024-08-19 22:04:57 +0400357 protocol = strings.ToLower(protocol)
giod28f83c2024-08-15 10:53:40 +0400358 c.l.Lock()
359 defer c.l.Unlock()
gioc76baed2024-08-19 22:04:57 +0400360 _, err := c.repo.Do(func(fs soft.RepoFS) (string, error) {
giod28f83c2024-08-15 10:53:40 +0400361 rel, err := c.readRelease(fs)
362 if err != nil {
363 return "", err
364 }
365 switch protocol {
366 case "tcp":
367 tcp, err := extractPorts(rel, "spec.values.tcp")
368 if err != nil {
369 return "", err
370 }
371 if err := removePort(tcp, port); err != nil {
372 return "", err
373 }
374 case "udp":
375 udp, err := extractPorts(rel, "spec.values.udp")
376 if err != nil {
377 return "", err
378 }
379 if err := removePort(udp, port); err != nil {
380 return "", err
381 }
382 default:
383 panic("MUST NOT REACH")
384 }
gio721c0042025-04-03 11:56:36 +0400385 svcType := ""
386 svcEnabled, err := extractBool(rel, "spec.values.controller.service.enabled")
giod28f83c2024-08-15 10:53:40 +0400387 if err != nil {
388 return "", err
389 }
gio721c0042025-04-03 11:56:36 +0400390 if svcEnabled {
391 svcType, err = extractString(rel, "spec.values.controller.service.type")
392 if err != nil {
393 return "", err
394 }
395 }
gioa344a2a2024-08-16 17:13:48 +0400396 if svcType == "NodePort" {
397 svcTCP, err := extractPorts(rel, "spec.values.controller.service.nodePorts.tcp")
398 if err != nil {
399 return "", err
400 }
401 svcUDP, err := extractPorts(rel, "spec.values.controller.service.nodePorts.udp")
402 if err != nil {
403 return "", err
404 }
405 if err := removePort(svcTCP, port); err != nil {
406 return "", err
407 }
408 if err := removePort(svcUDP, port); err != nil {
409 return "", err
410 }
giod28f83c2024-08-15 10:53:40 +0400411 }
412 if err := c.writeRelease(fs, rel); err != nil {
413 return "", err
414 }
415 return fmt.Sprintf("ingress: remove %s port map %d", protocol, port), nil
416 })
gioc76baed2024-08-19 22:04:57 +0400417 return err
418}
419
420func (c *repoClient) readState(fs soft.RepoFS) (state, error) {
421 r, err := fs.Reader(fmt.Sprintf("%s-state.json", c.path))
422 if err != nil {
423 return state{}, err
424 }
425 defer r.Close()
gio721c0042025-04-03 11:56:36 +0400426 buf, err := io.ReadAll(r)
427 if err != nil {
gioc76baed2024-08-19 22:04:57 +0400428 return state{}, err
429 }
gio721c0042025-04-03 11:56:36 +0400430 var ret state
431 if err := json.NewDecoder(bytes.NewReader(buf)).Decode(&ret); err == nil {
432 return ret, nil
433 }
434 var old oldState
435 if err := json.NewDecoder(bytes.NewReader(buf)).Decode(&old); err != nil {
436 return state{}, err
437 }
438 ret = state{
439 PreOpenPorts: old.PreOpenPorts,
440 ProxyPreOpenPorts: []int{},
441 Blocklist: old.Blocklist,
442 Reserve: map[int]Reservation{},
443 }
444 for port, secret := range old.Reserve {
445 ret.Reserve[port] = Reservation{secret, false}
446 }
gioc76baed2024-08-19 22:04:57 +0400447 return ret, err
giod28f83c2024-08-15 10:53:40 +0400448}
449
450func (c *repoClient) writeState(fs soft.RepoFS) error {
451 w, err := fs.Writer(fmt.Sprintf("%s-state.json", c.path))
452 if err != nil {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400453 return err
454 }
giod28f83c2024-08-15 10:53:40 +0400455 defer w.Close()
gio721c0042025-04-03 11:56:36 +0400456 if err := json.NewEncoder(w).Encode(state{c.preOpenPorts, c.proxyPreOpenPorts, c.blocklist, c.reserve}); err != nil {
giod28f83c2024-08-15 10:53:40 +0400457 return err
458 }
459 return err
460}
461
462func (c *repoClient) readRelease(fs soft.RepoFS) (map[string]any, error) {
463 ret := map[string]any{}
464 if err := soft.ReadYaml(fs, c.path, &ret); err != nil {
465 return nil, err
466 }
467 return ret, nil
468}
469
470func (c *repoClient) writeRelease(fs soft.RepoFS, rel map[string]any) error {
471 return soft.WriteYaml(fs, c.path, rel)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400472}
473
474type server struct {
giod28f83c2024-08-15 10:53:40 +0400475 s *http.Server
476 r *http.ServeMux
477 client client
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400478}
479
480func newServer(port int, client client) *server {
481 r := http.NewServeMux()
482 s := &http.Server{
483 Addr: fmt.Sprintf(":%d", port),
484 Handler: r,
485 }
giod28f83c2024-08-15 10:53:40 +0400486 return &server{s, r, client}
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400487}
488
489func (s *server) Start() error {
gioefa0ed42024-06-13 12:31:43 +0400490 s.r.HandleFunc("/api/reserve", s.handleReserve)
giocdfa3722024-06-13 20:10:14 +0400491 s.r.HandleFunc("/api/allocate", s.handleAllocate)
492 s.r.HandleFunc("/api/remove", s.handleRemove)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400493 if err := s.s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
494 return err
495 }
496 return nil
497}
498
499func (s *server) Close() error {
500 return s.s.Close()
501}
502
503type allocateReq struct {
504 Protocol string `json:"protocol"`
505 SourcePort int `json:"sourcePort"`
506 TargetService string `json:"targetService"`
507 TargetPort int `json:"targetPort"`
giobd7ab0b2024-06-17 12:55:17 +0400508 Secret string `json:"secret"`
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400509}
510
giocdfa3722024-06-13 20:10:14 +0400511type removeReq struct {
512 Protocol string `json:"protocol"`
513 SourcePort int `json:"sourcePort"`
514 TargetService string `json:"targetService"`
515 TargetPort int `json:"targetPort"`
516}
517
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400518func extractAllocateReq(r io.Reader) (allocateReq, error) {
519 var req allocateReq
520 if err := json.NewDecoder(r).Decode(&req); err != nil {
521 return allocateReq{}, err
522 }
523 req.Protocol = strings.ToLower(req.Protocol)
524 if req.Protocol != "tcp" && req.Protocol != "udp" {
525 return allocateReq{}, fmt.Errorf("Unexpected protocol %s", req.Protocol)
526 }
527 return req, nil
528}
529
giocdfa3722024-06-13 20:10:14 +0400530func extractRemoveReq(r io.Reader) (removeReq, error) {
531 var req removeReq
532 if err := json.NewDecoder(r).Decode(&req); err != nil {
533 return removeReq{}, err
534 }
535 req.Protocol = strings.ToLower(req.Protocol)
536 if req.Protocol != "tcp" && req.Protocol != "udp" {
537 return removeReq{}, fmt.Errorf("Unexpected protocol %s", req.Protocol)
538 }
539 return req, nil
540}
541
gioefa0ed42024-06-13 12:31:43 +0400542type reserveResp struct {
543 Port int `json:"port"`
544 Secret string `json:"secret"`
545}
546
gioa344a2a2024-08-16 17:13:48 +0400547func extractField(data map[string]any, path string) (any, error) {
548 var val any = data
giod28f83c2024-08-15 10:53:40 +0400549 for _, i := range strings.Split(path, ".") {
gioa344a2a2024-08-16 17:13:48 +0400550 valM, ok := val.(map[string]any)
551 if !ok {
gio721c0042025-04-03 11:56:36 +0400552 return nil, fmt.Errorf("expected map, %s", i)
gioa344a2a2024-08-16 17:13:48 +0400553 }
554 val, ok = valM[i]
giod28f83c2024-08-15 10:53:40 +0400555 if !ok {
556 return nil, fmt.Errorf("%s not found", i)
557 }
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400558 }
gioa344a2a2024-08-16 17:13:48 +0400559 return val, nil
560}
561
562func extractPorts(data map[string]any, path string) (map[string]any, error) {
563 ret, err := extractField(data, path)
564 if err != nil {
565 return nil, err
566 }
567 retM, ok := ret.(map[string]any)
568 if !ok {
569 return nil, fmt.Errorf("expected map")
570 }
571 return retM, nil
572}
573
574func extractString(data map[string]any, path string) (string, error) {
575 ret, err := extractField(data, path)
576 if err != nil {
577 return "", err
578 }
579 retS, ok := ret.(string)
580 if !ok {
gio721c0042025-04-03 11:56:36 +0400581 return "", fmt.Errorf("expected string")
582 }
583 return retS, nil
584}
585
586func extractBool(data map[string]any, path string) (bool, error) {
587 ret, err := extractField(data, path)
588 if err != nil {
589 return false, err
590 }
591 retS, ok := ret.(bool)
592 if !ok {
593 return false, fmt.Errorf("expected boolean")
gioa344a2a2024-08-16 17:13:48 +0400594 }
595 return retS, nil
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400596}
597
giod28f83c2024-08-15 10:53:40 +0400598func addPort(pm map[string]any, sourcePort int, targetService string, targetPort int) error {
599 sourcePortStr := strconv.Itoa(sourcePort)
600 if _, ok := pm[sourcePortStr]; ok || sourcePort == 80 || sourcePort == 443 || sourcePort == 22 {
601 return fmt.Errorf("port %d is already taken", sourcePort)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400602 }
giod28f83c2024-08-15 10:53:40 +0400603 pm[sourcePortStr] = fmt.Sprintf("%s:%d", targetService, targetPort)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400604 return nil
605}
606
giod28f83c2024-08-15 10:53:40 +0400607func removePort(pm map[string]any, port int) error {
608 sourcePortStr := strconv.Itoa(port)
giocdfa3722024-06-13 20:10:14 +0400609 if _, ok := pm[sourcePortStr]; !ok {
giod28f83c2024-08-15 10:53:40 +0400610 return fmt.Errorf("port %d is not open to remove", port)
giocdfa3722024-06-13 20:10:14 +0400611 }
612 delete(pm, sourcePortStr)
613 return nil
614}
615
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400616func (s *server) handleAllocate(w http.ResponseWriter, r *http.Request) {
617 if r.Method != http.MethodPost {
618 http.Error(w, "only post method is supported", http.StatusBadRequest)
619 return
620 }
621 req, err := extractAllocateReq(r.Body)
622 if err != nil {
giod28f83c2024-08-15 10:53:40 +0400623 fmt.Println(err.Error())
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400624 http.Error(w, err.Error(), http.StatusBadRequest)
625 return
626 }
giod28f83c2024-08-15 10:53:40 +0400627 if err := s.client.AddPortForwarding(
628 req.Protocol,
629 req.SourcePort,
630 req.Secret,
631 fmt.Sprintf("%s:%d", req.TargetService, req.TargetPort),
632 ); err != nil {
633 fmt.Println(err.Error())
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400634 http.Error(w, err.Error(), http.StatusInternalServerError)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400635 }
636}
637
gio721c0042025-04-03 11:56:36 +0400638type reserveReq struct {
639 RemoteProxy bool `json:"remoteProxy"`
640}
641
gioefa0ed42024-06-13 12:31:43 +0400642func (s *server) handleReserve(w http.ResponseWriter, r *http.Request) {
643 if r.Method != http.MethodPost {
644 http.Error(w, "only post method is supported", http.StatusBadRequest)
645 return
646 }
gio721c0042025-04-03 11:56:36 +0400647 var req reserveReq
648 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
649 fmt.Println(err.Error())
650 http.Error(w, err.Error(), http.StatusInternalServerError)
651 return
652 }
gioefa0ed42024-06-13 12:31:43 +0400653 var port int
giod28f83c2024-08-15 10:53:40 +0400654 var secret string
655 var err error
gio721c0042025-04-03 11:56:36 +0400656 if port, secret, err = s.client.ReservePort(req.RemoteProxy); err != nil {
giod28f83c2024-08-15 10:53:40 +0400657 fmt.Println(err.Error())
gioefa0ed42024-06-13 12:31:43 +0400658 http.Error(w, err.Error(), http.StatusInternalServerError)
659 return
660 }
gioefa0ed42024-06-13 12:31:43 +0400661 go func() {
662 time.Sleep(30 * time.Minute)
giod28f83c2024-08-15 10:53:40 +0400663 s.client.ReleaseReservedPort(port)
gioefa0ed42024-06-13 12:31:43 +0400664 }()
giod28f83c2024-08-15 10:53:40 +0400665 if err := json.NewEncoder(w).Encode(reserveResp{port, secret}); err != nil {
666 fmt.Println(err.Error())
gioefa0ed42024-06-13 12:31:43 +0400667 http.Error(w, err.Error(), http.StatusInternalServerError)
668 return
669 }
670}
671
giocdfa3722024-06-13 20:10:14 +0400672func (s *server) handleRemove(w http.ResponseWriter, r *http.Request) {
673 if r.Method != http.MethodPost {
674 http.Error(w, "only post method is supported", http.StatusBadRequest)
675 return
676 }
677 req, err := extractRemoveReq(r.Body)
678 if err != nil {
giod28f83c2024-08-15 10:53:40 +0400679 fmt.Println(err.Error())
giocdfa3722024-06-13 20:10:14 +0400680 http.Error(w, err.Error(), http.StatusBadRequest)
681 return
682 }
giod28f83c2024-08-15 10:53:40 +0400683 if err := s.client.RemovePortForwarding(req.Protocol, req.SourcePort); err != nil {
684 fmt.Println(err.Error())
giocdfa3722024-06-13 20:10:14 +0400685 http.Error(w, err.Error(), http.StatusInternalServerError)
686 return
687 }
giocdfa3722024-06-13 20:10:14 +0400688}
689
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400690// TODO(gio): deduplicate
gioe72b54f2024-04-22 10:44:41 +0400691func createRepoClient(addr string, keyPath string) (soft.RepoIO, error) {
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400692 sshKey, err := os.ReadFile(keyPath)
693 if err != nil {
694 return nil, err
695 }
696 signer, err := ssh.ParsePrivateKey(sshKey)
697 if err != nil {
698 return nil, err
699 }
700 repoAddr, err := soft.ParseRepositoryAddress(addr)
701 if err != nil {
702 return nil, err
703 }
704 repo, err := soft.CloneRepository(repoAddr, signer)
705 if err != nil {
706 return nil, err
707 }
gioff2a29a2024-05-01 17:06:42 +0400708 return soft.NewRepoIO(repo, signer)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400709}
710
gioc76baed2024-08-19 22:04:57 +0400711type SecretGenerator func() (string, error)
712
Davit Tabidze6bf29832024-06-17 16:51:54 +0400713func generateSecret() (string, error) {
714 b := make([]byte, secretLength)
715 _, err := rand.Read(b)
716 if err != nil {
717 return "", fmt.Errorf("error generating secret: %v", err)
718 }
giob1c4e542024-07-15 12:10:52 +0400719 return base64.StdEncoding.EncodeToString(b), nil
gioefa0ed42024-06-13 12:31:43 +0400720}
721
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400722func main() {
723 flag.Parse()
724 repo, err := createRepoClient(*repoAddr, *sshKey)
725 if err != nil {
726 log.Fatal(err)
727 }
giod28f83c2024-08-15 10:53:40 +0400728 c, err := newRepoClient(
729 repo,
730 *ingressNginxPath,
731 *minPreOpenPorts,
732 *preOpenPortsBatchSize,
gioc76baed2024-08-19 22:04:57 +0400733 generateSecret,
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400734 )
giod28f83c2024-08-15 10:53:40 +0400735 if err != nil {
736 log.Fatal(err)
737 }
738 s := newServer(*port, c)
Giorgi Lekveishvilib59b7c22024-04-03 22:17:50 +0400739 log.Fatal(s.Start())
740}