Files
netbird/client/netbird-electron/electron/grpc-client.cjs
2025-11-01 12:17:02 +01:00

386 lines
13 KiB
JavaScript

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');
const os = require('os');
const { app } = require('electron');
class DaemonClient {
constructor(address) {
this.address = address;
// Path to proto file - use resourcesPath for packaged app, or relative path for dev
const isPackaged = app && app.isPackaged;
this.protoPath = isPackaged
? path.join(process.resourcesPath, 'proto/daemon.proto')
: path.join(__dirname, '../../proto/daemon.proto');
this.client = null;
this.initializeClient();
}
initializeClient() {
try {
const packageDefinition = protoLoader.loadSync(this.protoPath, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
const DaemonService = protoDescriptor.daemon.DaemonService;
// Create client with Unix socket or TCP
const credentials = grpc.credentials.createInsecure();
this.client = new DaemonService(this.address, credentials);
console.log(`gRPC client initialized with address: ${this.address}`);
} catch (error) {
console.error('Failed to initialize gRPC client:', error);
}
}
promisifyCall(method, request = {}) {
return new Promise((resolve, reject) => {
if (!this.client) {
reject(new Error('gRPC client not initialized'));
return;
}
try {
this.client[method](request, (error, response) => {
if (error) {
const enhancedError = {
...error,
method,
message: error.message || 'Unknown gRPC error',
code: error.code,
};
reject(enhancedError);
} else {
resolve(response);
}
});
} catch (error) {
console.error(`gRPC call ${method} failed synchronously:`, error);
reject({
method,
message: error.message,
code: error.code || 'UNKNOWN',
originalError: error,
});
}
});
}
async getStatus() {
try {
const response = await this.promisifyCall('Status', {});
return {
status: response.status || 'Unknown',
version: response.daemonVersion || '0.0.0'
};
} catch (error) {
console.error('getStatus error:', error);
return {
status: 'Error',
version: '0.0.0'
};
}
}
async login() {
try {
const response = await this.promisifyCall('Login', {});
return {
needsSSOLogin: response.needsSSOLogin || false,
userCode: response.userCode || '',
verificationURI: response.verificationURI || '',
verificationURIComplete: response.verificationURIComplete || ''
};
} catch (error) {
console.error('login error:', error);
throw error;
}
}
async waitSSOLogin(userCode) {
try {
const hostname = os.hostname();
const response = await this.promisifyCall('WaitSSOLogin', {
userCode,
hostname
});
return {
email: response.email || ''
};
} catch (error) {
console.error('waitSSOLogin error:', error);
throw error;
}
}
async up() {
await this.promisifyCall('Up', {});
}
async down() {
await this.promisifyCall('Down', {});
}
async getConfig() {
try {
const username = os.userInfo().username;
// Get active profile name
const profiles = await this.listProfiles();
const activeProfile = profiles.find(p => p.active);
const profileName = activeProfile?.name || 'default';
const response = await this.promisifyCall('GetConfig', { username, profileName });
return {
managementUrl: response.managementUrl || '',
preSharedKey: response.preSharedKey || '',
interfaceName: response.interfaceName || '',
wireguardPort: response.wireguardPort || 51820,
mtu: response.mtu || 1280,
serverSSHAllowed: response.serverSSHAllowed || false,
autoConnect: !response.disableAutoConnect, // Invert the daemon's disableAutoConnect
rosenpassEnabled: response.rosenpassEnabled || false,
rosenpassPermissive: response.rosenpassPermissive || false,
lazyConnectionEnabled: response.lazyConnectionEnabled || false,
blockInbound: response.blockInbound || false,
networkMonitor: response.networkMonitor || false,
disableDns: response.disable_dns || false,
disableClientRoutes: response.disable_client_routes || false,
disableServerRoutes: response.disable_server_routes || false,
blockLanAccess: response.block_lan_access || false,
};
} catch (error) {
console.error('getConfig error:', error);
// Return default config on error
return {
managementUrl: '',
preSharedKey: '',
interfaceName: 'wt0',
wireguardPort: 51820,
mtu: 1280,
serverSSHAllowed: false,
autoConnect: false,
rosenpassEnabled: false,
rosenpassPermissive: false,
lazyConnectionEnabled: false,
blockInbound: false,
networkMonitor: true,
disableDns: false,
disableClientRoutes: false,
disableServerRoutes: false,
blockLanAccess: false,
};
}
}
async updateConfig(config) {
try {
const username = os.userInfo().username;
// Get active profile name
const profiles = await this.listProfiles();
const activeProfile = profiles.find(p => p.active);
const profileName = activeProfile?.name || 'default';
// Build the SetConfigRequest with proper field names matching proto
const request = {
username,
profileName,
};
// Map config fields to proto field names (snake_case for gRPC)
if (config.managementUrl !== undefined) request.managementUrl = config.managementUrl;
if (config.interfaceName !== undefined) request.interfaceName = config.interfaceName;
if (config.wireguardPort !== undefined) request.wireguardPort = config.wireguardPort;
if (config.preSharedKey !== undefined) request.optionalPreSharedKey = config.preSharedKey;
if (config.mtu !== undefined) request.mtu = config.mtu;
if (config.serverSSHAllowed !== undefined) request.serverSSHAllowed = config.serverSSHAllowed;
if (config.autoConnect !== undefined) request.disableAutoConnect = !config.autoConnect; // Invert for daemon
if (config.rosenpassEnabled !== undefined) request.rosenpassEnabled = config.rosenpassEnabled;
if (config.rosenpassPermissive !== undefined) request.rosenpassPermissive = config.rosenpassPermissive;
if (config.lazyConnectionEnabled !== undefined) request.lazyConnectionEnabled = config.lazyConnectionEnabled;
if (config.blockInbound !== undefined) request.block_inbound = config.blockInbound;
if (config.networkMonitor !== undefined) request.networkMonitor = config.networkMonitor;
if (config.disableDns !== undefined) request.disable_dns = config.disableDns;
if (config.disableClientRoutes !== undefined) request.disable_client_routes = config.disableClientRoutes;
if (config.disableServerRoutes !== undefined) request.disable_server_routes = config.disableServerRoutes;
if (config.blockLanAccess !== undefined) request.block_lan_access = config.blockLanAccess;
await this.promisifyCall('SetConfig', request);
} catch (error) {
console.error('updateConfig error:', error);
throw error;
}
}
async listProfiles() {
try {
const username = os.userInfo().username;
const response = await this.promisifyCall('ListProfiles', { username });
console.log('Raw gRPC response profiles:', JSON.stringify(response.profiles, null, 2));
const mapped = (response.profiles || []).map((profile) => ({
id: profile.id || profile.name, // Use name as id if id is not provided
name: profile.name,
email: profile.email,
active: profile.is_active || false, // gRPC uses snake_case: is_active
}));
console.log('Mapped profiles:', JSON.stringify(mapped, null, 2));
return mapped;
} catch (error) {
console.error('listProfiles error:', error);
// Return empty array on error instead of throwing
if (error.code === 'EPIPE' || error.code === 'ECONNREFUSED') {
console.warn('gRPC connection lost, returning empty profiles list');
}
return [];
}
}
async switchProfile(profileName) {
try {
console.log('gRPC client: switchProfile called with profileName:', profileName);
const username = os.userInfo().username;
const result = await this.promisifyCall('SwitchProfile', { profileName, username });
console.log('gRPC client: switchProfile result:', result);
return result;
} catch (error) {
console.error('switchProfile error:', error);
throw error;
}
}
async addProfile(profileName) {
try {
const username = os.userInfo().username;
await this.promisifyCall('AddProfile', { username, profileName });
} catch (error) {
console.error('addProfile error:', error);
throw error;
}
}
async removeProfile(profileName) {
try {
const username = os.userInfo().username;
await this.promisifyCall('RemoveProfile', { username, profileName });
} catch (error) {
console.error('removeProfile error:', error);
throw error;
}
}
async logout() {
try {
await this.promisifyCall('Logout', {});
} catch (error) {
console.error('logout error:', error);
throw error;
}
}
async createDebugBundle(anonymize = true) {
try {
const response = await this.promisifyCall('DebugBundle', {
anonymize,
systemInfo: true,
status: '',
logFileCount: 5
});
return response.path || '';
} catch (error) {
console.error('createDebugBundle error:', error);
throw error;
}
}
async getPeers() {
try {
console.log('[getPeers] Calling Status RPC with getFullPeerStatus: true');
const response = await this.promisifyCall('Status', {
getFullPeerStatus: true,
shouldRunProbes: false,
});
console.log('[getPeers] Status response:', JSON.stringify({
status: response.status,
hasFullStatus: !!response.fullStatus,
peersCount: response.fullStatus?.peers?.length || 0
}));
// Extract peers from fullStatus
const peers = response.fullStatus?.peers || [];
console.log(`[getPeers] Found ${peers.length} peers`);
// Map the peers to the format expected by the UI
const mapped = peers.map(peer => ({
ip: peer.IP || '',
pubKey: peer.pubKey || '',
connStatus: peer.connStatus || 'Disconnected',
connStatusUpdate: peer.connStatusUpdate ? new Date(peer.connStatusUpdate.seconds * 1000).toISOString() : '',
relayed: peer.relayed || false,
localIceCandidateType: peer.localIceCandidateType || '',
remoteIceCandidateType: peer.remoteIceCandidateType || '',
fqdn: peer.fqdn || '',
localIceCandidateEndpoint: peer.localIceCandidateEndpoint || '',
remoteIceCandidateEndpoint: peer.remoteIceCandidateEndpoint || '',
lastWireguardHandshake: peer.lastWireguardHandshake ? new Date(peer.lastWireguardHandshake.seconds * 1000).toISOString() : '',
bytesRx: peer.bytesRx || 0,
bytesTx: peer.bytesTx || 0,
rosenpassEnabled: peer.rosenpassEnabled || false,
networks: peer.networks || [],
latency: peer.latency ? (peer.latency.seconds * 1000 + peer.latency.nanos / 1000000) : 0,
relayAddress: peer.relayAddress || '',
}));
console.log('[getPeers] Returning mapped peers:', JSON.stringify(mapped.map(p => ({ ip: p.ip, fqdn: p.fqdn, connStatus: p.connStatus }))));
return mapped;
} catch (error) {
console.error('getPeers error:', error);
return [];
}
}
async getLocalPeer() {
try {
const response = await this.promisifyCall('Status', {
getFullPeerStatus: true,
shouldRunProbes: false,
});
const localPeer = response.fullStatus?.localPeerState;
if (!localPeer) {
console.log('[getLocalPeer] No local peer state found');
return null;
}
const mapped = {
ip: localPeer.IP || '',
pubKey: localPeer.pubKey || '',
fqdn: localPeer.fqdn || '',
kernelInterface: localPeer.kernelInterface || false,
rosenpassEnabled: localPeer.rosenpassEnabled || false,
rosenpassPermissive: localPeer.rosenpassPermissive || false,
networks: localPeer.networks || [],
};
console.log('[getLocalPeer] Local peer:', JSON.stringify({ ip: mapped.ip, fqdn: mapped.fqdn }));
return mapped;
} catch (error) {
console.error('getLocalPeer error:', error);
return null;
}
}
}
module.exports = { DaemonClient };