Skip to content
This repository has been archived by the owner on May 4, 2018. It is now read-only.

linux abstract sockets support in connect / listen #1486

Open
sidorares opened this issue Sep 22, 2014 · 29 comments
Open

linux abstract sockets support in connect / listen #1486

sidorares opened this issue Sep 22, 2014 · 29 comments

Comments

@sidorares
Copy link

libuv already supports abstract sockets in uv_pipe_getsockname

Unfortunately, there is no way to connect to abstract socket with uv_pipe_connect.

I'm currently wrapping socat binary to be able to connect to abstract socket address from node-dbus client but it would make life so much easier if abstract socket would just work from core node.

Some my notes here: sidorares/abstractsocket#3

Few problems here:
Afaik python and ruby connect to abstract sockets using "\0name". At uv level allowing zero in socket name would require changing uv_pipe_connect signature (or adding very similar one like uv_pipe_connect2(req, handle, name, name_length, cb). Possible solution: encode leading zero somehow ( "@" prefix? do we want to allow non-abstract sockets to connect to "@name"? If so, how to escape it - "@@name"? ).

I have node version which is able to connect to socat abstract-listen:somename - server by doing net.connect('\0somename'). Would you be interested in PR? Is yes, which is preferred way - add name_length parameters where required or encode "\0" at js side and decode back at uv side ( by prefixing with @ for example )?

@saghul
Copy link
Contributor

saghul commented Sep 22, 2014

Hi! Yeah, both connect and bind (for pipes) would need to be adjusted. Now, given that abstract sockets are a linux-only thing, we'd be penalyzing the API for everyone else, I'm not 100% sure I'd want to do that. You can still create the socket yourself and pass it uv_pipe_open, that will work.

As for making this work with node, I created node-abstractsocket a while ago: https://github.com/saghul/node-abstractsocket, which exposes the same API as the net module, you might find it useful.

@sidorares
Copy link
Author

great, I searched for existing module a while ago and couldn't find any

basic support ( pipe_open only ) would be extra ~10 lines of code in terms of size and nearly zero penalty in execution speed ( extra check first byte of name ). API wise, that's extra paragraph in the doco "to connect to abstract socket you need to prefix name with '@'"

@saghul
Copy link
Contributor

saghul commented Sep 22, 2014

great, I searched for existing module a while ago and couldn't find any

I created it recently, to get my feet wet in addons development, but it should be solid, there is not much to it :-)

basic support ( pipe_open only ) would be extra ~10 lines of code in terms of size and nearly zero penalty in execution speed ( extra check first byte of name ). API wise, that's extra paragraph in the doco "to connect to abstract socket you need to prefix name with '@'"

"@" is just what Linux uses to pretty print abstract socket names, using it to delimit actual abstract sockets is a workaround. I just did a quick test and I can bind to a Unix domain socket called "@lala" just fine. Whould would use that as a name I don't know, but I'd rather not guess :-)

@sidorares
Copy link
Author

well, that would be handled in "unescape" part of non-abstract connect :)
anyway, I tried your module and it worked perfectly ( to my surprise no big difference in speed )

another reason to support - python and ruby have abstract sockets support, so might benefit pyuv
As a node user, I don't want to see in core something linux only (in most cases, dbus only). But as a client library author, I do - no native dependencies and cross platform happiness.

@saghul
Copy link
Contributor

saghul commented Sep 22, 2014

another reason to support - python and ruby have abstract sockets support, so might benefit pyuv
As a node user, I don't want to see in core something linux only (in most cases, dbus only). But as a client library author, I do - no native dependencies and cross platform happiness.

I actually plan to add support for it in pyuv, but there it's easy because a Python string can have embedded nulls, so I can extract the C char pointer and real length and workaround this limitation.

Given that this a Linux only thing, the cross-platform argument loses a bit of strength ;-) but that doesn't mean it wouldn't be nice to have it baked in.

@indutny, @bnoordhuis, @piscisaureus: what do you guys think? Should we get the length when binding / connecting to a pipe?

@sidorares
Copy link
Author

s/cross-platform/cross-architecture/ :) raspberry pi / android devices / routers / tizen

@txdv
Copy link
Contributor

txdv commented Sep 22, 2014

So we only need to add a length parameter alongside the the string?

Trivial solution and binding authors wouldn't be too mad about. Would add a bit more C code, but C is already tedious.

@saghul
Copy link
Contributor

saghul commented Sep 22, 2014

So we only need to add a length parameter alongside the the string?

Yes, that's all we need because strlen won't do for abstract sockets. Then well check internally if name[0] == '\0' and build the sockaddr_un struct accordingly.

@txdv
Copy link
Contributor

txdv commented Sep 25, 2014

Passing a \0 prefixed should already work for uv_pipe_connect.

We are not using strlen anywhere.

But for uv_pipe_bind we'd need a length parameter, since we are using strdup:

pipe_fname = strdup(name);

But we could just add a third parameter ssize_t length which when set to NULL, would do a normal strdup.

We also could just check in the string if the first char is == '\0'. I don't think any one would open a pipe with the name "".

@saghul
Copy link
Contributor

saghul commented Sep 25, 2014

strncpy will stop when it finds the first '\0', it doesn't work.

@txdv
Copy link
Contributor

txdv commented Sep 25, 2014

but it works for some reason even with that strncpy

@txdv
Copy link
Contributor

txdv commented Sep 25, 2014

My proposed api breakes if you pass a memory chunk with the size 1 having only "\0".

Then it will raise a memory exception.

@saghul
Copy link
Contributor

saghul commented Sep 25, 2014

That approach won't work. Abstract sockets are allowed to have embedded nulls, which you can't accommodate unless you accept the length.

Add a test with "\0uv-test\0ouch" and you'll see.

@txdv
Copy link
Contributor

txdv commented Sep 25, 2014

another funny thing is that "\0\0" is a valid unix domain socket.

@sidorares
Copy link
Author

@txdv yeah, while your solution is good enough for my use case unfortunately it's not just '\0' + normal c string

@txdv
Copy link
Contributor

txdv commented Sep 25, 2014

what other usecases are there?

@saghul
Copy link
Contributor

saghul commented Sep 25, 2014

@txdv what I said, embedded nulls.

@txdv
Copy link
Contributor

txdv commented Sep 25, 2014

0 everywhere. Why would they add those embedded zeros?

@txdv
Copy link
Contributor

txdv commented Sep 25, 2014

http://troydhanson.github.io/misc/Unix_domain_sockets.html

it says nothing about embedded nulls apart from the first one

@saghul
Copy link
Contributor

saghul commented Sep 25, 2014

man unix:

abstract: an abstract socket address is distinguished by the fact that sun_path[0] is a null byte ('\0'). The socket's address in this namespace is
given by the additional bytes in sun_path that are covered by the specified length of the address structure. (Null bytes in the name have no special
significance.
) The name has no connection with filesystem pathnames. When the address of an abstract socket is returned by getsockname(2), getpeer‐
name(2), and accept(2), the returned addrlen is greater than sizeof(sa_family_t) (i.e., greater than 2), and the name of the socket is contained in the
first (addrlen - sizeof(sa_family_t)) bytes of sun_path. The abstract socket namespace is a nonportable Linux extension.

@bnoordhuis
Copy link
Contributor

@saghul is correct; if the path starts with a null byte, the kernel treats it as an opaque binary blob (up to 108 bytes, that is.)

By the way, there's also an autobind feature for abstract sockets. When you call bind() with addrlen == sizeof(short), the kernel will assign the first free name from a 1M entries namespace. It can, in theory, fail with ENOSPC when all entries are in use or take a long time to complete because the scan is linear.

@sidorares
Copy link
Author

@bnoordhuis interesting. Is this (autobind) documented somewhere?

@bnoordhuis
Copy link
Contributor

@sidorares It should be documented in man 7 unix somewhere (and looking at it, it recommends that you use sizeof(sa_family_t).)

@sidorares
Copy link
Author

   Autobind feature
   If a bind(2) call specifies addrlen as sizeof(sa_family_t), or the SO_PASSCRED socket option was specified for a
   socket  that  was  not explicitly bound to an address, then the socket is autobound to an abstract address.  The
   address consists of a null byte followed by 5 bytes in the character set [0-9a-f].  Thus, there is  a  limit  of
   2^20  autobind  addresses.   (From Linux 2.1.15, when the autobind feature was added, 8 bytes were used, and the
   limit was thus 2^32 autobind addresses.  The change to 5 bytes came in Linux 2.3.15.)

@kroggen
Copy link

kroggen commented Mar 19, 2018

As Android uses abstract sockets, it would be good if libuv could support it.

Here are some alternatives to use a length parameter:

1 . Using the length encoded in the string, like a netstring format but without the final comma:

\04:test

\0 + <length> + : + <socket name>

2 . If the first char is null, use the next one as que delimiter of the end:

\0|test|
\0=test=
\0:test:

In this method the user can choose a delimiter that is not contained in the socket name.

Both methods support nulls in the socket name.

@kroggen
Copy link

kroggen commented Mar 19, 2018

Interestingly, with method 2 if we will use an abstract socket name without nulls inside (most cases) we can just do:

\0\0test

Because it will have the null character at the end.

Creating the socket name for libuv

Without nulls:

char name[128];
name[0] = 0;
name[1] = 0;
strncpy(&name[2], plain_name, 128-3);

Containing null:

char name[128], *ptr = &name[0];
*p++ = 0;
*p++ = '|';  /* or another char */
memcpy(p, sock_name, sock_length);
p += sock_length;
*p++ = '|';  /* or another char */

and then:

uv_pipe_bind(handle, name);

@kroggen
Copy link

kroggen commented Mar 19, 2018

We could even have a function inside libuv for creating these socket names.

It could detect the presence of chars to select the delimiter automatically:

char * uv_build_abstract_socket_name(char *name, int length, char *buf) {
  char used[256], delimiter, *dest = buf;
  int i, found=0;

  if (length > MAX_ABS_SOCK_LEN) return 0;

  /* mark the used characters */
  memset(used, 0, 256);
  for (i=0; i<length; i++){
    used[name[i]] = 1;
  }
  /* search for an unused character */
  for (i=0; i<256; i++){
    if (!used[i]) {
      delimiter = i;
      found = 1;
      break;
    }
  }
  if (!found) return 0;

  /* build the abstract socket name */
  *dest++ = 0;
  *dest++ = delimiter;
  memcpy(dest, name, length);
  dest += length;
  *dest++ = delimiter;
  return buf;
}

Usage:

char buf[128], *sock_name;

sock_name = uv_build_abstract_socket_name(name, strlen(name), buf);
if (!sock_name) handle_error_here;

uv_pipe_bind(handle, sock_name);

@kroggen
Copy link

kroggen commented Mar 19, 2018

And then libuv could read these values, like this (not tested):

int uv__get_pipe_name(char *name, char *pipe_name, int max_len) {
  int len;
  if (name[0]==0) {
    char delimiter = name[1];
    char *end = strchr(&name[2], delimiter);
    len = end - name - 2;
    if (len > max_len) return 0;
    *pipe_name++ = 0;  /* store the starting null char */
    memcpy(pipe_name, &name[2], len);
    len++;             /* include the first null char */
  } else {
    len = strlen(name);
    if (len > max_len) return 0;
    strcpy(pipe_name, name);
  }
  return len;
}

Usage:

  char pipe_name[128];
  int len = uv__get_pipe_name(name, pipe_name, sizeof(saddr.sun_path) - 1);
  if (len <= 0) return UV_EINVAL;
  ...
  memcpy(saddr.sun_path, pipe_name, len);

I know this is not the place to post code, but as I don't have enough time to make the full implementation I will let my code here as a contribution.

@kroggen
Copy link

kroggen commented Mar 19, 2018

I like the idea of using a function from libuv to build the abstract socket name. This allows us to change the internal (libuv) name representation without breaking backwards compatibility.

Note that the user does not need to know how libuv encodes it. It just needs to call the uv_build_abstract_socket_name function (or another name).

We could even use another format: with the length encoded in a single byte.

00 04 test
|  |  |
|  |  +-> pipe name
|  +-> length 
+-> null character prefix

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants