[client/ui-wails] Add Forwarding service for the exposed-services list

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.
This commit is contained in:
Zoltán Papp
2026-05-05 13:53:40 +02:00
parent 93275f9052
commit fffb9dd219
6 changed files with 259 additions and 23 deletions

View File

@@ -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);

View File

@@ -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,

View File

@@ -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<ForwardingRule> = {}) {
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<ForwardingRule>);
}
}
/**
* 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<PortInfo> = {}) {
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<PortInfo>);
}
}
/**
* 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<PortRange> = {}) {
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<PortRange>);
}
}
/**
* 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);

View File

@@ -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.
*/

View File

@@ -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))

View File

@@ -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{}
}