Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@jamesmcm
Copy link
Owner

This commit introduces a major new feature: a client-daemon architecture that allows vopono to be run by unprivileged users without requiring sudo for every command. See issue #298.

Previously, every vopono exec invocation required root privileges to manipulate network namespaces, leading to frequent sudo password prompts. This was inconvenient for interactive use and a significant barrier for scripting or system integrations (e.g., desktop shortcuts).

This change addresses the problem by splitting vopono into two main components:

  1. A persistent, root-level daemon that manages namespaces.
  2. A lightweight client that communicates with the daemon.

How It Works: The Client-Daemon Flow

  1. Daemon Startup: A new vopono daemon subcommand is added. This is intended to be run as root, typically via a systemd service. The daemon creates and listens on a Unix domain socket at /run/vopono.sock.

  2. Client Execution: When a non-root user runs vopono exec ..., the client first attempts to connect to this socket.

    • If the daemon is not running, it gracefully falls back to the original behavior, using sudo to elevate privileges.
    • If the daemon is running, the client proceeds with the new IPC-based workflow.
  3. Inter-Process Communication (IPC):

    • The client serializes its ExecCommand arguments into a binary format (using bincode) and sends it to the daemon.
    • Crucially, it then passes its own stdin (0), stdout (1), and stderr (2) file descriptors to the daemon using the SCM_RIGHTS control message over the socket. This is a kernel-level mechanism for securely transferring open file handles between processes.
  4. Daemon-Side Execution:

    • The daemon accepts the connection and uses SO_PEERCRED to securely verify the connecting user's UID and GID. This is essential for security.
    • It receives the command and the file descriptors.
    • It performs all the required privileged operations: creating the network namespace, setting up veth pairs, configuring routing and firewall rules, and running the VPN protocol.
  5. Spawning the Child Process:

    • The daemon spawns the user's requested application using a pre_exec hook, which allows for fine-grained control before the new program starts.
    • Inside pre_exec, it uses setns() to move the child into the correct network namespace and unshare(CLONE_NEWNS) to give it a private mount namespace. This allows for safely bind-mounting files like /etc/resolv.conf without affecting the host.
    • It then drops privileges by setting the child's UID and GID to match those of the original client user.
    • Finally, it connects the child's stdin, stdout, and stderr to the file descriptors it received from the client.

This design ensures that I/O flows directly between the sandboxed application and the user's terminal, with the daemon acting only as a privileged setup coordinator.

Handling Interactive Sessions and Signals

A significant challenge is correctly handling interactive applications (like shells or text editors) that require a controlling terminal (TTY).

  • PTY (Pseudoterminal) Creation: If the daemon detects that the
    client's stdin is a TTY, it creates a PTY pair. The child process's
    stdio is connected to the PTY slave, making the application believe it's
    running in a real terminal.

  • I/O and Signal Forwarding: The daemon bridges I/O between the client's
    real terminal FDs and the PTY master. When the client traps a signal
    (e.g., SIGINT from Ctrl+C), it sends a message to the daemon, which
    then injects the corresponding control character into the PTY. This
    ensures that signals are delivered correctly to the application inside
    the namespace, preserving job control and interactive behavior.

When the child process exits, the daemon sends the final exit code back to the client, which then exits with the same code, seamlessly restoring control to the user's original shell.

This commit introduces a major new feature: a client-daemon architecture
that allows `vopono` to be run by unprivileged users without requiring
`sudo` for every command.

Previously, every `vopono exec` invocation required root privileges to
manipulate network namespaces, leading to frequent `sudo` password prompts.
This was inconvenient for interactive use and a significant barrier for
scripting or system integrations (e.g., desktop shortcuts).

This change addresses the problem by splitting `vopono` into two main
components:
1.  A persistent, root-level daemon that manages namespaces.
2.  A lightweight client that communicates with the daemon.

How It Works: The Client-Daemon Flow

1.  **Daemon Startup**: A new `vopono daemon` subcommand is added. This
    is intended to be run as root, typically via a systemd service. The
    daemon creates and listens on a Unix domain socket at `/run/vopono.sock`.

2.  **Client Execution**: When a non-root user runs `vopono exec ...`,
    the client first attempts to connect to this socket.
    - If the daemon is not running, it gracefully falls back to the
      original behavior, using `sudo` to elevate privileges.
    - If the daemon is running, the client proceeds with the new IPC-based
      workflow.

3.  **Inter-Process Communication (IPC)**:
    - The client serializes its `ExecCommand` arguments into a binary
      format (using `bincode`) and sends it to the daemon.
    - Crucially, it then passes its own `stdin` (0), `stdout` (1), and
      `stderr` (2) file descriptors to the daemon using the `SCM_RIGHTS`
      control message over the socket. This is a kernel-level mechanism
      for securely transferring open file handles between processes.

4.  **Daemon-Side Execution**:
    - The daemon accepts the connection and uses `SO_PEERCRED` to securely
      verify the connecting user's UID and GID. This is essential for
      security.
    - It receives the command and the file descriptors.
    - It performs all the required privileged operations: creating the
      network namespace, setting up veth pairs, configuring routing and
      firewall rules, and running the VPN protocol.

5.  **Spawning the Child Process**:
    - The daemon spawns the user's requested application using a `pre_exec`
      hook, which allows for fine-grained control before the new program
      starts.
    - Inside `pre_exec`, it uses `setns()` to move the child into the
      correct network namespace and `unshare(CLONE_NEWNS)` to give it a
      private mount namespace. This allows for safely bind-mounting files
      like `/etc/resolv.conf` without affecting the host.
    - It then drops privileges by setting the child's UID and GID to
      match those of the original client user.
    - Finally, it connects the child's `stdin`, `stdout`, and `stderr` to
      the file descriptors it received from the client.

This design ensures that I/O flows directly between the sandboxed
application and the user's terminal, with the daemon acting only as a
privileged setup coordinator.

Handling Interactive Sessions and Signals

A significant challenge is correctly handling interactive applications
(like shells or text editors) that require a controlling terminal (TTY).

-   **PTY (Pseudoterminal) Creation**: If the daemon detects that the
    client's `stdin` is a TTY, it creates a PTY pair. The child process's
    stdio is connected to the PTY slave, making the application believe it's
    running in a real terminal.

-   **I/O and Signal Forwarding**: The daemon bridges I/O between the client's
    real terminal FDs and the PTY master. When the client traps a signal
    (e.g., `SIGINT` from `Ctrl+C`), it sends a message to the daemon, which
    then injects the corresponding control character into the PTY. This
    ensures that signals are delivered correctly to the application inside
    the namespace, preserving job control and interactive behavior.

When the child process exits, the daemon sends the final exit code back
to the client, which then exits with the same code, seamlessly restoring
control to the user's original shell.
@jamesmcm jamesmcm merged commit ee645eb into master Sep 13, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants