mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 08:16:39 +00:00
386 lines
13 KiB
JavaScript
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 };
|