From fffb9dd219aeab2dcea8cf2797de9556191a2046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Tue, 5 May 2026 13:53:40 +0200 Subject: [PATCH] [client/ui-wails] Add Forwarding service for the exposed-services list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surfaces the daemon's existing ForwardingRules RPC as a Wails service so the React frontend can render the reverse-proxy / exposed-services list in the planned dashboard. Forwarding.List() returns one ForwardingRule per active rule with protocol, destination port (single or range), translated address / hostname, and translated port. The PortInfo oneof from the proto is flattened to a `{port?: number, range?: {start, end}}` shape so TS consumers don't have to peek at proto-internal type discriminators. Regenerate frontend/bindings (forwarding.ts, models.ts, index.ts) so the React side picks up the new service. peers.ts churn is a doc comment refresh only — no API change. --- .../client/ui-wails/services/forwarding.ts | 29 ++++ .../netbird/client/ui-wails/services/index.ts | 5 + .../client/ui-wails/services/models.ts | 147 +++++++++++++++--- .../netbird/client/ui-wails/services/peers.ts | 13 +- client/ui-wails/main.go | 1 + client/ui-wails/services/forwarding.go | 87 +++++++++++ 6 files changed, 259 insertions(+), 23 deletions(-) create mode 100644 client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/forwarding.ts create mode 100644 client/ui-wails/services/forwarding.go diff --git a/client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/forwarding.ts b/client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/forwarding.ts new file mode 100644 index 000000000..b1c18c3a2 --- /dev/null +++ b/client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/forwarding.ts @@ -0,0 +1,29 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * Forwarding groups the daemon RPCs that surface exposed/forwarded services. + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as $models from "./models.js"; + +/** + * List returns the current set of forwarding rules from the daemon's + * reverse proxy. The frontend renders these as the "exposed services" list. + */ +export function List(): $CancellablePromise<$models.ForwardingRule[]> { + return $Call.ByID(3893357601).then(($result: any) => { + return $$createType1($result); + }); +} + +// Private type creation functions +const $$createType0 = $models.ForwardingRule.createFrom; +const $$createType1 = $Create.Array($$createType0); diff --git a/client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/index.ts b/client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/index.ts index 90203c174..bb3a7b821 100644 --- a/client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/index.ts +++ b/client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/index.ts @@ -3,6 +3,7 @@ import * as Connection from "./connection.js"; import * as Debug from "./debug.js"; +import * as Forwarding from "./forwarding.js"; import * as Networks from "./networks.js"; import * as Peers from "./peers.js"; import * as Profiles from "./profiles.js"; @@ -11,6 +12,7 @@ import * as Update from "./update.js"; export { Connection, Debug, + Forwarding, Networks, Peers, Profiles, @@ -25,6 +27,7 @@ export { DebugBundleParams, DebugBundleResult, Features, + ForwardingRule, LocalPeer, LogLevel, LoginParams, @@ -33,6 +36,8 @@ export { Network, PeerLink, PeerStatus, + PortInfo, + PortRange, Profile, ProfileRef, SelectNetworksParams, diff --git a/client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.ts b/client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.ts index d22a345e0..ee6f26b96 100644 --- a/client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.ts +++ b/client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.ts @@ -291,6 +291,55 @@ export class Features { } } +/** + * ForwardingRule is one entry from the daemon's reverse-proxy table — + * what we ship to the frontend's "exposed services" view. + */ +export class ForwardingRule { + "protocol": string; + "destinationPort": PortInfo; + "translatedAddress": string; + "translatedHostname": string; + "translatedPort": PortInfo; + + /** Creates a new ForwardingRule instance. */ + constructor($$source: Partial = {}) { + if (!("protocol" in $$source)) { + this["protocol"] = ""; + } + if (!("destinationPort" in $$source)) { + this["destinationPort"] = (new PortInfo()); + } + if (!("translatedAddress" in $$source)) { + this["translatedAddress"] = ""; + } + if (!("translatedHostname" in $$source)) { + this["translatedHostname"] = ""; + } + if (!("translatedPort" in $$source)) { + this["translatedPort"] = (new PortInfo()); + } + + Object.assign(this, $$source); + } + + /** + * Creates a new ForwardingRule instance from a string or object. + */ + static createFrom($$source: any = {}): ForwardingRule { + const $$createField1_0 = $$createType0; + const $$createField4_0 = $$createType0; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("destinationPort" in $$parsedSource) { + $$parsedSource["destinationPort"] = $$createField1_0($$parsedSource["destinationPort"]); + } + if ("translatedPort" in $$parsedSource) { + $$parsedSource["translatedPort"] = $$createField4_0($$parsedSource["translatedPort"]); + } + return new ForwardingRule($$parsedSource as Partial); + } +} + /** * LocalPeer mirrors LocalPeerState — what this client looks like on the mesh. */ @@ -322,7 +371,7 @@ export class LocalPeer { * Creates a new LocalPeer instance from a string or object. */ static createFrom($$source: any = {}): LocalPeer { - const $$createField3_0 = $$createType0; + const $$createField3_0 = $$createType1; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("networks" in $$parsedSource) { $$parsedSource["networks"] = $$createField3_0($$parsedSource["networks"]); @@ -503,8 +552,8 @@ export class Network { * Creates a new Network instance from a string or object. */ static createFrom($$source: any = {}): Network { - const $$createField3_0 = $$createType0; - const $$createField4_0 = $$createType1; + const $$createField3_0 = $$createType1; + const $$createField4_0 = $$createType2; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("domains" in $$parsedSource) { $$parsedSource["domains"] = $$createField3_0($$parsedSource["domains"]); @@ -631,7 +680,7 @@ export class PeerStatus { * Creates a new PeerStatus instance from a string or object. */ static createFrom($$source: any = {}): PeerStatus { - const $$createField16_0 = $$createType0; + const $$createField16_0 = $$createType1; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("networks" in $$parsedSource) { $$parsedSource["networks"] = $$createField16_0($$parsedSource["networks"]); @@ -640,6 +689,61 @@ export class PeerStatus { } } +/** + * PortInfo carries the destination or translated port for a forwarding rule. + * Exactly one of Port or Range is populated, mirroring the daemon's oneof. + */ +export class PortInfo { + "port"?: number | null; + "range"?: PortRange | null; + + /** Creates a new PortInfo instance. */ + constructor($$source: Partial = {}) { + + Object.assign(this, $$source); + } + + /** + * Creates a new PortInfo instance from a string or object. + */ + static createFrom($$source: any = {}): PortInfo { + const $$createField1_0 = $$createType4; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("range" in $$parsedSource) { + $$parsedSource["range"] = $$createField1_0($$parsedSource["range"]); + } + return new PortInfo($$parsedSource as Partial); + } +} + +/** + * PortRange describes a contiguous port range. Both ends are inclusive. + */ +export class PortRange { + "start": number; + "end": number; + + /** Creates a new PortRange instance. */ + constructor($$source: Partial = {}) { + if (!("start" in $$source)) { + this["start"] = 0; + } + if (!("end" in $$source)) { + this["end"] = 0; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new PortRange instance from a string or object. + */ + static createFrom($$source: any = {}): PortRange { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new PortRange($$parsedSource as Partial); + } +} + /** * Profile is one named daemon profile. */ @@ -725,7 +829,7 @@ export class SelectNetworksParams { * Creates a new SelectNetworksParams instance from a string or object. */ static createFrom($$source: any = {}): SelectNetworksParams { - const $$createField0_0 = $$createType0; + const $$createField0_0 = $$createType1; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("networkIds" in $$parsedSource) { $$parsedSource["networkIds"] = $$createField0_0($$parsedSource["networkIds"]); @@ -837,11 +941,11 @@ export class Status { * Creates a new Status instance from a string or object. */ static createFrom($$source: any = {}): Status { - const $$createField2_0 = $$createType2; - const $$createField3_0 = $$createType2; - const $$createField4_0 = $$createType3; - const $$createField5_0 = $$createType5; - const $$createField6_0 = $$createType7; + const $$createField2_0 = $$createType5; + const $$createField3_0 = $$createType5; + const $$createField4_0 = $$createType6; + const $$createField5_0 = $$createType8; + const $$createField6_0 = $$createType10; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("management" in $$parsedSource) { $$parsedSource["management"] = $$createField2_0($$parsedSource["management"]); @@ -905,7 +1009,7 @@ export class SystemEvent { * Creates a new SystemEvent instance from a string or object. */ static createFrom($$source: any = {}): SystemEvent { - const $$createField6_0 = $$createType8; + const $$createField6_0 = $$createType11; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("metadata" in $$parsedSource) { $$parsedSource["metadata"] = $$createField6_0($$parsedSource["metadata"]); @@ -1056,12 +1160,15 @@ export class WaitSSOParams { } // Private type creation functions -const $$createType0 = $Create.Array($Create.Any); -const $$createType1 = $Create.Map($Create.Any, $$createType0); -const $$createType2 = PeerLink.createFrom; -const $$createType3 = LocalPeer.createFrom; -const $$createType4 = PeerStatus.createFrom; -const $$createType5 = $Create.Array($$createType4); -const $$createType6 = SystemEvent.createFrom; -const $$createType7 = $Create.Array($$createType6); -const $$createType8 = $Create.Map($Create.Any, $Create.Any); +const $$createType0 = PortInfo.createFrom; +const $$createType1 = $Create.Array($Create.Any); +const $$createType2 = $Create.Map($Create.Any, $$createType1); +const $$createType3 = PortRange.createFrom; +const $$createType4 = $Create.Nullable($$createType3); +const $$createType5 = PeerLink.createFrom; +const $$createType6 = LocalPeer.createFrom; +const $$createType7 = PeerStatus.createFrom; +const $$createType8 = $Create.Array($$createType7); +const $$createType9 = SystemEvent.createFrom; +const $$createType10 = $Create.Array($$createType9); +const $$createType11 = $Create.Map($Create.Any, $Create.Any); diff --git a/client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/peers.ts b/client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/peers.ts index 2d361d41b..98032de6e 100644 --- a/client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/peers.ts +++ b/client/ui-wails/frontend/bindings/github.com/netbirdio/netbird/client/ui-wails/services/peers.ts @@ -25,9 +25,16 @@ export function Get(): $CancellablePromise<$models.Status> { } /** - * Watch starts the background loops that feed the frontend: a status - * stream (push-driven on connection-state change) and an event stream - * (DNS / network / auth / connectivity / update notifications). + * Watch starts the background loops that feed the frontend: + * - statusStreamLoop: push-driven snapshots on connection-state change + * (Connected/Disconnected/Connecting, peer list, address). Drives the + * tray icon, Status page, and Peers page. + * - toastStreamLoop: DNS / network / auth / connectivity / update + * SystemEvent stream. Drives OS notifications, the Recent Events + * list, and the update-overlay flag. The daemon-side RPC is named + * SubscribeEvents — only the loop's local alias differs to keep the + * two streams distinguishable in this file. + * * Safe to call once at boot; both loops self-restart on stream errors * via exponential backoff. */ diff --git a/client/ui-wails/main.go b/client/ui-wails/main.go index 99287f3fa..bef421e16 100644 --- a/client/ui-wails/main.go +++ b/client/ui-wails/main.go @@ -99,6 +99,7 @@ func main() { app.RegisterService(application.NewService(connection)) app.RegisterService(application.NewService(settings)) app.RegisterService(application.NewService(services.NewNetworks(conn))) + app.RegisterService(application.NewService(services.NewForwarding(conn))) app.RegisterService(application.NewService(profiles)) app.RegisterService(application.NewService(services.NewDebug(conn))) app.RegisterService(application.NewService(update)) diff --git a/client/ui-wails/services/forwarding.go b/client/ui-wails/services/forwarding.go new file mode 100644 index 000000000..4626bf764 --- /dev/null +++ b/client/ui-wails/services/forwarding.go @@ -0,0 +1,87 @@ +//go:build !android && !ios && !freebsd && !js + +package services + +import ( + "context" + + "github.com/netbirdio/netbird/client/proto" +) + +// PortRange describes a contiguous port range. Both ends are inclusive. +type PortRange struct { + Start uint32 `json:"start"` + End uint32 `json:"end"` +} + +// PortInfo carries the destination or translated port for a forwarding rule. +// Exactly one of Port or Range is populated, mirroring the daemon's oneof. +type PortInfo struct { + Port *uint32 `json:"port,omitempty"` + Range *PortRange `json:"range,omitempty"` +} + +// ForwardingRule is one entry from the daemon's reverse-proxy table — +// what we ship to the frontend's "exposed services" view. +type ForwardingRule struct { + Protocol string `json:"protocol"` + DestinationPort PortInfo `json:"destinationPort"` + TranslatedAddress string `json:"translatedAddress"` + TranslatedHostname string `json:"translatedHostname"` + TranslatedPort PortInfo `json:"translatedPort"` +} + +// Forwarding groups the daemon RPCs that surface exposed/forwarded services. +type Forwarding struct { + conn DaemonConn +} + +func NewForwarding(conn DaemonConn) *Forwarding { + return &Forwarding{conn: conn} +} + +// List returns the current set of forwarding rules from the daemon's +// reverse proxy. The frontend renders these as the "exposed services" list. +func (s *Forwarding) List(ctx context.Context) ([]ForwardingRule, error) { + cli, err := s.conn.Client() + if err != nil { + return nil, err + } + resp, err := cli.ForwardingRules(ctx, &proto.EmptyRequest{}) + if err != nil { + return nil, err + } + out := make([]ForwardingRule, 0, len(resp.GetRules())) + for _, r := range resp.GetRules() { + out = append(out, forwardingRuleFromProto(r)) + } + return out, nil +} + +func forwardingRuleFromProto(r *proto.ForwardingRule) ForwardingRule { + return ForwardingRule{ + Protocol: r.GetProtocol(), + DestinationPort: portInfoFromProto(r.GetDestinationPort()), + TranslatedAddress: r.GetTranslatedAddress(), + TranslatedHostname: r.GetTranslatedHostname(), + TranslatedPort: portInfoFromProto(r.GetTranslatedPort()), + } +} + +func portInfoFromProto(p *proto.PortInfo) PortInfo { + if p == nil { + return PortInfo{} + } + switch sel := p.GetPortSelection().(type) { + case *proto.PortInfo_Port: + port := sel.Port + return PortInfo{Port: &port} + case *proto.PortInfo_Range_: + r := sel.Range + if r == nil { + return PortInfo{} + } + return PortInfo{Range: &PortRange{Start: r.GetStart(), End: r.GetEnd()}} + } + return PortInfo{} +} \ No newline at end of file