Skip to content

Instantly share code, notes, and snippets.

@aszlig

aszlig/test.adoc Secret

Last active January 15, 2019 23:40
Show Gist options
  • Save aszlig/2744d23ee4abce179f7a75b5450b6548 to your computer and use it in GitHub Desktop.
Save aszlig/2744d23ee4abce179f7a75b5450b6548 to your computer and use it in GitHub Desktop.

IP2Unix - Turn IP sockets into Unix domain sockets

built with nix build status

Executes a program and converts IP to Unix domain sockets at runtime based on a list of rules, either given via short command line options (see [Rule specification]) or via a file with a list of rules (see [Rule file format]). The first matching rule causes ip2unix to replace the current IP socket with a Unix domain socket based on the options given. For example if a socketPath is specified, the Unix domain socket will bind or listen to the given path.

Problem statement

A lot of programs are designed to only work with IP sockets, however very few of them allow to communicate via Unix domain sockets. Unix domain sockets usually are just files, so standard Unix file permissions apply to them.

IP sockets also have the disadvantage that other programs on the same host are able to connect to them, unless you use complicated netfilter rules or network namespaces.

So if you either have a multi-user system or just want to separate privileges, Unix domain sockets are a good way to achieve that.

Another very common use case in nowadays systems is when you’re using systemd and want to use socket units to allow parallel startup of services.

Short example

Let’s say you have a small HTTP server you want to make available behind a HTTP reverse proxy.

$ ip2unix -r path=/run/my-http-server.socket my-http-server

This will simply convert all IP sockets to the Unix domain socket available at /run/my-http-server.socket. If you use a web server like nginx, you can use the following directive to connect to that socket:

proxy_pass http://unix:/run/my-http-server.socket;

More examples can be found further below in section [Examples].

A short summary of all the options is available via ip2unix --help or man ip2unix if you want to see all the details and options available.

Table of Contents

1. Build from source

1.1. Requirements

  • Meson, at least version 0.46.0.

  • Ninja, at least version 1.5.

  • yaml-cpp, at least version 0.5.0

  • C++ compiler supporting C++17 (GNU G++ version 7.0 onwards).

  • Python 3, at least version 3.6 is needed for running the integration tests.

Optional dependencies:
  • AsciiDoc or Asciidoctor for generating the manpage. The former is recommended as it generates a better manpage and also provides validation.

  • pytest for running automated tests.

  • systemd-socket-activate helper to run test cases specific to systemd socket activation support.

1.2. Cloning the repository

The source code can be fetched via Git using the following command:

$ git clone https://github.com/nixcloud/ip2unix.git

You will get an ip2unix directory inside your current working directory. All of the following steps are to be performed inside this ip2unix directory.

1.2.1. Using the Nix package manager

This is the easiest and recommended way to compile it from source and it should work on any distribution.

If you are not running NixOS you can install Nix via the following command:

$ curl https://nixos.org/nix/install | sh

In order to build ip2unix issue the following command from the top of the source tree:

$ nix-build

This takes care of fetching the dependencies, building and running the test suite. The resulting command can now be found in result/bin/ip2unix.

If you want to add the package to your user environment, you can install it using the command:

$ nix-env -f . -i

1.2.2. Debian and derivatives

Fetching the requirements on Debian 9 can be tricky because only GCC version 6 is available, so using the Nix package manager is recommended here as it does not interfere with the rest of the system.

However if you still want to build it on Debian 9, you can install g++-7 from testing. Please be aware that mixing packages from testing might update unrelated packages and might make your system less stable.

In addition to the C++ compiler, the version of Meson on Debian 9 is too old as well. However it can be installed from the Python Package Index using pip:

$ sudo apt-get install python3-pip
$ pip3 install meson

After you are done getting a newer Meson and C++ compiler, you can use the following command to install the other required dependencies:

$ sudo apt-get install ninja-build pkg-config libyaml-cpp-dev

If you want to have a manpage and support for systemd socket activation:

$ sudo apt-get install asciidoctor libsystemd-dev

In case you want to run the test suite, pytest is required:

$ sudo apt-get install python3-pytest

1.2.3. RPM-based distributions

On Fedora 29, all of the dependencies are recent enough, so in order to install the required dependencies:

$ sudo yum install meson gcc-c++ yaml-cpp-devel

The optional dependencies for the manpage and for systemd socket activation:

$ sudo yum install asciidoctor systemd-devel

If you want to run the test suite:

$ sudo yum install python3-pytest

1.2.4. Arch Linux and derivatives

To install the required dependencies:

$ sudo pacman -S yaml-cpp meson gcc pkg-config

If you want to have the manpage:

$ sudo pacman -S asciidoctor

In case you want to run the test suite:

$ sudo pacman -S python-pytest

1.3. Building

$ meson build

If you want to specify a different compiler executable, eg. g++-7:

$ CXX=g++-7 meson build

Compile:

$ ninja -C build

The executable is then placed in build/ip2unix, so to show the usage:

$ build/ip2unix --help

1.4. Installation

To install ip2unix, run the following command:

$ ninja -C build install

By default, this will install ip2unix in /usr/local/bin/ip2unix.

1.5. Running tests

$ ninja -C build test

2. Rule specification

Arguments specified via -r contain a comma-separated list of either flags or options. If a value contains a comma (,), it has to be escaped using a backslash (\) character. If you want to have a verbatim backslash character just use two consecutive backslashes instead.

The following flags are available:

in | out

Corresponds to the direction rule file option and if it is not set, both incoming and outgoing connections are matched.

tcp | udp

Either match TCP or UDP sockets or both if none of these flags are set (type rule file option).

systemd[=FD_NAME]

Enable systemd socket activation (see socketActivation below), optionally specifying a file descriptior name (fdName).

reject[=ERRNO]

Reject calls to connect and bind with EACCES by default or the ERRNO specified either via name or as an integer.

blackhole

When binding the socket, use a temporary file system path and unlink it shortly after the bind. This is a way to deactivate a specific socket without the application noticing.

ignore

Don’t handle the socket matching this rule, see the corresponding rule file option *ignore.

These options are available:

addr[ess]=ADDRESS

Optional, specifies an IPv4 or IPv6 address, see address rule file option.

port=PORT[-PORT_END]

Optional, specifies a port to match, see the port and optionally the portEnd rule file option if you want to specify a port range.

unix=PATH

Match on an existing Unix domain socket path, where PATH may contain glob(7)-style wildcards. See the unix rule file option below for the description of the syntax. Note that if you don’t explicitly specify this option, only IP sockets are matched.

path=SOCKET_PATH

The path to the socket file to either bind or connect to, which is similar to the socketPath rule file option but also allows relative paths.

3. Rule file format

The rule file (specified via -f is a YAML file (or JSON, as it is a subset of YAML), consisting of an array of objects.

Each object consists of keys/values which define which IP sockets to match and which Unix domain sockets to assign them to.

3.1. Rule file options

direction

Whether this rule applies to a server-side socket (incoming), a client-side socket (outgoing) or both if not defined.

type

Specifies the IP type, which currently is either tcp for TCP sockets, udp for UDP sockets or if it is not defined it matches both UDP and TCP sockets.

address

The IP address to match, which can be either an IPv4 or an IPv6 address.

port

UDP or TCP port number (depending on which type is set), which for outgoing connections specifies the target port and for incomping connections the port that the socket is bound to.

portEnd

Optionally specifies the end of a port range to match, so for example if port is 2000 and portEnd is 3000 all ports in the range from 2000 to 3000 (inclusive) are matched.

unix

Match an existing Unix domain socket against a glob(7)-style pattern and replace it by a new Unix domain socket according to the rule options.

Note that if you don’t explicitly specify this option, only IP sockets are matched.

The pattern syntax is as follows:

*

Any string, including the empty string, but not /.

?

A single character excluding /.

[...]

A single character, namely any of the characters enclosed by the brackets. Ranges can be specified by two characters separated by -. For example [a-f] is equivalent to [abcdef].

**

Match multiple (zero or more) directories, for example foo/**/bar matches foo/a/b/c/bar as well as foo/a/c/bar and foo/bar.

socketPath

The path to the socket file to use for either binding or connecting to depending on whether the above options apply for a particular IP socket.

Placeholders are allowed here and those are substituted accordingly:

%p

port number

%a

IP address or unknown

%t

socket type (tcp, udp or unknown)

%%

verbatim %

socketActivation

If ip2unix is compiled with systemd support, whether to use socket activation instead of a socketPath. See systemd.socket(5).

fdName

An optional file descriptor name for socket activation which can be used to distinguish between several socket units. This corresponds to the FileDescriptorName systemd socket option.

reject

If true, reject calls to connect and bind with EACCES.

rejectError

Specifies an alternative error code to be returned by reject instead of EACCES. This can be either a string such as EADDRINUSE (case does not matter) or an integer.

blackhole

If true, a temporary file system path is used and unlinked shortly thereafter, so the socket is effectively deactivated in a way that the application should not recognize. Only valid if direction is incoming.

ignore

Prevents a socket from being converted to a Unix domain socket if this is true. This is useful to exempt specific sockets from being matched when another rule matches a broad scope.

4. Examples

4.1. Simple HTTP client/server

On the server side with the rule file rules-server.yaml:

- direction: incoming
  socketPath: /tmp/test.socket

The following command spawns a small test web server listening on /tmp/test.socket:

$ ip2unix -f rules-server.yaml python3 -m http.server 8000

The same can be achieved using -r:

$ ip2unix -r in,path=/tmp/test.socket python3 -m http.server 8000

On the client side with rules-client.yaml:

- direction: outgoing
  socketPath: /tmp/test.socket

This connects to the test server listening on /tmp/test.socket and should show the directory listing:

$ ip2unix -f rules-client.yaml curl http://1.2.3.4/

With the -r option:

$ ip2unix -r out,path=/tmp/test.socket curl http://1.2.3.4/

4.2. More complicated example

- direction: outgoing                 ## (1)
  port: 53
  ignore: true
- direction: outgoing                 ## (2)
  type: tcp
  socketPath: /run/some.socket
- direction: incoming                 ## (3)
  address: 1.2.3.4
  socketPath: /run/another.socket
- direction: incoming                 ## (4)
  port: 80
  address: abcd::1
  blackhole: true
- direction: incoming                 ## (5)
  port: 80
  reject: true
  rejectError: EADDRINUSE
- direction: incoming                 ## (6)
  type: tcp
  port: 22
  socketActivation: true
  fdName: ssh
  1. All outgoing connections to port 53 (no matter if it’s TCP or UDP) will not be converted into Unix domain sockets.

  2. This rule will redirect all TCP connections except to port 53 (see above) to use the Unix domain socket at /run/some.socket.

  3. Matches the socket that listens to any port on the IPv4 address 1.2.3.4 and instead binds it to the Unix domain socket at /run/another.socket.

  4. The application may bind to the IPv6 address abcd::1 on port 80 but it will not receive any connections, because no socket path exists.

  5. Trying to bind to port 80 on addresses other than abcd::1 will result in an EADDRINUSE error.

  6. Will prevent the TCP socket that would listen on port 22 to not listen at all and instead use the systemd-provided file descriptor named ssh for operations like accept(2).

The same can be achieved solely using -r commandline arguments:

$ ip2unix -r out,port=53,ignore \
          -r out,tcp,path=/run/some.socket \
          -r in,addr=1.2.3.4,path=/run/another.socket \
          -r in,port=80,reject=EADDRINUSE \
          -r in,tcp,port=22,systemd=ssh

5. Limitations

  • The program uses LD_PRELOAD (ld.so(8)), so it will only work with programs that are dynamically linked against the C library. Using ip2unix on statically linked executables or on executables that don’t use the socket family functions of the C library (like Go programs) will not work at the moment.

  • If a client which is already using Unix datagram sockets sends packets via sendto or sendmsg to a socket provided by ip2unix without binding first, ip2unix is not able to identify the peer and will subsequently reject the packet. This is not the case when using ip2unix itself on the the client side and it also does not seem to be very common as the author so far did not find such an application in the wild.

    However, if this really is an issue to you, the recommended workaround is either to use ip2unix to wrap the client (if it supports IP sockets) or fix the server to natively use Unix domain sockets.

6. Similar projects

socket_wrapper

The goal is a different one here and its main use is testing. Instead of using rules, socket_wrapper turns all of the IP sockets into Unix sockets and uses a central directory to do the mapping.

Containing all Unix sockets into one directory has the nice effect that it is easy to map any address/port combination to Unix sockets. While this is way easier to implement than our approach it has the drawback that everything is contained and no IP communication is possible anymore.

7. Thanks

Special thanks to the NLnet foundation for sponsoring this work.

Copyright © 2018 aszlig. License LGPLv3: GNU LGPL version 3 only https://www.gnu.org/licenses/lgpl-3.0.html.

This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment