grpc/grpc-node

grpc-js not supporting the namedpipe communication

vamsideepak opened this issue · 15 comments

I have created the below sample application to test the named pipe support in grpc-js but unfortunately it is not working

Reproduction steps

Please find the below details to reproduce the issue
##Client.js

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');

const PROTO_PATH = path.resolve(__dirname, 'namedpipe.proto');

const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const namedService = grpc.loadPackageDefinition(packageDefinition).NamedPipe.NamedPipeService;

const pipePath = '\\\\.\\pipe\\mytestpipe';

const client = new namedService('localhost', grpc.credentials.createInsecure(), {
  'grpc.default_authority': 'localhost',
  'grpc.channelOverride': {
    'grpc.testing.fixed_reconnect_backoff_ms': 2000,
    'grpc.testing.fixed_address_list': [{
      'address': pipePath,
    }],
  },
});

client.YourRpcMethod({ message: 'Request from client' }, (error, response) => {
  if (!error) {
    console.log('Response:', response.message);
  } else {
    console.error('Error:', error);
  }
});

##Server.js

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');
const net = require('net');

const PROTO_PATH = path.resolve(__dirname, 'namedpipe.proto');

const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const NamedService = grpc.loadPackageDefinition(packageDefinition).NamedPipe.NamedPipeService;

const server = new grpc.Server();

server.addService(NamedService.service, {
  YourRpcMethod: (call, callback) => {
    callback(null, { message: 'Response from server' });
  }
});

const pipePath = '\\\\.\\pipe\\mytestpipe';

const pipeServer = net.createServer((stream) => {
  server.emit('connection', stream);
});

pipeServer.listen(pipePath, () => {
  console.log(`Server running on named pipe: ${pipePath}`);
  server.bindAsync(pipePath, grpc.ServerCredentials.createInsecure(), (err, port) => {
    if (err) {
      console.error('Error binding server:', err);
    } else {
      console.log('Server bound successfully');
      server.start();
    }
  });
});

// Handle server shutdown gracefully
process.on('SIGINT', () => {
  console.log('Server shutting down...');
  server.tryShutdown(() => {
    console.log('Server shut down gracefully.');
    pipeServer.close();
  });
});

Proto File:

syntax = "proto3";

package NamedPipe;

service NamedPipeService {
  rpc YourRpcMethod(RequestMessage) returns (ResponseMessage);
}

message RequestMessage {
  string message = 1;
}

message ResponseMessage {
  string message = 1;
}

##Error:

Server running on named pipe: \\.\pipe\mytestpipe
Error binding server: Error: Name resolution failed for target dns:\\.\pipe\mytestpipe
    at Object.onError (C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\server.js:490:34)
    at C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\resolver-dns.js:207:31

Is grpc-js supports the named pipes ? If yes please provide any inputs to resolve this.

Environment

  • OS name, version and architecture: windows 11
  • Node version - 16.14.2
  • Node installation method - exe installation
  • Package name and version 1.4.1

First, version 1.4.1 is very old. The latest release is 1.9.14, and I recommend upgrading.

Second, as shown in issue #2642, you can connect to named pipes using the uds UDS resolver, not the default dns resolver. In your case, the target string uds:////./pipe/mytestpipe unix:////./pipe/mytestpipe should work. That would be the argument to both the client constructor

Third, the arguments you are passing to the client constructor just don't correspond to the actual API of this library. The grpc.channelOverride option is for a channel object created with the grpc.Channel constructor (or obtained from client.getChannel(). The options grpc.testing.fixed_reconnect_backoff_ms and grpc.testing.fixed_address_list are not supported in this library. Also, the option grpc.default_authority does work, but the value localhost is the default for uds addresses anyway.

In short, on the client and the server, you should have const pipePath = 'uds:////./pipe/mytestpipe' const pipePath = 'unix:////./pipe/mytestpipe', and on the client, the client constructor line should just say const client = new namedService(pipePath, grpc.credentials.createInsecure())

@murgatroid99

Changed my code like below

client.js

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');

const PROTO_PATH = path.resolve(__dirname, 'namedpipe.proto');

const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const namedService = grpc.loadPackageDefinition(packageDefinition).NamedPipe.NamedPipeService;

const pipePath = "uds:\\\\.\\pipe\\TEST"

const client = new namedService(pipePath, grpc.credentials.createInsecure());

client.YourRpcMethod({ message: 'Request from client' }, (error, response) => {
  if (!error) {
    console.log('Response:', response.message);
  } else {
    console.error('Error:', error);
  }
});

Server.js

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');
const net = require('net');

const PROTO_PATH = path.resolve(__dirname, 'namedpipe.proto');

const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const NamedService = grpc.loadPackageDefinition(packageDefinition).NamedPipe.NamedPipeService;

const server = new grpc.Server();

server.addService(NamedService.service, {
  YourRpcMethod: (call, callback) => {
    callback(null, { message: 'Response from server' });
  }
});

const pipePath = "uds:\\\\.\\pipe\\TEST"

const pipeServer = net.createServer((stream) => {
  server.emit('connection', stream);
});

pipeServer.listen(pipePath, () => {
  console.log(`Server running on named pipe: ${pipePath}`);
  server.bindAsync(pipePath, grpc.ServerCredentials.createInsecure(), (err, port) => {
    if (err) {
      console.error('Error binding server:', err);
    } else {
      console.log('Server bound successfully');
      server.start();
    }
  });
});

// Handle server shutdown gracefully
process.on('SIGINT', () => {
  console.log('Server shutting down...');
  server.tryShutdown(() => {
    console.log('Server shut down gracefully.');
    pipeServer.close();
  });
});

Now getting permission error :

node:events:504
      throw er; // Unhandled 'error' event
      ^

Error: listen EACCES: permission denied uds:\\.\pipe\TEST
    at Server.setupListenHandle [as _listen2] (node:net:1313:21)
    at listenInCluster (node:net:1378:12)
    at Server.listen (node:net:1476:5)
    at Object.<anonymous> (C:\Users\xxx\Desktop\NamedPipes\server.js:25:12)
    at Module._compile (node:internal/modules/cjs/loader:1103:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1157:10)
    at Module.load (node:internal/modules/cjs/loader:981:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
    at node:internal/main/run_main_module:17:47
Emitted 'error' event on Server instance at:
    at emitErrorNT (node:net:1357:8)
    at processTicksAndRejections (node:internal/process/task_queues:83:21) {
  code: 'EACCES',
  errno: -4092,
  syscall: 'listen',
  address: 'uds:\\\\.\\pipe\\TEST',
  port: -1
}

Other things I have tried
I have tried as below based on nodejs document.

net.createServer().listen(
  path.join('\\\\?\\pipe', process.cwd(), 'myctl'));

https://nodejs.org/api/net.html#identifying-paths-for-ipc-connections

Error:

Server running on named pipe:  \\?\pipe\C:\Users\xxx\Desktop\NamedPipes\myctl
Error binding server: Error: Failed to parse DNS address dns:uds:\\.\pipe\TEST
    at Object.onError (C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\server.js:490:34)
    at Immediate.<anonymous> (C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\resolver-dns.js:143:31)
    at processImmediate (node:internal/timers:466:21)
Server shutting down...
Server shut down gracefully.

The changes you made do not match the changes I suggested, so it is unsurprising that they didn't work. My suggestion was to use the target string uds:////./pipe/mytestpipe unix:////./pipe/mytestpipe. Note that this uses forward slashes instead of backslashes. It won't work with backslashes.

@murgatroid99
I have tried both forward and backward slashes as you mentioned but still the same error I am getting. don't know the exact reason. It is simple POC and does not have complex functionality . You can pull the code from here and test it once.
https://github.com/vamsideepak/Node-Grpc-NamedPipes

steps to reproduce
step1 : npm install
step2 : node server.js
step3 : node client.js

I can't show you the exact code cause I'm on a trip and apparently Gitlab is blocked here, but you can refer to NodeJS net package. https://nodejs.org/api/net.html#identifying-paths-for-ipc-connections

That's what grpc is using under the hood, and named pipes are support (as well a Unix sockets on Unix), and the format would be \\.\pipe\pipename. I don't pass the uds: stuff.

I can guarantee you that named pipes communication works, I've achieved it between golang and node, and between python and node.

@vamsideepak OK, the relevant code in gRPC is made for handling UDS paths, not named pipes, so it may parse a little weirdly. I don't expect it to work with backslashes, so show your POC code with forward slashes.

@pelletier197 I wrote the grpc-js code, I know how it works. grpc-js performs its own target name parsing and transformation before passing it along to the net library by way of the http2 library. It will not create a UDS or named pipe connection unless the target string starts with uds: unix:. The target string is parsed as described in this naming document; you can see the uds: unix: logic there.

@murgatroid99 Tried as you mentioned. Still the same error. I tried below list but still no luck.
1.\.\pipe\pipename
2.uds:\.\pipe\pipename
3.uds:////./pipe/pipename
4.uds////.//pipe//pipename
5.////./pipe/pipename
6.////.//pipe//pipename
If you want reproduce the issue . you can check POC code below
https://github.com/vamsideepak/Node-Grpc-NamedPipes

image

I just realized that in your code, you are creating a net.Server and calling pipeServer.listen before calling server.bindAsync, both with the same target string. This can never work, at least with named pipes, because the address string formats are incompatible. Just remove all of the pipeServer code, and you might be able to get this working.

Regarding the addresses themselves, I said previously that the address needs to use forward slashes and it needs to start with uds: unix:. So the only one that might work is number 3 in that list. Try again with that address, without the pipeServer code.

@murgatroid99 Tried without pipeServer and getting different error as below

Server.js

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');

const PROTO_PATH = path.resolve(__dirname, 'namedpipe.proto');

const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const NamedService = grpc.loadPackageDefinition(packageDefinition).NamedPipe.NamedPipeService;
const pipePath = "uds:////./pipe/pipename"

const server = new grpc.Server();

server.bindAsync(pipePath, grpc.ServerCredentials.createInsecure(), (err, port) => {
    if (err) {
      console.error('Error binding server:', err);
    } else {
      console.log('Server bound successfully');
      server.start();
    }
  });

server.addService(NamedService.service, {
  YourRpcMethod: (call, callback) => {
    callback(null, { message: 'Response from server' });
  }
});


// Handle server shutdown gracefully
process.on('SIGINT', () => {
  console.log('Server shutting down...');
  server.tryShutdown(() => {
    console.log('Server shut down gracefully.');
    
  });
});

Client.js

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');

const PROTO_PATH = path.resolve(__dirname, 'namedpipe.proto');

const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const namedService = grpc.loadPackageDefinition(packageDefinition).NamedPipe.NamedPipeService;

const pipePath = "uds:////./pipe/pipename"

const client = new namedService(pipePath, grpc.credentials.createInsecure());

client.YourRpcMethod({ message: 'Request from client' }, (error, response) => {
  if (!error) {
    console.log('Response:', response.message);
  } else {
    console.error('Error:', error);
  }
});

Error:

Error binding server: Error: Failed to parse DNS address dns:uds:////./pipe/pipename
    at Object.onError (C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\server.js:490:34)
    at Immediate.<anonymous> (C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\resolver-dns.js:143:31)
    at processImmediate (node:internal/timers:466:21)

I noticed one thing even if I have added the uds why it is automatically taking (dns as prefix) dns:uds:////./pipe/pipename

@murgatroid99 Another observation as @pelletier197 mentioned
I have tried below pipe strings without dns: prefix and it is automatically taking as below
Pipe Strings List:

const pipePath = "//./pipe/pipename";
const pipePath = "///./pipe/pipename";
const pipePath = "////./pipe/pipename";
const pipePath = "////.//pipe//pipename";
const pipePath = "dns:\\\\.\\pipe\\pipename";

Error List:

Error binding server: Error: Name resolution failed for target dns://./pipe/pipename
    at Object.onError (C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\server.js:490:34)
    at C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\resolver-dns.js:207:31
Error binding server: Error: Name resolution failed for target dns:///./pipe/pipename
    at Object.onError (C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\server.js:490:34)
    at C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\resolver-dns.js:207:31 
Error binding server: Error: Name resolution failed for target dns:////./pipe/pipename
    at Object.onError (C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\server.js:490:34)
    at C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\resolver-dns.js:207:31 
Error binding server: Error: Name resolution failed for target dns:////.//pipe//pipename
    at Object.onError (C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\server.js:490:34)
    at C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\resolver-dns.js:207:31 
Error binding server: Error: Name resolution failed for target dns:\\.\pipe\pipename
    at Object.onError (C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\server.js:490:34)
    at C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\resolver-dns.js:207:31

I made a mistake in my previous suggestions: the correct prefix for UDS and named pipe addresses is actually unix:, not uds:. Try again with unix:////./pipe/pipename.

EDIT: I updated my previous comments to refer to the unix: prefix, so that anyone who reads this issue in the future will have the correct information.

@murgatroid99 tried unix:////./pipe/pipename

E No address added out of total 1 resolved
Error binding server: Error: No address added out of total 1 resolved
    at bindResultPromise.then.errorString (C:\Users\xxx\Desktop\NamedPipes\node_modules\@grpc\grpc-js\build\src\server.js:475:42)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)

@murgatroid99 Finally it worked when I ran the application with administration level. May I know the reason why it is not working normally ? is there any problem ?

I am not very familiar with the named pipes system, so I don't know how its permissions system works. It may be that you just always need administrator privileges to do this.

@murgatroid99 Thank you so much for your continuous support and help ❤️. Closing this as it is resolved.