The journey of making Rustyproxy work on NixOS

The journey of making Rustyproxy work on NixOS

A few years ago i decided to get rid of Burpsuite because of its ram consumption and almost stale development (That last point is not true anymore). I decided i would learn rust by making my own burplike. At that time i was on Archlinux so everything was fine, but a few months ago i decided to give a go to NixOS.

Now the proxy i am working with is a rust project with two parts, a server and a GUI. There are two problems we need to fix when try to compile for NixOS. The server needs to compile OpenSSL and the GUI needs Wayland/Xorg and Openssl too !

In this document, we'll focus on the GUI:


$ nix-shell -p cargo
$ cargo --version
cargo 1.77.1 # UHHHH
$ cargo run
[...]
    Finished dev [unoptimized + debuginfo] target(s) in 0.35s
     Running `/home/vaelio/repos/RustyProxy/target/debug/rustyproxy`
Err(WinitEventLoop(Os(OsError { line: 80, file: "/home/vaelio/.cargo/registry/src/index.crates.io-6f17d22bba15001f/winit-0.29.15/src/platform_impl/linux/wayland/event_loop/mod.rs", error: WaylandError(Connection(NoWaylandLib)) })))

# UHHHH

Again, two problems:
- cargo version is fairly old
- apparently the binary produced by cargo can't find wayland dependencies at runtime


Using nix develop

After researching a bit on google, we find that for both issues we should use a nix flake. Wth is that ? Basically its a script written in nix programming language, that is supposed to be used when compiling / packaging progams.
Usually, you'll find something like this:


{
  description = "rust devshell for rustyproxy";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    rust-overlay.url = "github:oxalica/rust-overlay";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        overlays = [ (import rust-overlay) ];
        pkgs = import nixpkgs {
          inherit system overlays;
        };
      in
      {
        devShells.default = with pkgs; mkShell {
          buildInputs = [
            openssl
            pkg-config
            rust-bin.stable.latest.default
            python3
            libGL
            libGLU
            libxkbcommon
          ];

          LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${ with pkgs; lib.makeLibraryPath [
                  wayland
                  libxkbcommon
                  fontconfig
                  libGL
                  libGLU
          ] }";
        };
      }
   );
}

Technically, this works. You put that in a flake.nix file, and you run nix develop, dependencies are pulled, the latest version of rust is pulled too, and a shell spawn with a patched LD_LIBRARY_PATH variable so that the program works.

In the long run however, i got tilted that i needed to be in the developper shell to be able to run it. I was thinking there must be a solution to run it with the appropriate dependencies without having to spawn a dev shell everytime!


Using nix run

This is what i ended up with (we'll go through everything). With this kind of of flake, you can do nix run .#rustyproxy for the GUI and nix run .#rustyproxy-srv for the server :).


{
  inputs = {
    fenix.url = "github:nix-community/fenix";
    naersk.url = "github:nix-community/naersk/master";
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, fenix, nixpkgs, utils, naersk }:
    utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { inherit system; };

        toolchain = fenix.packages.${system}.stable.rust;

        naersk' = naersk.lib.${system}.override {
          cargo = toolchain;
          rustc = toolchain;
        };
        libPath = with pkgs; lib.makeLibraryPath [
          libGL
          libxkbcommon
          wayland
          xorg.libX11
          xorg.libXcursor
          xorg.libXi
          xorg.libXrandr
        ];

      in
      {
        packages = {
          rustyproxy = naersk'.buildPackage {
            src = ./.;
            pname = "rustyproxy";
            nativeBuildInputs = [ pkgs.autoPatchelfHook pkgs.makeWrapper ];
            buildInputs = [ pkgs.perl pkgs.pkg-config pkgs.libGL pkgs.libGLU pkgs.libxkbcommon pkgs.wayland ];
            overrideMain = old: {
              preConfigure = ''
                cargo_build_options="$cargo_build_options --bin rustyproxy"
              '';
            };
            postInstall = ''
              wrapProgram "$out/bin/rustyproxy" --prefix LD_LIBRARY_PATH : "${libPath}"
            '';
          };
          rustyproxy-srv = naersk'.buildPackage {
            src = ./.;
            pname = "rustyproxy-srv";
            buildInputs = [ pkgs.perl pkgs.pkg-config pkgs.libGL pkgs.libGLU pkgs.libxkbcommon ];
            nativeBuildInputs = [ pkgs.autoPatchelfHook pkgs.makeWrapper];
            overrideMain = old: {
              preConfigure = ''
                cargo_build_options="$cargo_build_options --bin rustyproxy-srv"
              '';
            };
          };
        };
        defaultPackage = self.packages.${system}.rustyproxy;
        devShell = with pkgs; mkShell {
          buildInputs = [ cargo rustc rustfmt pre-commit rustPackages.clippy pkg-config libGL libGLU libxkbcommon zenity ];
          RUST_SRC_PATH = rustPlatform.rustLibSrc;
          LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${ with pkgs; lib.makeLibraryPath [ wayland libxkbcommon fontconfig libGL libGLU] }";
        };
      }
    );
}


The inputs

inputs = {
    fenix.url = "github:nix-community/fenix";
    naersk.url = "github:nix-community/naersk/master";
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    utils.url = "github:numtide/flake-utils";
  };

This part is fairly straight forward but i'll explain the two first:
- fenix is used to change the version of rust compiler
- naersk is used to change the way we build the programs (i think you can do it without that, but its what i got)
The two last inputs are pretty common inputs

The outputs


outputs = { self, fenix, nixpkgs, utils, naersk }:
    utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { inherit system; };

        toolchain = fenix.packages.${system}.stable.rust;

        naersk' = naersk.lib.${system}.override {
          cargo = toolchain;
          rustc = toolchain;
        };
        libPath = with pkgs; lib.makeLibraryPath [
          libGL
          libxkbcommon
          wayland
          xorg.libX11
          xorg.libXcursor
          xorg.libXi
          xorg.libXrandr
        ];

      in
      {
        packages = {
          rustyproxy = naersk'.buildPackage {
            src = ./.;
            pname = "rustyproxy";
            nativeBuildInputs = [ pkgs.autoPatchelfHook pkgs.makeWrapper ];
            buildInputs = [ pkgs.perl pkgs.pkg-config pkgs.libGL pkgs.libGLU pkgs.libxkbcommon pkgs.wayland ];
            overrideMain = old: {
              preConfigure = ''
                cargo_build_options="$cargo_build_options --bin rustyproxy"
              '';
            };
            postInstall = ''
              wrapProgram "$out/bin/rustyproxy" --prefix LD_LIBRARY_PATH : "${libPath}"
            '';
          };
          rustyproxy-srv = naersk'.buildPackage {
            src = ./.;
            pname = "rustyproxy-srv";
            buildInputs = [ pkgs.perl pkgs.pkg-config pkgs.libGL pkgs.libGLU pkgs.libxkbcommon ];
            nativeBuildInputs = [ pkgs.autoPatchelfHook pkgs.makeWrapper];
            overrideMain = old: {
              preConfigure = ''
                cargo_build_options="$cargo_build_options --bin rustyproxy-srv"
              '';
            };
          };
        };
        defaultPackage = self.packages.${system}.rustyproxy;
        devShell = with pkgs; mkShell {
          buildInputs = [ cargo rustc rustfmt pre-commit rustPackages.clippy pkg-config libGL libGLU libxkbcommon zenity ];
          RUST_SRC_PATH = rustPlatform.rustLibSrc;
          LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${ with pkgs; lib.makeLibraryPath [ wayland libxkbcommon fontconfig libGL libGLU] }";
        };
      }
    );

There are a few variables interesting in there:
- The toolchain variable is set and allows us to choose which rust toolchain to use (Note the system variable, this should allow for cross compiling, although i haven't tried that)
- The naersk' variable is the customized naersk build system
- The libPath variable contains all the path to the libs needed

We also define two packages:
- rustyproxy (The GUI)
- rustyproxy-srv (I wont explain this one because its simpler than the GUI)


The package definition


rustyproxy = naersk'.buildPackage {
            src = ./.;
            pname = "rustyproxy";
            nativeBuildInputs = [ pkgs.autoPatchelfHook pkgs.makeWrapper ];
            buildInputs = [ pkgs.perl pkgs.pkg-config pkgs.libGL pkgs.libGLU pkgs.libxkbcommon pkgs.wayland ];
            overrideMain = old: {
              preConfigure = ''
                cargo_build_options="$cargo_build_options --bin rustyproxy"
              '';
            };
            postInstall = ''
              wrapProgram "$out/bin/rustyproxy" --prefix LD_LIBRARY_PATH : "${libPath}"
            '';
          };
          

This part has changed quite a few from the usual stuff:
- src variable is used to define the root directory of the program (Here its ./. because in the cargo settings i have one project with two binaries, instead of two projects).
- pname is the program name (needs to match the name of the executable file)
- nativeBuildInputs is containing the libs needed to be able to compile the program (we could have put cargo here for exemple), we have pkgs.autoPatchelfHook which i included but is really not useful for us i believe but is responsible for patching the executable after compilation in order to fix some dependency issues. And we also have pkgs.makeWrapper that allows triggering some actions before and after compilation.
- buildInputs is containing all the necessary lib for the compilation of the program
- overrideMain This part is not necessary but i had to add it because otherwise nix was building the two packages every time i wanted to build one, which is obviously time and disk space consuming. This is because of my projects structure and most likely is going to be useless for you.
- postInstall This is the special thing. This allows patching LD_LIBRARY_PATH correctly once the program is compiled and stored in the nix store


Conclusion

I am a big noob in Nix and not the smartest, so this isn't the power user experience but oh boy it was hard to gather informations. Nix is great though, but the docs needs work (Removing deprecated options and give more complete and practical exemple).