diff --git a/docker/images/visor/Dockerfile b/docker/images/visor/Dockerfile index 2576d49260..3ad3b669e2 100644 --- a/docker/images/visor/Dockerfile +++ b/docker/images/visor/Dockerfile @@ -20,6 +20,7 @@ RUN apk add --no-cache make git && \ cp ./apps/vpn-server /apps/ && \ cp ./apps/vpn-client /apps/ && \ cp ./apps/skysocks /apps/ && \ + cp ./apps/skysocks-client /apps/ && \ cp ./apps/skychat /apps/ ## Resulting image diff --git a/pkg/transport/network/addrresolver/client.go b/pkg/transport/network/addrresolver/client.go index 8136afdff8..d74cd5d3fb 100644 --- a/pkg/transport/network/addrresolver/client.go +++ b/pkg/transport/network/addrresolver/client.go @@ -27,12 +27,14 @@ import ( const ( // sudphPriority is used to set an order how connection filters apply. - sudphPriority = 1 - stcprBindPath = "/bind/stcpr" - addrChSize = 1024 - udpKeepAliveInterval = 10 * time.Second - udpKeepAliveMessage = "keepalive" - defaultUDPPort = "30178" + sudphPriority = 1 + stcprBindPath = "/bind/stcpr" + stcprHeartbeatPath = "/heartbeat/stcpr" + stcprKeepHeartbeatInterval = 300 * time.Second + addrChSize = 1024 + udpKeepHeartbeatInterval = 10 * time.Second + udpKeepHeartbeatMessage = "heartbeat" + defaultUDPPort = "30178" ) var ( @@ -216,6 +218,12 @@ func (c *httpClient) BindSTCPR(ctx context.Context, port string) error { return fmt.Errorf("status: %d, error: %w", resp.StatusCode, extractError(resp.Body)) } + go func() { + if err := c.keepStcprHeartbeatLoop(ctx); err != nil { + c.log.WithError(err).Errorf("Failed to send TCP heartbeat signal to address-resolver") + } + }() + return nil } @@ -273,8 +281,8 @@ func (c *httpClient) BindSUDPH(filter *pfilter.PacketFilter, hs Handshake) (<-ch addrCh := c.readSUDPHMessages(arConn) go func() { - if err := c.keepAliveLoop(arConn); err != nil { - c.log.WithError(err).Errorf("Failed to send keep alive UDP packet to address-resolver") + if err := c.keepSudphHeartbeatLoop(arConn); err != nil { + c.log.WithError(err).Errorf("Failed to send UDP heartbeat packet to address-resolver") } }() @@ -413,18 +421,36 @@ func (c *httpClient) Close() error { return nil } +// Keep stcpr heartbeat in address-resolver +func (c *httpClient) keepStcprHeartbeatLoop(ctx context.Context) error { + for { + _, err := c.Get(ctx, stcprHeartbeatPath) + if err != nil { + return err + } + + c.log.Debugf("Sent TCP heartbeat signal to address-resolver") + select { + case <-c.closed: + return nil + default: + time.Sleep(stcprKeepHeartbeatInterval) + } + } +} + // Keep NAT mapping alive. -func (c *httpClient) keepAliveLoop(w io.Writer) error { +func (c *httpClient) keepSudphHeartbeatLoop(w io.Writer) error { for { select { case <-c.closed: return nil default: - if _, err := w.Write([]byte(udpKeepAliveMessage)); err != nil { + if _, err := w.Write([]byte(udpKeepHeartbeatMessage)); err != nil { return err } - time.Sleep(udpKeepAliveInterval) + time.Sleep(udpKeepHeartbeatInterval) } } } diff --git a/pkg/transport/network/client.go b/pkg/transport/network/client.go index 53f1810269..7c5fb47c5d 100644 --- a/pkg/transport/network/client.go +++ b/pkg/transport/network/client.go @@ -308,12 +308,10 @@ type dialFunc func(ctx context.Context, addr string) (net.Conn, error) // and dials that visor address(es) // dial process is specific to transport type and is provided by the client func (c *resolvedClient) dialVisor(ctx context.Context, rPK cipher.PubKey, dial dialFunc) (net.Conn, error) { - c.log.Infof("Dialing PK %v", rPK) visorData, err := c.ar.Resolve(ctx, string(c.netType), rPK) if err != nil { return nil, fmt.Errorf("resolve PK: %w", err) } - c.log.Infof("Resolved PK %v to visor data %v", rPK, visorData) if visorData.IsLocal { for _, host := range visorData.Addresses { diff --git a/pkg/visor/api.go b/pkg/visor/api.go index 0240605687..744ff20dd4 100644 --- a/pkg/visor/api.go +++ b/pkg/visor/api.go @@ -76,6 +76,7 @@ type API interface { SetMinHops(uint16) error + GetPersistentTransports() ([]transport.PersistentTransports, error) SetPersistentTransports([]transport.PersistentTransports) error } @@ -787,3 +788,8 @@ func (v *Visor) SetMinHops(in uint16) error { func (v *Visor) SetPersistentTransports(pts []transport.PersistentTransports) error { return v.conf.UpdatePersistentTransports(pts) } + +// GetPersistentTransports sets min_hops routing config of visor +func (v *Visor) GetPersistentTransports() ([]transport.PersistentTransports, error) { + return v.conf.GetPersistentTransports() +} diff --git a/pkg/visor/hypervisor.go b/pkg/visor/hypervisor.go index 1b926f0573..03320cf629 100644 --- a/pkg/visor/hypervisor.go +++ b/pkg/visor/hypervisor.go @@ -262,6 +262,7 @@ func (hv *Hypervisor) makeMux() chi.Router { r.Get("/visors/{pk}/update/available/{channel}", hv.visorUpdateAvailable()) r.Get("/visors/{pk}/runtime-logs", hv.getRuntimeLogs()) r.Post("/visors/{pk}/min-hops", hv.postMinHops()) + r.Get("/visors/{pk}/persistent-transports", hv.getPersistentTransports()) r.Put("/visors/{pk}/persistent-transports", hv.putPersistentTransports()) }) }) @@ -1342,6 +1343,17 @@ func (hv *Hypervisor) putPersistentTransports() http.HandlerFunc { }) } +func (hv *Hypervisor) getPersistentTransports() http.HandlerFunc { + return hv.withCtx(hv.visorCtx, func(w http.ResponseWriter, r *http.Request, ctx *httpCtx) { + pts, err := ctx.API.GetPersistentTransports() + if err != nil { + httputil.WriteJSON(w, r, http.StatusInternalServerError, err) + return + } + httputil.WriteJSON(w, r, http.StatusOK, pts) + }) +} + /* <<< Helper functions >>> */ diff --git a/pkg/visor/rpc_client.go b/pkg/visor/rpc_client.go index 575e669041..6a8f993e0e 100644 --- a/pkg/visor/rpc_client.go +++ b/pkg/visor/rpc_client.go @@ -350,6 +350,13 @@ func (rc *rpcClient) SetPersistentTransports(pts []transport.PersistentTransport return err } +// GetPersistentTransports gets the persistent_transports from visor routing config +func (rc *rpcClient) GetPersistentTransports() ([]transport.PersistentTransports, error) { + var tps []transport.PersistentTransports + err := rc.Call("GetPersistentTransports", &struct{}{}, &tps) + return tps, err +} + // StatusMessage defines a status of visor update. type StatusMessage struct { Text string @@ -949,3 +956,8 @@ func (mc *mockRPCClient) SetMinHops(_ uint16) error { func (mc *mockRPCClient) SetPersistentTransports(_ []transport.PersistentTransports) error { return nil } + +// GetPersistentTransports implements API +func (mc *mockRPCClient) GetPersistentTransports() ([]transport.PersistentTransports, error) { + return []transport.PersistentTransports{}, nil +} diff --git a/static/skywire-manager-src/src/app/app.datatypes.ts b/static/skywire-manager-src/src/app/app.datatypes.ts index 0ddd59af99..28dfdfd0f8 100644 --- a/static/skywire-manager-src/src/app/app.datatypes.ts +++ b/static/skywire-manager-src/src/app/app.datatypes.ts @@ -7,6 +7,7 @@ export class Node { version: string; apps: Application[]; transports: Transport[]; + persistentTransports: PersistentTransport[]; routesCount: number; minHops: number; routes?: Route[]; @@ -28,13 +29,21 @@ export interface Application { } export interface Transport { - isUp: boolean; id: string; localPk: string; remotePk: string; type: string; recv: number|null; sent: number|null; + + // Calculated internally + isPersistent?: boolean; + notFound?: boolean; +} + +export interface PersistentTransport { + pk: string; + type: string; } export interface Route { diff --git a/static/skywire-manager-src/src/app/components/pages/node-list/node-list.component.html b/static/skywire-manager-src/src/app/components/pages/node-list/node-list.component.html index 6ce6c3ff95..fb6135f10a 100644 --- a/static/skywire-manager-src/src/app/components/pages/node-list/node-list.component.html +++ b/static/skywire-manager-src/src/app/components/pages/node-list/node-list.component.html @@ -81,7 +81,7 @@ - star_outline + star_outline {{ dataSorter.sortingArrow }} diff --git a/static/skywire-manager-src/src/app/components/pages/node-list/node-list.component.scss b/static/skywire-manager-src/src/app/components/pages/node-list/node-list.component.scss index 296715889c..ce07e8576f 100644 --- a/static/skywire-manager-src/src/app/components/pages/node-list/node-list.component.scss +++ b/static/skywire-manager-src/src/app/components/pages/node-list/node-list.component.scss @@ -23,10 +23,6 @@ color: $yellow; } -.gray-text { - color: $light-gray !important; -} - .small-column { width: 1px; } diff --git a/static/skywire-manager-src/src/app/components/pages/node/routing/all-transports/all-transports.component.html b/static/skywire-manager-src/src/app/components/pages/node/routing/all-transports/all-transports.component.html index 44f648e5c8..5c2f454be5 100644 --- a/static/skywire-manager-src/src/app/components/pages/node/routing/all-transports/all-transports.component.html +++ b/static/skywire-manager-src/src/app/components/pages/node/routing/all-transports/all-transports.component.html @@ -1,6 +1,5 @@ + *ngIf="node" + [node]="node" + [showShortList]="false"> diff --git a/static/skywire-manager-src/src/app/components/pages/node/routing/all-transports/all-transports.component.ts b/static/skywire-manager-src/src/app/components/pages/node/routing/all-transports/all-transports.component.ts index 77f24aa195..eb25b043ac 100644 --- a/static/skywire-manager-src/src/app/components/pages/node/routing/all-transports/all-transports.component.ts +++ b/static/skywire-manager-src/src/app/components/pages/node/routing/all-transports/all-transports.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { Subscription } from 'rxjs'; -import { Node, Transport } from '../../../../../app.datatypes'; +import { Node } from '../../../../../app.datatypes'; import { NodeComponent } from '../../node.component'; /** @@ -13,17 +13,13 @@ import { NodeComponent } from '../../node.component'; styleUrls: ['./all-transports.component.scss'] }) export class AllTransportsComponent implements OnInit, OnDestroy { - transports: Transport[]; - nodePK: string; + node: Node; private dataSubscription: Subscription; ngOnInit() { // Get the node data from the parent page. - this.dataSubscription = NodeComponent.currentNode.subscribe((node: Node) => { - this.nodePK = node.localPk; - this.transports = node.transports; - }); + this.dataSubscription = NodeComponent.currentNode.subscribe((node: Node) => this.node = node); } ngOnDestroy() { diff --git a/static/skywire-manager-src/src/app/components/pages/node/routing/routing.component.html b/static/skywire-manager-src/src/app/components/pages/node/routing/routing.component.html index 8b6b5eb125..54b16c449d 100644 --- a/static/skywire-manager-src/src/app/components/pages/node/routing/routing.component.html +++ b/static/skywire-manager-src/src/app/components/pages/node/routing/routing.component.html @@ -1,7 +1,6 @@ + [node]="node" + [showShortList]="true"> { this.nodePK = node.localPk; - this.transports = node.transports; + this.node = node; this.routes = node.routes; }); } diff --git a/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/create-transport/create-transport.component.html b/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/create-transport/create-transport.component.html index cf9b9a931e..fc01da9c8e 100644 --- a/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/create-transport/create-transport.component.html +++ b/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/create-transport/create-transport.component.html @@ -39,6 +39,15 @@ {{ 'transports.dialog.errors.transport-type-error' | translate }} + + + {{ 'transports.dialog.make-persistent' | translate }} + help + , private snackbarService: SnackbarService, private storageService: StorageService, + private nodeService: NodeService, ) { } ngOnInit() { @@ -74,6 +79,13 @@ export class CreateTransportComponent implements OnInit, OnDestroy { } } + /** + * Used by the checkbox for the persistent setting. + */ + setMakePersistent(event) { + this.makePersistent = event.checked ? true : false; + } + /** * Creates the transport. */ @@ -84,37 +96,103 @@ export class CreateTransportComponent implements OnInit, OnDestroy { this.button.showLoading(); - this.operationSubscription = this.transportService.create( - // The node pk is obtained from the currently openned node page. + const newTransportPk: string = this.form.get('remoteKey').value; + const newTransportType: string = this.form.get('type').value; + const newTransportLabel: string = this.form.get('label').value; + + if (this.makePersistent) { + // Check the current visor config. + this.operationSubscription = this.nodeService.getNode(NodeComponent.getCurrentNodeKey()).subscribe(nodeData => { + let noNeedToAddToPersistents = false; + + // Check if the transport is already in the persistent list. + nodeData.persistentTransports.forEach(t => { + if (t.pk.toUpperCase() === newTransportPk.toUpperCase() && t.type.toUpperCase() === newTransportType.toUpperCase()) { + noNeedToAddToPersistents = true; + } + }); + + if (noNeedToAddToPersistents) { + this.createTransport(newTransportPk, newTransportType, newTransportLabel, true); + } else { + this.createPersistent(nodeData.persistentTransports, newTransportPk, newTransportType, newTransportLabel); + } + }, err => { + this.onError(err); + }); + } else { + this.createTransport(newTransportPk, newTransportType, newTransportLabel, false); + } + } + + /** + * Updates the persistent transports list. + * @param currentList Current persistent transports list. + * @param newTransportPk Public key of the new transport. + * @param newTransportType Type of the new transport. + */ + private createPersistent( + currentList: PersistentTransport[], + newTransportPk: string, + newTransportType: string, + newTransportLabel: string + ) { + // Add the new transport. + currentList.push({ + pk: newTransportPk, + type: newTransportType, + }); + + this.operationSubscription = this.transportService.savePersistentTransportsData( NodeComponent.getCurrentNodeKey(), - this.form.get('remoteKey').value, - this.form.get('type').value, - ).subscribe({ - next: this.onSuccess.bind(this), - error: this.onError.bind(this) + currentList + ).subscribe(() => { + this.createTransport(newTransportPk, newTransportType, newTransportLabel, true); + }, err => { + this.onError(err); }); } - private onSuccess(response: any) { - // Save the label. - const label = this.form.get('label').value; - let errorSavingLabel = false; - if (label) { - if (response && response.id) { - this.storageService.saveLabel(response.id, label, LabeledElementTypes.Transport); - } else { - errorSavingLabel = true; + /** + * Creates a transport with the data entered in the form. + * @param newTransportPk Public key of the new transport. + * @param newTransportType Type of the new transport. + */ + private createTransport(newTransportPk: string, newTransportType: string, newTransportLabel: string, creatingAfterPersistent: boolean) { + this.operationSubscription = this.transportService.create( + // The node pk is obtained from the currently openned node page. + NodeComponent.getCurrentNodeKey(), + newTransportPk, + newTransportType, + ).subscribe(response => { + // Save the label. + let errorSavingLabel = false; + if (newTransportLabel) { + if (response && response.id) { + this.storageService.saveLabel(response.id, newTransportLabel, LabeledElementTypes.Transport); + } else { + errorSavingLabel = true; + } } - } - NodeComponent.refreshCurrentDisplayedData(); - this.dialogRef.close(); + NodeComponent.refreshCurrentDisplayedData(); + this.dialogRef.close(); - if (!errorSavingLabel) { - this.snackbarService.showDone('transports.dialog.success'); - } else { - this.snackbarService.showWarning('transports.dialog.success-without-label'); - } + if (!errorSavingLabel) { + this.snackbarService.showDone('transports.dialog.success'); + } else { + this.snackbarService.showWarning('transports.dialog.success-without-label'); + } + }, err => { + if (!creatingAfterPersistent) { + this.onError(err); + } else { + NodeComponent.refreshCurrentDisplayedData(); + this.dialogRef.close(); + + this.snackbarService.showWarning('transports.dialog.only-persistent-created'); + } + }); } private onError(err: OperationError) { diff --git a/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/transport-details/transport-details.component.html b/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/transport-details/transport-details.component.html index 0896336b00..900d27e54b 100644 --- a/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/transport-details/transport-details.component.html +++ b/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/transport-details/transport-details.component.html @@ -4,10 +4,12 @@ list{{ 'transports.details.basic.title' | translate }}
- {{ 'transports.details.basic.state' | translate }} -
- {{ ('transports.statuses.' + (data.isUp ? 'online' : 'offline')) | translate }} -
+ {{ 'transports.details.basic.persistent' | translate }} + + {{ 'common.yes' | translate }} + help + + {{ 'common.no' | translate }}
{{ 'transports.details.basic.id' | translate }} {{ data.id }} diff --git a/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/transport-details/transport-details.component.scss b/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/transport-details/transport-details.component.scss index e69de29bb2..a54adeac41 100644 --- a/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/transport-details/transport-details.component.scss +++ b/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/transport-details/transport-details.component.scss @@ -0,0 +1,5 @@ +.help-icon { + opacity: 0.5; + font-size: 14px; + cursor: default; +} diff --git a/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/transport-list.component.html b/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/transport-list.component.html index 6b88138f81..8a48b2f032 100644 --- a/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/transport-list.component.html +++ b/static/skywire-manager-src/src/app/components/pages/node/routing/transport-list/transport-list.component.html @@ -32,15 +32,18 @@ *ngIf="dataSource && dataSource.length > 0" >more_horiz -
- {{ 'transports.remove-all-offline' | translate }} -
{{ 'selection.select-all' | translate }}
{{ 'selection.unselect-all' | translate }}
+
+ {{ 'transports.make-selected-persistent' | translate }} +
+
+ {{ 'transports.make-selected-non-persistent' | translate }} +
{{ 'selection.delete-all' | translate }}
@@ -70,9 +73,9 @@ - - - {{ dataSorter.sortingArrow }} + + star_outline + {{ dataSorter.sortingArrow }} {{ 'transports.id' | translate }} @@ -103,7 +106,7 @@ - + - + + - + + {{ 'transports.offline' | translate }} + + + {{ 'transports.offline' | translate }} {{ transport.type }} - + {{ transport.sent | autoScale }} - + {{ transport.recv | autoScale }} + + {{ 'transports.offline' | translate }} + + + {{ 'transports.offline' | translate }} +