August 11, 2025 in Systems11 minutes
In a previous post on sockets in Linux we briefly explored Unix Domain Sockets (UDS) by invoking the AF_UNIX
domain in a few examples. In this post I’d like to dig into more of the specifics of using this domain on Linux as there are some quirks that are worth covering that you may not expect if you’ve only ever used domains like AF_INET
/AF_INET6
.
First, let’s start with why you might want to use a unix socket. If you have multiple services on the same machine that need to communicate with each other, you certainly could have them communicate using the traditional TCP or UDP approach using localhost. However, unix sockets offer a simpler and more efficient alternative - they don’t use these L3 or L4 protocols at all and don’t even touch most parts of the traditional networking stack (there’s no need/purpose in configuring iptables to allow access to a unix socket). However, unix sockets are still a full blown communication channel facilitated purely by the kernel that support all three socket types we’ve explored so far - SOCK_STREAM
, SOCK_DGRAM
, and SOCK_SEQPACKET
. You get your choice of which suits you, and you get a communication mechanism that is guaranteed to stay local.
In addition to the inherent security benefits of intentionally restricting access to local services, there are other features of unix sockets we’ll get into that can help even further with security-related concerns.
The tradeoffs between these types we discussed previously remain largely the same whether or not you’re using AF_UNIX
. The only difference is that of course the reliability consideration is certainly quite different on AF_UNIX
(by design we’re cutting out a lot of possible failure modes) so you may make a different decision if you care about the other considerations like preserving message boundaries or ordering. The observed behavior for these types is generally the same as well, like EOF detection on connection-oriented types (recv()
returns 0-length).
It’s also worth restating that unless you’re doing low-level-ish bluetooth programming, SOCK_SEQPACKET
is really only supported on Unix sockets; it’s not supported on the perhaps more familiar AF_INET
/AF_INET6
.
A Unix socket can have one of three “address types”, and the difference between them has to do with how the of the bind()
syscall is invoked by the application.
I’ve also heard these referred to as “unnamed”. In short, this is the address type a socket will use by default, and will remain that way if bind()
is never called.
The primary reason you might see sockets like this is for the client-side of a connection which is established to some other application’s listening socket. While the server side must call bind()
(to make itself available via some named path or an abstract name as we’ll see below), the connect()
side does not and can remain anonymous. However, it does have the option of calling bind()
prior to calling connect()
. This can be done for identification purposes, but also can be useful as a “callback number” if bidirectional communication after the initial connection closes is desired and both endpoints are capable of serving as both client and server flexibly.
There’s another use case where you’ll see these, and that’s when socketpair()
is being used. This is a slightly more niche feature, mostly associated with unix sockets, but one that I want to save for a future blog post.
I may be biased, but to me this is the “canonical” unix socket address type. The “address” in question for these sockets is a real filesystem path - though the file at that path behaves more like a symlink than a real file. Certainly no actual I/O (packet flow etc) takes place on that filesystem, it’s really just a pointer to a socket. Similar to what we’ve already seen in previous blogs, each of the client and server applications get their own file descriptor tables, and each has an FD for each socket which points to an inode that represents their respective sockets.
For example, here’s the path we’ll use in the upcoming examples - /tmp/myunixsocket.sock
. You’ll notice stat
recognizes it as a socket, not just a normal file:
1mierdin@lucy:~$ stat /tmp/myunixsocket.sock
2 File: /tmp/myunixsocket.sock
3 Size: 0 Blocks: 0 IO Block: 4096 socket
4Device: 8,2 Inode: 2883639 Links: 1
5Access: (0775/srwxrwxr-x) Uid: ( 1000/ mierdin) Gid: ( 1000/ mierdin)
6Access: 2025-08-11 08:29:04.398107113 -0400
7Modify: 2025-08-11 08:29:02.205162445 -0400
8Change: 2025-08-11 08:29:02.205162445 -0400
9 Birth: 2025-08-11 08:29:02.205162445 -0400
Before calling listen()
a server should (won’t be reachable otherwise) first call bind()
and provide the path name. This will actually create the file on the filesystem, so naturally the user that is running the program must have permissions to do this at that location.
This is just one example of how filesystem permissions serve an important security role for Unix sockets, and they follow mostly an intuitive pattern. You need write permission to call connect()
for instance. You can control access to this socket in the same way you’d control access to any other file or directory on the system.
Place your sockets well!
Many examples here and elsewhere online use /tmp
for these paths. It’s important to note that this is just for ease of demonstration. Real production servers should ensure they’re binding to a filesystem location which is well protected.
As with AF_INET
/AF_INET6
addresses, if you try to bind to the same pathname, you’ll get EADDRINUSE
error from the kernel. But with unix sockets, and pathname addresses in particular, there’s one extra wrinkle - you must clean up or unlink this socket file yourself, either on server startup, or shutdown. Even if no other process is actually listening on that pathname, if the file exists when bind()
is called, you’ll get that error.
Sometimes you need to create a listening unix socket but you don’t want to use a path name address type. There are use cases which might have a readonly filesystem, for example - so creating the pathname file is out of the question. For this, the option of an “abstract” socket name exists.
The main difference between this and the pathname address type is that the “abstract” type is arbitrary and has nothing to do with any filesystem path. It’s whatever you want to call it, as long as it’s unique in that network namespace. This is especially useful for temporary sockets which may be set up “on demand” based on some other signal an application might use. Abstract socket names have also one benefit over their pathname counterparts in that there is no cleanup necessary for these - as soon as the file descriptor for a socket using an abstract name is closed, the name doesn’t exist.
The main downside to these is that we no longer have access to all of the security benefits of using a real filesystem path. Access control (while still limited to the local machine by virtue of this still being a unix socket) is now entirely up to the application - anything on the machine can see and connect to this socket. Another to point out is that this is another Linux-specific extension, in case portability is a concern.
Here’s a Python server which demonstrates the path
and abstract
types.
1import socket
2import os
3import struct
4import threading
5import time
6
7
8def named_server():
9 s = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
10 socket_path = "/tmp/myunixsocket.sock"
11
12 # Named address types do not auto-remove themselves.
13 # When your server exits, you must ensure the file
14 # referenced by `socket_path` is unlinked/deleted,
15 # or you can delete it just before bind() as done here.
16 try :
17 os.remove(socket_path)
18 except OSError :
19 pass
20
21 s.bind(socket_path)
22 s.listen()
23
24 while True:
25 conn, addr = s.accept()
26 i = 0
27 while True:
28 data = conn.recv(4096)
29 if not data:
30 break
31 data = len(data)
32 print(f"Received data {i} of {data} bytes")
33 i +=1
34
35
36def abstract_server():
37 s = socket.socket(socket.AF_UNIX, socket.SOCK_SEQPACKET)
38
39 # Abstract address types do their own cleanup when the
40 # corresponding socket FD is closed (for instance if
41 # our program exits). So no need to remove/unlink anything.
42 # Just bind to the name, with the null byte prefix "\0"
43 # to indicate the abstract type
44 s.bind('\0our_abstract_socket')
45 s.listen()
46
47 while True:
48 conn, addr = s.accept()
49 i = 0
50 while True:
51 data = conn.recv(4096)
52 if not data:
53 break
54 data = len(data)
55 print(f"Received data {i} of {data} bytes")
56 i +=1
57
58if __name__ == "__main__":
59 threading.Thread(target=named_server, daemon=True).start()
60 abstract_server()
If we start this (as well as a client script which makes a connection to both listening sockets) we get the following ESTAB
sockets:
1mierdin@lucy:~$ ss -p -A 'unix_seqpacket' | sed -n '1p; /python3/p'
2State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
3ESTAB 0 0 * 217512 * 218297 users:(("python3",pid=42820,fd=3))
4ESTAB 0 0 @our_abstract_socket 217511 * 214734 users:(("python3",pid=42818,fd=5))
5ESTAB 0 0 /tmp/myunixsocket.sock 218297 * 217512 users:(("python3",pid=42818,fd=6))
6ESTAB 0 0 * 214734 * 217511 users:(("python3",pid=42820,fd=4))
The first and last are our client sockets. These have no local address, as we didn’t call bind()
with them at all. So these are “unnamed”. The second, @our_abstract_socket
is our abstract socket, as indicated by the @
symbol. The third is showing it’s bound to the named path /tmp/myunixsocket.sock
.
Remember, these are just the ESTAB
sockets that represent live connections - there are also LISTEN
sockets which the server is actually calling accept()
on (this is how the ESTAB
sockets are produced). You can see these if you include the -a
flag above. They’ll have the same Local Address
as their ESTAB
counterpart.
You’ll also notice that in lieu of “port”, which we’d see for AF_INET(6)
sockets, the inode of the socket is shown instead. If you look closely, you’ll notice that he local inode for each socket corresponds with the peer inode of another. this is how you can match up which unnamed socket is connected to which server socket.
Check for kernel support!
A lesson I had to learn the hard way after more struggling than I care to admit, information about the peer inode is only available if your kernel was built with CONFIG_NETLINK_DIAG
and CONFIG_UNIX_DIAG
(ss
uses netlink to obtain this information). If you’re seeing 0 here instead, check that your kernel was built with this option.
Any of these four sockets can have getpeername()
or getsockname()
called on them, though obviously only sockets being referred to here which are bound to an abstract or path name will produce a non-empty result.
Often in Linux, it can be desired to isolate resources using namespaces. If you’ve used these for things like sockets or interfaces, you’re probably familiar with network namespaces. One “oddity” about unix sockets is that if you’re using a “path” address type, a “mount namespace” is required, rather than a network namespace. This can be a very useful property - perhaps you’re running a local proxy and you want local requests to be in the root mount namespace, but outbound network requests are done using a network namespace. However, if you want the listening socket to be namespaced, you’ll have to reach for a mount namespace as well.
Abstract address types, on the other hand are bounded entirely by network namespaces, like you might already be accustomed to doing with sockets from the INET domains. Without the use of network namespaces, having multiple abstract sockets with the same name wouldn’t be possible (just as with pathname type, if you try to bind to an abstract name that’s already in use, you’ll get EADDRINUSE
). We can see below that we indeed have two different programs running and bound to the same abstract socket name (because they’re isolated using network namespaces)
1mierdin@lucy:~$ ss -p -A 'unix_seqpacket' | sed -n '1p; /our_abstract_socket/p'
2State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
3ESTAB 0 0 @our_abstract_socket 217511 * 214734 users:(("python3",pid=42818,fd=5))
4mierdin@lucy:~$ sudo nsenter -t $(pgrep -f "^python3 unix_abstract_server.py") -n ss -ap -A 'unix_seqpacket' | sed -n '1p; /python3/p'
5State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
6LISTEN 0 128 @our_abstract_socket 227365 * 0 users:(("python3",pid=43518,fd=3))
I’d like to highlight that the docs and other resources commonly refer to the “abstract namespace” referring to sockets with this address type. This only refers to the scope in which the name is unique - it doesn’t mean you have to use network namespaces (i.e. CLONE_NEWNS
) in order to use them. Just a little ambiguity between two totally unrelated usages of the term “namespace”.
Hopefully you’ve been able to see that Unix sockets have their place, and in fact have advantages over what we’ve seen thus far, provided the local-only constraints work for you. In future blog posts we’ll explore some additional functionality that is specific to this domain (as you might imagine, there are more liberties we can take when it comes to efficiency and flexibility when we know both sides of a connection are local).