Introducing oniux: Kernel-level Tor isolation for any Linux app

by cve | May 14, 2025

When launching privacy-critical apps and services, developers want to make sure that every packet really only goes through Tor. One mistyped proxy setting–or a single system-call outside the SOCKS wrapper–and your data is suddenly on the line.

That's why today, we are excited to introduce oniux: a small command-line utility providing Tor network isolation for third-party applications using Linux namespaces. Built on Arti, and onionmasq, oniux drop-ships any Linux program into its own network namespace to route it through Tor and strips away the potential for data leaks. If your work, activism, or research demands rock-solid traffic isolation, oniux delivers it.

What are Linux namespaces? 🐧

Namespaces are an isolation feature found in the Linux kernel that were introduced around the year 2000. They provide a secure way of isolating a certain part of an application from the rest of the system. Namespaces come in various forms and shapes. Some examples include network namespaces, mount namespaces, process namespaces, and a few more; each of them isolating a certain amount of system resources from an application.

What do we mean by system resources? In Linux, system resources are available globally by all applications on the system. The most notable example of this is probably your operating system clock, but there are many other areas as well, such as the list of all processes, the file system, and the list of users.

Namespaces containerize a certain part of an application from the rest of the operating system; this is exactly what Docker uses in order to provide its isolation primitives.

Tor + Namespaces = ❤️

As outlined above, namespaces are a powerful feature that gives us the ability to isolate Tor network access of an arbitrary application. We put each application in a network namespace that doesn't provide access to system-wide network interfaces (such as eth0), and instead provides a custom network interface onion0.

This allows us to isolate an arbitrary application over Tor in the most secure way possible software-wise, namely by relying on a security primitive offered by the operating system kernel. Unlike SOCKS, the application cannot accidentally leak data by failing to make some connection via the configured SOCKS, which may happen due to a mistake by the developer.

oniux vs. torsocks

You may have also heard of a tool with a similar goal, known as torsocks, which works by overwriting all network-related libc functions in a way to route traffic over a SOCKS proxy offered by Tor. While this approach is a bit more cross-platform, it has the notable downside that applications making system calls not through a dynamically linked libc, either with malicious intent or not, will leak data. Most notably, this excludes support for purely static binaries and applications from the Zig ecosystem.

The following provides a basic comparison on oniux vs torsocks:

oniux torsocks Standalone application Requires running Tor daemon Uses Linux namespaces Uses an ld.so preload hack Works on all applications Only works on applications making system calls through libc Malicious application cannot leak Malicious application can leak by making a system call through raw assembly Linux only Cross-platform New and experimental Battle-proven for over 15 years Uses Arti as its engine Uses CTor as its engine Written in Rust Written in C

How can I use oniux? 🧅

First, you need a Linux system with a Rust toolchain installed. Afterwards, you can install oniux with the following command:

$ cargo install --git https://gitlab.torproject.org/tpo/core/oniux oniux@0.4.0

Once that is done, you are ready to go for using oniux! 🙂

Using oniux is straightforward:

# Perform a simple HTTPS query using oniux!
$ oniux curl https://icanhazip.com
<A TOR EXIT NODE IP ADDRESS>
# oniux also supports IPv6 of course!
$ oniux curl -6 https://ipv6.icanhazip.com
<A TOR EXIT NODE IPv6 ADDRESS>
# Tor without onion services is like a car without an engine ...
$ oniux curl http://2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion/index.html
# You can also enable logging if you are a nerd. 🤓
$ RUST_LOG=debug oniux curl https://icanhazip.com
# If you want, you can "torify" your entire shell, isolating all processes within!
$ oniux bash
# If you are in a desktop environment, you can isolate graphical applications too!
$ oniux hexchat

How does this work internally? ⚙️

oniux works by immediately spawning a child process using the clone(2) system call, which is isolated in its own network, mount, PID, and user namespace. This process then mounts its own copy of /proc followed by UID and GID mappings to the respective UID and GID of the parent process.

Afterwards, it creates a temporary file with nameserver entries which will then be bind mounted onto /etc/resolv.conf, so that applications running within will use a custom name resolver that supports resolving through Tor.

Next, the child process utilizes onionmasq to create a TUN interface named onion0 followed by some rtnetlink(7) operations required to set up the interface, such as assigning IP addresses.

Then, the child process sends the file descriptor of the TUN interface over a Unix Domain socket to the parent process, who has been waiting for this message ever since executing the initial clone(2).

Once that is done, the child process drops all of its capabilities which were acquired as part of being the root process in the user namespace.

Finally, the command supplied by the user is executed using facilities provided by the Rust standard library.

oniux is experimental ⚠️

Although this section should not discourage you from using oniux, you should keep in mind that this is a relatively new feature which uses new Tor software, such as Arti and onionmasq.

While things are already working as expected at the moment, tools such as torsocks have been around for over 15 years, giving them more experience on the battlefield.

But we do want to reach a similar state with oniux, so please go ahead and check it out!

Credits

Many thanks to the developers of smoltcp, which is a Rust crate that implements a full IP stack in Rust -- something, we make heavy use of.

Also many thanks go to 7ppKb5bW, who taught us on how this can implemented without the use of capabilities(7) by using user_namespaces(7) properly.

Last but not least, many thanks to all people and organizations who support Tor financially. The Tor Project, Inc. is a 501(c)(3) nonprofit advancing human rights and defending privacy online through free software and open networks. The oniux release is powered by a community of supporters. Please consider donating today to continue advancing our work that makes privacy possible.


This is a companion discussion topic for the original entry at https://blog.torproject.org/introducing-oniux-tor-isolation-using-linux-namespaces
8 Likes

You seem to have a couple of truncated onion addresses here. The oniux GitLab repo is here (Clearnet The Tor Project / Core / oniux · GitLab). The curl command example has what looks like the first 53 characters of an onion address too.

Also, for users who do not have cargo set up to resolve .onion addresses you may want to explain how this is done as cargo install --git http://eweiibe6tdjsdprb4px6rqrzzcsi22m4koia44kc5pcjr7nec2rlxyad.onion/tpo/core/oniux on my system results in a DNS error failed to resolve address.

Anyhow, after updating my rust toolchain (1.69.0 resulting in a dependency issue) I managed to build and install oniux. I look forward to trying out this utility, it looks very useful.

I just tried oniux wget -O - https://check.torproject.org and got

!doctype html>
<html lang="en_US">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>
    
      Congratulations. This browser is configured to use Tor.
...

:grinning_face:

Are you using an old or patched version of curl?

curl refuses to resolve .onion domains to avoid leakage:

I got those errors while trying using oniux hexchat:

2025-05-16T12:36:19Z DEBUG oniux] spawned onion-tunnel thread
thread '<unnamed>' panicked at src/main.rs:174:18:
called `Result::unwrap()` on an `Err` value: ArtiSetup(Error { detail: StateMgrSetup(Error { source: Inaccessible(BadOwner("/", 65534)), action: Initializing, resource: Directory { dir: "/home/dirtyoldman/.local/share/arti/state" } }) })
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'main' panicked at src/main.rs:136:22:
called `Result::unwrap()` on an `Err` value: No such file or directory (os error 2)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'main' panicked at core/src/panicking.rs:221:5:
panic in a function that cannot unwind
stack backtrace:
   0:     0x5d24ed3ef36a - <std::sys::backtrace::BacktraceLock::print::DisplayBacktrace as core::fmt::Display>::fmt::h304520fd6a30aa07
   1:     0x5d24ed41ad5b - core::fmt::write::hf5713710ce10ff22
   2:     0x5d24ed3eb7a3 - std::io::Write::write_fmt::hda708db57927dacf
   3:     0x5d24ed3f07f2 - std::panicking::default_hook::{{closure}}::he1ad87607d0c11c5
   4:     0x5d24ed3f045e - std::panicking::default_hook::h81c8cd2e7c59ee33
   5:     0x5d24ed3f107f - std::panicking::rust_panic_with_hook::had2118629c312a4a
   6:     0x5d24ed3f0d33 - std::panicking::begin_panic_handler::{{closure}}::h7fa5985d111bafa2
   7:     0x5d24ed3ef849 - std::sys::backtrace::__rust_end_short_backtrace::h704d151dbefa09c5
   8:     0x5d24ed3f09f4 - rust_begin_unwind
   9:     0x5d24ec773fd5 - core::panicking::panic_nounwind_fmt::hc0ae93930ea8f76c
  10:     0x5d24ec774062 - core::panicking::panic_nounwind::h9f485ff9b02bac75
  11:     0x5d24ec7741a6 - core::panicking::panic_cannot_unwind::hea865182d7ce50af
  12:     0x5d24ed2f9f36 - nix::sched::sched_linux_like::clone::callback::h7d99c497d382c0d3
  13:     0x7ca9ce138684 - clone
  14:                0x0 - <unknown>
thread caused non-unwinding panic. aborting.

Any suggestions?

This is a great project. Thank you for putting in the work.

I am currently behind a fascist firewall. There doesn’t seem to be a way to instruct oniux to only try ports 80 and 443 right now. Some configuration options would be nice. :slight_smile:

2 Likes

it looks like /, the root of your filesystem, isn’t owned by root (the super-admin user). Arti, and by extension oniux, makes sure its files can only be mangled by the current user and root. Any idea why / wouldn’t be owned by root? This seems like a strange setup, likely to cause issues with more than Arti.

This can mean any number of things. It could indicate the command you tried to run couldn’t be found in your PATH. Could you run the command again with RUST_LOG=debug, to help identify at which step this error was emitted?

Sadly oniux doesn’t have any knobs to turn at this point. I’ve opened oniux#13 so that eventually it will cover your use case :slight_smile:

My setup is the usual setup of CachyOS with their graphical installer. Here is output with RUST_LOG=debug:

oniux RUST_LOG hexchat
thread 'main' panicked at src/main.rs:136:22:
called `Result::unwrap()` on an `Err` value: No such file or directory (os error 2)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'main' panicked at core/src/panicking.rs:221:5:
panic in a function that cannot unwind
stack backtrace:
   0:     0x5a1d725b836a - <std::sys::backtrace::BacktraceLock::print::DisplayBacktrace as core::fmt::Display>::fmt::h304520fd6a30aa07
   1:     0x5a1d725e3d5b - core::fmt::write::hf5713710ce10ff22
   2:     0x5a1d725b47a3 - std::io::Write::write_fmt::hda708db57927dacf
   3:     0x5a1d725b97f2 - std::panicking::default_hook::{{closure}}::he1ad87607d0c11c5
   4:     0x5a1d725b945e - std::panicking::default_hook::h81c8cd2e7c59ee33
   5:     0x5a1d725ba07f - std::panicking::rust_panic_with_hook::had2118629c312a4a
   6:     0x5a1d725b9d33 - std::panicking::begin_panic_handler::{{closure}}::h7fa5985d111bafa2
   7:     0x5a1d725b8849 - std::sys::backtrace::__rust_end_short_backtrace::h704d151dbefa09c5
   8:     0x5a1d725b99f4 - rust_begin_unwind
   9:     0x5a1d7193cfd5 - core::panicking::panic_nounwind_fmt::hc0ae93930ea8f76c
  10:     0x5a1d7193d062 - core::panicking::panic_nounwind::h9f485ff9b02bac75
  11:     0x5a1d7193d1a6 - core::panicking::panic_cannot_unwind::hea865182d7ce50af
  12:     0x5a1d724c2f36 - nix::sched::sched_linux_like::clone::callback::h7d99c497d382c0d3
  13:     0x72b11dd38684 - clone
  14:                0x0 - <unknown>
thread caused non-unwinding panic. aborting.

~
❯ RUST_LOG=debug oniux hexchat
[2025-05-20T18:46:58Z DEBUG oniux::mount] mounted `/` with `MsFlags::MS_PRIVATE`
[2025-05-20T18:46:58Z DEBUG oniux::mount] mounted `procfs` at `"/proc"`
[2025-05-20T18:46:58Z DEBUG oniux] finished mount namespace setup
[2025-05-20T18:46:58Z DEBUG oniux::user] setgroups false
[2025-05-20T18:46:58Z DEBUG oniux::user] mapped UID 1000 to 1000
[2025-05-20T18:46:58Z DEBUG oniux::user] mapped GID 1000 to 1000
[2025-05-20T18:46:58Z DEBUG oniux] finished user namespace mappings
[2025-05-20T18:46:58Z DEBUG oniux] created temporary resolv.conf(5) at "/tmp/.tmpnEh3yZ"
[2025-05-20T18:46:58Z DEBUG oniux::mount] created bind mount "/tmp/.tmpnEh3yZ" -> "/etc/resolv.conf"
[2025-05-20T18:46:58Z DEBUG oniux] mounted "/tmp/.tmpnEh3yZ" to /etc/resolv.conf
[2025-05-20T18:46:58Z DEBUG oniux::netlink] created netlink socket to find onion0
[2025-05-20T18:46:58Z WARN  netlink_packet_route::link::buffer_tool] Specified IFLA_INET6_ICMP6STATS NLA attribute holds more(most likely new kernel) data which is unknown to netlink-packet-route crate, expecting 48, got 56
[2025-05-20T18:46:58Z DEBUG oniux::netlink] created socket for adding an IP address to 2
[2025-05-20T18:46:58Z DEBUG oniux::netlink] added IP to 2
[2025-05-20T18:46:58Z DEBUG oniux::netlink] created socket for adding an IP address to 2
[2025-05-20T18:46:58Z DEBUG oniux::netlink] added IP to 2
[2025-05-20T18:46:58Z DEBUG oniux::netlink] created netlink socket to set 2 UP
[2025-05-20T18:46:58Z DEBUG oniux::netlink] setted interface 2 to UP
[2025-05-20T18:46:58Z DEBUG oniux::netlink] created socket for adding default gateway for Inet
[2025-05-20T18:46:58Z DEBUG oniux::netlink] added default gateway Inet
[2025-05-20T18:46:58Z DEBUG oniux::netlink] created socket for adding default gateway for Inet6
[2025-05-20T18:46:58Z DEBUG oniux::netlink] added default gateway Inet6
[2025-05-20T18:46:58Z DEBUG oniux] finished setting up networking
[2025-05-20T18:46:58Z DEBUG oniux] dropped all capabilites
[2025-05-20T18:46:58Z DEBUG oniux] sent TUN device
[2025-05-20T18:46:58Z DEBUG oniux] received TUN file descriptor
[2025-05-20T18:46:58Z DEBUG oniux] spawned onion-tunnel thread
thread 'main' panicked at src/main.rs:136:22:
called `Result::unwrap()` on an `Err` value: No such file or directory (os error 2)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread 'main' panicked at core/src/panicking.rs:221:5:
panic in a function that cannot unwind
stack backtrace:
   0:     0x55e56cf6e36a - <std::sys::backtrace::BacktraceLock::print::DisplayBacktrace as core::fmt::Display>::fmt::h304520fd6a30aa07
   1:     0x55e56cf99d5b - core::fmt::write::hf5713710ce10ff22
   2:     0x55e56cf6a7a3 - std::io::Write::write_fmt::hda708db57927dacf
   3:     0x55e56cf6f7f2 - std::panicking::default_hook::{{closure}}::he1ad87607d0c11c5
   4:     0x55e56cf6f45e - std::panicking::default_hook::h81c8cd2e7c59ee33
   5:     0x55e56cf7007f - std::panicking::rust_panic_with_hook::had2118629c312a4a
   6:     0x55e56cf6fd33 - std::panicking::begin_panic_handler::{{closure}}::h7fa5985d111bafa2
   7:     0x55e56cf6e849 - std::sys::backtrace::__rust_end_short_backtrace::h704d151dbefa09c5
   8:     0x55e56cf6f9f4 - rust_begin_unwind
   9:     0x55e56c2f2fd5 - core::panicking::panic_nounwind_fmt::hc0ae93930ea8f76c
  10:     0x55e56c2f3062 - core::panicking::panic_nounwind::h9f485ff9b02bac75
  11:     0x55e56c2f31a6 - core::panicking::panic_cannot_unwind::hea865182d7ce50af
  12:     0x55e56ce78f36 - nix::sched::sched_linux_like::clone::callback::h7d99c497d382c0d3
  13:     0x7129e1d38684 - clone
  14:                0x0 - <unknown>
thread caused non-unwinding panic. aborting.

All commands except hexchat work fine.

have you installed hexchat? i tried installing CachyOS, and i get the same error before install hexchat, but it works fine after a quick sudo pacman -S hexchat

That was it! thank you!

1 Like

Everyone should please be aware that Hexchat is no longer in development by the original guy/gal, AFAIK it is unmaintained at the moment.

gives

curl: (6) Not resolving .onion address (RFC 7686)

here

1 Like

Didn’t work for me on Ubuntu 25.04, first of all the guide says “Once that is done you are ready to start using it!” but I had to look for it, it’s somewhere in the release folder, but once found I attempt to use the curl test in the guide:

Does it need root access?

And why does it mount something???

Works nicely under Windows 11 / WSL2.

oniux leverage unprivileged user namespaces to setup namespaces without the need for root access. Sadly that feature is disabled on recent Ubuntu. Supporting environment without unprivileged user namespaces is still work in progress.

This is a common pattern amongst container and container-adjacent technologies to hide/modify a file as perceived from inside without actually modifying (or even having the permission to modify) that file from the outside. In oniux’s case, the specific paths that are shadowed that way are /etc/resolv.conf and the procfs. You can see that by running something like diff <(mount | sort) <(oniux mount | sort) (assuming oniux works for you).