Build package/module for closed-source binary package Pulse Secure VPN

I’ve got to switch from OpenVPN to Pulse Secure VPN due to job restrictions. There are deb and rpm packages available for Pulse Secure, and I’ve spent several days trying to create a Nix package+module, but with no success, mainly due to multiple binaries and scripts with hard-coded paths.

I suspect that this wouldn’t be too difficult for a seasoned Nix expert, but it’s the first thing that I’ve not been able to handle after a bit more than a year of Nix usage. I’d continue to try but because I need this for work and it’s time sensitive, so I’m willing to pay. I would also like to release the work in the open to be upstreamed.

Please get in touch if you are willing to help.

2 Likes

I would give it a try, do you have a link to the sources? Installing Pulse Secure Client for Linux couldn’t find anything about a download on the install page :confused:

You can also ping me on matrix if you want to: @janik0:matrix.org

1 Like

Had to use this also in my previous work, and it’s a pain. Never worked correctly.

I was given some deb packages.

One of the main issue is that the binary want’s to read files in /opt and that seems completely hardcoded. So I thought about using buildFHSUserEnv but wasn’t able to make it work.

So I’ve been shamelessly linking stuff in /opt…

This is the closest I got:

{ pkgs
, lib
, ...
}:

let

  pulsesecure = pkgs.stdenv.mkDerivation rec {
    pname = "pulsesecure";
    version = "9.1r12.0";

    src = ./ps-pulse-linux-9.1r12.0-b10247-64-bit-installer.deb;

    dontBuild = true;
    dontConfigure = true;

    unpackPhase = ''
      ${pkgs.dpkg}/bin/dpkg-deb -x $src .
      '';

    nativeBuildInputs = [
      pkgs.autoPatchelfHook
      pkgs.glib
      pkgs.libbsd
      pkgs.cairo
      pkgs.atk
      pkgs.gdk-pixbuf
      pkgs.gtkmm3
      pkgs.gnome3.webkitgtk
      pkgs.curl
    ];

    installPhase = ''
      mkdir -p $out/{opt,bin}
      cp -r opt/pulsesecure $out/opt
      cp opt/pulsesecure/bin/pulsesecure $out/bin
      cp opt/pulsesecure/bin/pulseUI $out/bin

      mkdir -p $out/share/dbus-1/system.d
      mv opt/pulsesecure/lib/JUNS/net.psecure.pulse.conf $out/share/dbus-1/system.d/net.psecure.pulse.conf

      addAutoPatchelfSearchPath $out/opt/pulsesecure/lib
      autoPatchelf opt/pulsesecure/bin/*
      '';
  };

  /* pulsesecureFHS = pkgs.buildFHSUserEnv { */
  /*   name = "pulseFHS"; */

  /*   targetPkgs = pkgs: [ */ 
  /*     pulsesecure */
  /*     (pkgs.runCommand "certs" {} */
  /*       '' */
  /*       mkdir -p $out/etc/pki/tls/ */
  /*       ln -s ${pkgs.cacert}/etc/ssl/certs $out/etc/pki/tls/certs */
  /*       '') */
  /*   ]; */
  /*   extraBuildCommands = '' */
  /*     ln -s ${pulsesecure}/opt */
  /*     ''; */
  /*   /1* runScript = "pulseUI"; *1/ */
  /* }; */

in {

  environment.systemPackages = [
    pulsesecure
  ];

  systemd.tmpfiles.rules = [ "d /var/lib/pulsesecure/pulse 0700 - - -" ];

  services.dbus.packages = [ pulsesecure ];

  systemd.services.pulsesecure = {
    description = "Pulsesecure VPN";
    wantedBy    = [ "multi-user.target" ];
    # Should use buildFHSUserEnv but pulsesecure is forking and so escape the chroot
    # Also there is dbus issues
    preStart = ''
      ln -s ${pkgs.iproute2}/bin/ip /sbin/ip
      ln -s ${pulsesecure}/opt /opt
      '';
    postStop = ''
      rm -f /sbin/ip
      rm -f /opt
      '';
    serviceConfig = {
      Type = "forking";
      ExecStart = "${pulsesecure}/bin/pulsesecure";
    };
  };

}

Good luck!

That’s basically the closest I got as well when trying to build it this morning.

Thanks everyone for your responses. This is as far as I got. It almost works. The only problem is that it can’t pop up the embedded browser. I tried wrapping the browser manually here, but it seems to detect it and think that it’s not installed. If I don’t wrap it, it tries to launch it but the logs show a parsing error. Here’s the code:

{ pkgs, ... }:
let
  version = "9.1r15.0-b15819";
  runtimeDeps = [
    pkgs.atk
    pkgs.atkmm
    pkgs.cairo
    pkgs.cairomm
    pkgs.curl
    pkgs.dbus
    pkgs.gdk-pixbuf
    pkgs.glib
    pkgs.glibmm
    pkgs.gtk3
    pkgs.gtkmm3
    pkgs.libbsd
    pkgs.libcef
    pkgs.librsvg
    pkgs.libsigcxx
    pkgs.libsoup
    pkgs.libuuid
    pkgs.nss
    pkgs.openssl
    pkgs.pango
    pkgs.pangomm
    pkgs.webkitgtk
    pkgs.xorg.libX11
  ];
in
  let pulsesecure-pkg = pkgs.stdenv.mkDerivation {
    pname = "pulsesecure-pkg";
    version = version;

    srcs = [
      ./binaries/ps-pulse-linux-9.1r15.0-b15819-64bit-installer.deb
      (pkgs.fetchurl {
        url = "https://pcstp.pulsesecure.net/cef/linux/cef64.94.tar.bz2";
        sha256 = "1qy65psav9mf6anx3fkiyzg07wax563nxpjskcy2ilw3ifwda4h6";
      })
    ];

    dontUnpack = true;

    nativeBuildInputs = [
      pkgs.autoPatchelfHook
    ];

    preBuild = ''
      LIB_PREFIX=$out/opt/pulsesecure
      addAutoPatchelfSearchPath $LIB_PREFIX/bin
      addAutoPatchelfSearchPath $LIB_PREFIX/lib/ConnectionManager
      addAutoPatchelfSearchPath $LIB_PREFIX/lib/ConnectionStore
      addAutoPatchelfSearchPath $LIB_PREFIX/lib/dispatch
      addAutoPatchelfSearchPath $LIB_PREFIX/lib/dsOpenSSL
      addAutoPatchelfSearchPath $LIB_PREFIX/lib/eapService
      addAutoPatchelfSearchPath $LIB_PREFIX/lib/iveConnectionMethod
      addAutoPatchelfSearchPath $LIB_PREFIX/lib/JamUI
      addAutoPatchelfSearchPath $LIB_PREFIX/lib/JUNS
      addAutoPatchelfSearchPath $LIB_PREFIX/lib/TnccPlugin
      addAutoPatchelfSearchPath $LIB_PREFIX/lib/TunnelManager
      addAutoPatchelfSearchPath $LIB_PREFIX/resource
      addAutoPatchelfSearchPath /

      addAutoPatchelfSearchPath $LIB_PREFIX/lib/cefRuntime/Release
      addAutoPatchelfSearchPath $LIB_PREFIX/lib/cefRuntime/Resources
    '';

    buildInputs = [
      pkgs.bzip2
      pkgs.dpkg
      pkgs.makeWrapper
      pkgs.unzip
      # The following are used by autoPatchelfHook
    ] ++ runtimeDeps;

    installPhase = ''
      for index in "''${!srcs[@]}"; do
        if [ $index == 0 ]; then
          packages=(''${srcs[$index]})
          echo $packages
          for package_index in "''${!packages[@]}"; do
            if [ $package_index == 0 ]; then
              deb=''${packages[$package_index]}
            elif [ $package_index == 1 ]; then
              cefRuntime=''${packages[$package_index]}
            fi
          done
        fi
      done

      dpkg-deb -x $deb $out;
      chmod 755 "$out"
      cd $out/opt/pulsesecure/lib
      tar xvjf $cefRuntime
      cefPath=cef_binary_94.4.8+g5b52963+chromium-94.0.4606.71_linux64_minimal
      mkdir $out/opt/pulsesecure/lib/cefRuntime
      mv $cefPath/Release $out/opt/pulsesecure/lib/cefRuntime
      mv $cefPath/Resources $out/opt/pulsesecure/lib/cefRuntime
      cd $out/..
      mkdir -p $out/opt/pulsesecure/bin-unwrapped
      mv $out/opt/pulsesecure/bin/cefBrowser $out/opt/pulsesecure/bin-unwrapped
      mv $out/opt/pulsesecure/bin/cefSubProcess $out/opt/pulsesecure/bin-unwrapped

      LIB_PREFIX="$out/opt/pulsesecure"
      LIBPATH="$LIB_PREFIX/lib/ConnectionManager:$LIB_PREFIX/lib/ConnectionStore:$LIB_PREFIX/lib/dispatch:$LIB_PREFIX/lib/dsOpenSSL:$LIB_PREFIX/lib/eapService:$LIB_PREFIX/lib/iveConnectionMethod:$LIB_PREFIX/lib/JamUI:$LIB_PREFIX/lib/JUNS:$LIB_PREFIX/lib/TnccPlugin:$LIB_PREFIX/lib/TunnelManager:$LIB_PREFIX/bin";
      NIX_REDIRECTS="/opt/pulsesecure/lib/JUNS/libdsAccessServicePS.so=$out/opt/pulsesecure/lib/JUNS/libdsAccessServicePS.so:/opt/pulsesecure/resource=$out/opt/pulsesecure/resource:/opt/pulsesecure/lib/JUNS/access.ini=$out/opt/pulsesecure/lib/JUNS/access.ini:/opt/pulsesecure/lib/JamUI/MessageCatalogPulseUI_EN.txt=$out/opt/pulsesecure/lib/JamUI/MessageCatalogPulseUI_EN.txt:/opt/pulsesecure/lib/JUNS/MessageCatalogCommon_EN.txt=$out/opt/pulsesecure/lib/JUNS/MessageCatalogCommon_EN.txt:/opt/pulsesecure/lib/eapService/MessageCatalogEapAM_EN.txt=$out/opt/pulsesecure/lib/eapService/MessageCatalogEapAM_EN.txt:/opt/pulsesecure/lib/TnccPlugin/MessageCatalogTncc_EN.txt=$out/opt/pulsesecure/lib/TnccPlugin/MessageCatalogTncc_EN.txt:/opt/pulsesecure/lib/ConnectionManager/MessageCatalogConnMgr_EN.txt=$out/opt/pulsesecure/lib/ConnectionManager/MessageCatalogConnMgr_EN.txt:/opt/pulsesecure/lib/iveConnectionMethod/MessageCatalogIveAM_EN.txt=$out/opt/pulsesecure/lib/iveConnectionMethod/MessageCatalogIveAM_EN.txt"

      # See: https://nixos.wiki/wiki/Packaging/Quirks_and_Caveats
      makeWrapper "$out/opt/pulsesecure/bin-unwrapped/cefBrowser" "$out/opt/pulsesecure/bin/cefBrowser" \
        --prefix LD_LIBRARY_PATH : "$LIBPATH" \
        --set NIX_REDIRECTS "$NIX_REDIRECTS" \
        --set LD_PRELOAD "${pkgs.libredirect}/lib/libredirect.so"

      makeWrapper "$out/opt/pulsesecure/bin-unwrapped/cefSubProcess" "$out/opt/pulsesecure/bin/cefSubProcess" \
        --prefix LD_LIBRARY_PATH : "$LIBPATH" \
        --set NIX_REDIRECTS "$NIX_REDIRECTS" \
        --set LD_PRELOAD "${pkgs.libredirect}/lib/libredirect.so"

      makeWrapper "$out/opt/pulsesecure/bin/pulsesecure" "$out/bin/pulsesecure-wrapped" \
        --prefix LD_LIBRARY_PATH : "$LIBPATH" \
        --set NIX_REDIRECTS "$NIX_REDIRECTS" \
        --set LD_PRELOAD "${pkgs.libredirect}/lib/libredirect.so"

      makeWrapper "$out/opt/pulsesecure/bin/jamCommand" "$out/bin/jamCommand" \
        --prefix LD_LIBRARY_PATH : "$LIBPATH" \
        --set NIX_REDIRECTS "$NIX_REDIRECTS" \
        --set LD_PRELOAD "${pkgs.libredirect}/lib/libredirect.so"

      makeWrapper "$out/opt/pulsesecure/bin/pulseUI" "$out/bin/pulseUI" \
        --prefix LD_LIBRARY_PATH : "$LIBPATH" \
        --set NIX_REDIRECTS "$NIX_REDIRECTS" \
        --set LD_PRELOAD "${pkgs.libredirect}/lib/libredirect.so"

      substituteInPlace \
        $out/opt/pulsesecure/bin/startup.sh \
        --replace /opt/pulsesecure/bin/pulsesecure $out/bin/pulsesecure-wrapped

      substituteInPlace \
        $out/lib/systemd/system/pulsesecure.service \
        --replace /opt/pulsesecure/bin $out/opt/pulsesecure/bin

      substituteInPlace \
        $out/opt/pulsesecure/bin/startup.sh \
        --replace /usr/bin/pgrep pgrep

      substituteInPlace \
        $out/opt/pulsesecure/bin/startup.sh \
        --replace pgrep ${pkgs.procps}/bin/pgrep

      substituteInPlace \
        $out/opt/pulsesecure/bin/startup.sh \
        --replace logger ${pkgs.inetutils}/bin/logger

      substituteInPlace \
        $out/opt/pulsesecure/bin/setup_cef.sh \
        --replace /usr/bin/curl ${pkgs.curl}/bin/curl

      substituteInPlace \
        $out/opt/pulsesecure/bin/setup_cef.sh \
        --replace /usr/bin/wget ${pkgs.wget}/bin/wget

      # This doesn't seem to work in the postInstall step
      # It doesn't see $out.  Why do the other postInstall steps work though?? @TODO
      mkdir -p $out/share/dbus-1/system.d $out/share/dbus-1/system-services $out/etc/systemd/system
      install -Dm644 $out/opt/pulsesecure/lib/JUNS/net.psecure.pulse.conf $out/share/dbus-1/system.d/net.psecure.pulse.conf

      ## This seems to already exist.  Generated by dbus service below?
      # cp -v $out/lib/systemd/system/pulsesecure.service $out/etc/systemd/system

      cat <<END > $out/share/dbus-1/system-services/net.psecure.pulse.service
      [D-BUS Service]
      Name=net.psecure.pulse
      Exec=$out/opt/pulsesecure/bin/startup.sh start
      User=root
      SystemdService=pulsesecure.service
      END
    '';
  };
in
  let pulsesecure = pkgs.symlinkJoin {
    name = "pulsesecure";
    paths = [
      pulsesecure-pkg
    ];
  };
in
{
  environment.systemPackages = [ pulsesecure ];
  services.dbus.packages = [ pulsesecure ];
  systemd.packages = [ pulsesecure ];

  # These must be individual so that the /opt/pulsesecure/lib folder remains writable
  systemd.tmpfiles.rules = [
    # Despite using patchelf, LD_LIBRARY_PATH, and NIX_REDIRECTS/LD_PRELOAD/libredirect, it seems
    # that some binaries insist on looking in /opt for libraries.
    "L+ /opt/pulsesecure/bin - - - - ${pulsesecure}/opt/pulsesecure/bin"
    "L+ /opt/pulsesecure/resource - - - - ${pulsesecure}/opt/pulsesecure/resource"
    "L+ /opt/pulsesecure/lib/ConnectionManager - - - - ${pulsesecure}/opt/pulsesecure/lib/ConnectionManager"
    "L+ /opt/pulsesecure/lib/ConnectionStore - - - - ${pulsesecure}/opt/pulsesecure/lib/ConnectionStore"
    "L+ /opt/pulsesecure/lib/dispatch - - - - ${pulsesecure}/opt/pulsesecure/lib/dispatch"
    "L+ /opt/pulsesecure/lib/dsOpenSSL - - - - ${pulsesecure}/opt/pulsesecure/lib/dsOpenSSL"
    "L+ /opt/pulsesecure/lib/eapService - - - - ${pulsesecure}/opt/pulsesecure/lib/eapService"
    "L+ /opt/pulsesecure/lib/iveConnectionMethod - - - - ${pulsesecure}/opt/pulsesecure/lib/iveConnectionMethod"
    "L+ /opt/pulsesecure/lib/JamUI - - - - ${pulsesecure}/opt/pulsesecure/lib/JamUI"
    "L+ /opt/pulsesecure/lib/JUNS - - - - ${pulsesecure}/opt/pulsesecure/lib/JUNS"
    "L+ /opt/pulsesecure/lib/TnccPlugin - - - - ${pulsesecure}/opt/pulsesecure/lib/TnccPlugin"
    "L+ /opt/pulsesecure/lib/TunnelManager - - - - ${pulsesecure}/opt/pulsesecure/lib/TunnelManager"

    "L+ /opt/pulsesecure/lib/cefRuntime - - - - ${pulsesecure}/opt/pulsesecure/lib/cefRuntime"

    "L+ /etc/pki/ca-trust/extracted/openssl/ca-bundle.trust.crt - - - - /etc/ssl/certs/ca-certificates.crt"
    "d /var/lib/pulsesecure/pulse 1755 root root"
  ];
}

I stumbled upon this thread after being forced to use Pulse Secure VPN for work. Taking inspiration from this and a whole lot of trial and error, I got the following overlay & systemd unit to sorta work and realised that it’s quite possibly a futile exercise

## Overlay at machines/framework/overlays/pulsesecure/default.nix
{
  config,
  pkgs,
  lib,
  ...
}: {
  nixpkgs.overlays = [
    (final: prev: {
      pulsesecure = pkgs.stdenv.mkDerivation rec {
        pname = "pulsesecure";
        version = "9.1r13";

        srcs = [
          ./ps-pulse-ubuntu-debian.deb
          (pkgs.fetchurl {
            url = "https://pcstp.pulsesecure.net/cef/linux/cef64.94.tar.bz2";
            sha256 = "1qy65psav9mf6anx3fkiyzg07wax563nxpjskcy2ilw3ifwda4h6";
          })
        ];

        #sourceRoot = ".";
        dontUnpack = true;

        #unpackCmd = "dpkg-deb -x $src .";

        dontConfigure = true;
        dontBuild = true;

        buildInputs = [
          pkgs.dpkg
          pkgs.curl
          pkgs.wget
        ];

        nativeBuildInputs = [
          pkgs.atk
          pkgs.atkmm
          pkgs.autoPatchelfHook
          pkgs.bzip2
          pkgs.cairo
          pkgs.cairomm
          pkgs.curl
          pkgs.dbus
          pkgs.gdk-pixbuf
          pkgs.glib
          pkgs.glibmm
          pkgs.gtk3
          pkgs.gtkmm3
          pkgs.inetutils
          pkgs.libbsd
          pkgs.librsvg
          pkgs.libcef
          pkgs.libsigcxx
          pkgs.libsoup
          pkgs.libuuid
          pkgs.makeWrapper
          pkgs.nss
          pkgs.openssl
          pkgs.pango
          pkgs.pangomm
          pkgs.procps
          pkgs.unzip
          pkgs.webkitgtk
          pkgs.wget
          pkgs.wrapGAppsHook
          pkgs.xorg.libX11
        ];

        installPhase = ''
          for index in "''${!srcs[@]}"; do
            if [ $index == 0 ]; then
              packages=(''${srcs[$index]})
              echo $packages
              for package_index in "''${!packages[@]}"; do
                if [ $package_index == 0 ]; then
                  deb=''${packages[$package_index]}
                elif [ $package_index == 1 ]; then
                  cefRuntime=''${packages[$package_index]}
                fi
              done
            fi
          done

          dpkg-deb -x $deb $out;
          chmod 755 "$out"
          cd $out/opt/pulsesecure/lib
          tar xvjf $cefRuntime
          cefPath=cef_binary_94.4.8+g5b52963+chromium-94.0.4606.71_linux64_minimal
          mkdir $out/opt/pulsesecure/lib/cefRuntime
          mv $cefPath/Release $out/opt/pulsesecure/lib/cefRuntime
          mv $cefPath/Resources $out/opt/pulsesecure/lib/cefRuntime
          cd $out/..

          mkdir -p $out/bin
          ls -l $out/opt/pulsesecure
          cp $out/opt/pulsesecure/bin/pulsesecure $out/bin
          cp $out/opt/pulsesecure/bin/pulseUI $out/bin
          cp $out/opt/pulsesecure/bin/pulselauncher $out/bin

          mkdir -p $out/share/dbus-1/system.d
          mv $out/opt/pulsesecure/lib/JUNS/net.psecure.pulse.conf $out/share/dbus-1/system.d/net.psecure.pulse.conf


          LIB_PREFIX=$out/opt/pulsesecure
          addAutoPatchelfSearchPath $LIB_PREFIX/lib/ConnectionManager
          addAutoPatchelfSearchPath $LIB_PREFIX/lib/ConnectionStore
          addAutoPatchelfSearchPath $LIB_PREFIX/lib/dispatch
          addAutoPatchelfSearchPath $LIB_PREFIX/lib/dsOpenSSL
          addAutoPatchelfSearchPath $LIB_PREFIX/lib/eapService
          addAutoPatchelfSearchPath $LIB_PREFIX/lib/iveConnectionMethod
          addAutoPatchelfSearchPath $LIB_PREFIX/lib/JamUI
          addAutoPatchelfSearchPath $LIB_PREFIX/lib/JUNS
          addAutoPatchelfSearchPath $LIB_PREFIX/lib/TnccPlugin
          addAutoPatchelfSearchPath $LIB_PREFIX/lib/TunnelManager
          addAutoPatchelfSearchPath $LIB_PREFIX/lib/cefRuntime/Release
          addAutoPatchelfSearchPath $LIB_PREFIX/lib/cefRuntime/Resources

          autoPatchelf $out/opt/pulsesecure/bin/*

          substituteInPlace \
                $out/opt/pulsesecure/bin/setup_cef.sh \
                --replace /usr/bin/curl ${pkgs.curl}/bin/curl

          substituteInPlace \
            $out/opt/pulsesecure/bin/setup_cef.sh \
            --replace /usr/bin/wget ${pkgs.wget}/bin/wget

          substituteInPlace \
            $out/opt/pulsesecure/bin/setup_cef.sh \
            --replace cdd94ba80748e78118835321bbd5fc8be23b03e9c11cab083640c3f558a79d01 aef6c5af0499e166d1a8cc87d345425715cc0a56a52768787711438afa272424

        '';

        meta = with lib; {
          description = "Pulse secure client for linux";
          license = licenses.unfree;
          platforms = platforms.linux;
        };
      };
    })
  ];
}
### nixos-config/system/pulsesecure/default.nix
{
  config,
  pkgs,
  lib,
  ...
}: let
  pulsesecure = pkgs.symlinkJoin {
    name = "pulsesecure";
    paths = [
      pkgs.pulsesecure
    ];
  };
in
  lib.mkIf (config.networking.hostName == "myhost")
  {
    services.dbus.packages = [pkgs.pulsesecure];
    #    systemd.packages = [pkgs.pulsesecure];

    systemd.tmpfiles.rules = ["d /var/lib/pulsesecure/pulse 0700 - - -"];

    systemd.services.pulsesecure = {
      description = "Pulsesecure VPN";
      wantedBy = ["multi-user.target"];
      # Should use buildFHSUserEnv but pulsesecure is forking and so escape the chroot
      # Also there are dbus issues
      preStart = ''
        mkdir -p /sbin
        ln -s ${pkgs.iproute2}/bin/iptables /sbin/iptables
        ln -s ${pkgs.iproute2}/bin/ip /sbin/ip
        ln -s ${pulsesecure}/opt/pulsesecure /opt
      '';
      postStop = ''
        rm -rf /sbin/
        rm -rf /opt/pulsesecure
      '';
      serviceConfig = {
        Type = "forking";
        ExecStart = "${pulsesecure}/bin/pulsesecure";
      };
    };
  }

Based on logs, I first noticed that setup_cef.sh has a checksum validation. So, replaced that checksum based on the patched libcef.so. Unfortunately, it appears that this checksum is calculated independently in the app (educated guess based on the logs). See below

00263,09 2023/07/13 00:38:31.014 2 root /nix/store/a4z24kl2sd5z5nxw1695hgd7cp7bsm5n-pulsesecure-9.1r13/bin/.pulsesecure-wrapped iveConnectionMethod p2780639 t2AAEB3 connInstance.cpp:2469 - 'iveConnectionMethod' session resumption failed, cleaning up existing connections
00243,09 2023/07/13 00:38:31.120 1 user /nix/store/a4z24kl2sd5z5nxw1695hgd7cp7bsm5n-pulsesecure-9.1r13/bin/.pulseUI-wrapped pulseUI p2796014 t2AA9EE JamConnectionModel.cpp:4986 - 'JamUI' getSDPAttributeFromConnStore : Failed to get attribute value
00253,09 2023/07/13 00:38:31.421 2 user /nix/store/a4z24kl2sd5z5nxw1695hgd7cp7bsm5n-pulsesecure-9.1r13/bin/.pulseUI-wrapped pulseUI p2796014 t2AA9EE CefShaVerify.cpp:86 - 'isAppInstalled' verifySHA2(/opt/pulsesecure/lib/cefRuntime/Release/libcef.so) failed
00250,09 2023/07/13 00:38:33.396 1 root /nix/store/a4z24kl2sd5z5nxw1695hgd7cp7bsm5n-pulsesecure-9.1r13/bin/.pulsesecure-wrapped iftProvider p2780639 t2AAE96 channelProviderImplEap.cpp:427 - 'iftProvider' EAP Authentication FAILED: Error: 15 0xf State: 3 0x3
00215,09 2023/07/13 00:38:33.396 1 root /nix/store/a4z24kl2sd5z5nxw1695hgd7cp7bsm5n-pulsesecure-9.1r13/bin/.pulsesecure-wrapped iftProvider p2780639 t2AAE96 channelProviderImplEap.cpp:449 - 'iftProvider' Eap failed 15 0xf
00255,09 2023/07/13 00:38:33.419 3 root /nix/store/a4z24kl2sd5z5nxw1695hgd7cp7bsm5n-pulsesecure-9.1r13/bin/.pulsesecure-wrapped iveConnectionMethod p2780639 t2AAE96 connInstance.cpp:2537 - 'iveConnectionMethod' on_ChannelFailed - 65472e50c95e49cd94acfe29592436d1
00262,09 2023/07/13 00:38:33.445 3 root /nix/store/a4z24kl2sd5z5nxw1695hgd7cp7bsm5n-pulsesecure-9.1r13/bin/.pulsesecure-wrapped ive

Specifically, CefShaVerify.cpp:86 - ‘isAppInstalled’ verifySHA2(/opt/pulsesecure/lib/cefRuntime/Release/libcef.so) failed

strace shows that the file is definitely being checked

openat(AT_FDCWD, "/opt/pulsesecure/lib/cefRuntime/Release/libcef.so", O_RDONLY) = 27

I also tried @erahhal 's approach and got the same parse error. I am close to giving up

The openconnect package does support Pulse VPN as long as you don’t need full “Host Checker” support (last I checked I think you could “fake” the result to the VPN server but some might have checks that are difficult to fake). There’s a module already at networking.openconnect.interfaces which should be enough to see if it has a chance of working.

1 Like

Unfortunately, that doesn’t work for me owing to having to use MFA instead of just credentials.

Hadn’t heard of openconnect before. Regarding MFA, would this address it:

Thanks @Princemachiavelli for the tip about openconnect. I ended up going on a rabbit hole and managed to get it to work based on this comment. It was stupidly simple and took all of 2 minutes as opposed to the 4 hours i wasted on trying to package pulse secure vpn client.

@erahhal Perhaps you could try that. I will take a look at the repo you linked to see if it’s any easier.

And the Love it/Oh I absolutely hate it/Love it again relationship with NixOS continues

1 Like

It looks like it supports 2FA via codes if that’s what you need to use.

Also the libcef.so file likely just shows that the program has hardcoded paths (it’s possible a cmdline flag or config file could also set this). I think to provide the files under the expected path you could just add something like this to your service:
BindReadOnlyPaths = [ "${pulsesecure}:/opt/pulsesecure" ]

I think a similar trick can be used to provide the iptables and ip commands since putting symlinks to /sbin could cause issues with other programs.

1 Like

Well thank you all so much, I’ve finally got something working thanks to all of your help. And here’s the code. (Change HOST to your VPN URL).

{ pkgs, lib, ... }:
let
  pulse-cookie = pkgs.python3.pkgs.buildPythonApplication rec {
    pname = "pulse-cookie";
    version = "1.0";

    src = pkgs.fetchPypi {
      inherit pname version;
      sha256 = "sha256-ZURSXfChq2k8ktKO6nc6AuVaAMS3eOcFkiKahpq4ebU=";
    };

    propagatedBuildInputs = [
      pkgs.python3.pkgs.pyqt6
      pkgs.python3.pkgs.pyqt6-webengine
      pkgs.python3.pkgs.setuptools
      pkgs.python3.pkgs.setuptools_scm
    ];

    preBuild = ''
      cat > setup.py << EOF
      from setuptools import setup

      # with open('requirements.txt') as f:
      #     install_requires = f.read().splitlines()

      setup(
        name='pulse-cookie',
        packages=['pulse_cookie'],
        package_dir={"": 'src'},
        version='1.0',
        author='Raj Magesh Gauthaman',
        description='wrapper around openconnect allowing user to log in through a webkit window for mfa',
        install_requires=[
          'PyQt6-WebEngine',
        ],
        entry_points={
          'console_scripts': ['get-pulse-cookie=pulse_cookie._cli:main']
        },
      )
      EOF
    '';

    meta = with lib; {
      homepage = "https://pypi.org/project/pulse-cookie/";
      description = "wrapper around openconnect allowing user to log in through a webkit window for mfa";
      license = licenses.gpl3;
    };
  };
  start-pulse-vpn = pkgs.writeShellScriptBin "start-pulse-vpn" ''
    HOST=https://vpn.example.com/saml
    DSID=$(${pulse-cookie}/bin/get-pulse-cookie -n DSID $HOST)
    sudo ${pkgs.openconnect}/bin/openconnect --protocol nc -C DSID=$DSID $HOST
  '';

in
{
  environment.systemPackages = with pkgs; [
    openconnect
    start-pulse-vpn
  ];
}
3 Likes

Erahhal,

I really appreciate your work on this. However I’m completely new to NixOS and this is a major blocker for me running it as a primary OS. Could you point me in the direction of documentation on how to implement your solution, or could you provide a summary of what you had to do and what files go where? I’ve not had to build a package for Nix before and the documentation I’m finding online has not been super helpful.

Thanks

you don’t need to build this. just include it in your configuration like this:

    imports = [
        ./filename.nix
    ];

then after you rebuild you should have the command start-pulse-vpn.

Do note that there are a couple caveats. The first is that some newer versions of the pulse server don’t work with --protocol pulse, but they still have the old Juniper Network Connect protocol enabled. This is the case with my work, so I have the script above set to use --protocol nc. If they’ve disabled nc, try pulse instead. If that still doesn’t work, you might be out of luck.

1 Like

That worked an absolute treat! Thank you so much for all your effort. Now to dig into it and figure out why it worked.

The pulse-cookie package looks pretty good and your packaging of it is pretty neat @erahhal. I started with no choices and now this thread has given me two choices and I am conflicted as I much prefer the Qt version over the selenium/Chromedriver version I got to work 2 days ago. Here is what I am using for reference,

The python scripts are part of my overlay called pulsevpn

#machines/framework/overlays/pulsevpn/src/openconnect.py

#!/usr/bin/env python

from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
import subprocess

host = "vpn.host.name"
user = "username"
driver = webdriver.Chrome("chromedriver")
wait = WebDriverWait(driver, 60)
driver.get("https://"+host)
dsid = wait.until(lambda driver: driver.get_cookie("DSID"))
driver.quit()
## Both pulse as well as nc work
subprocess.run(["sudo","openconnect", "-C", dsid["value"], "--protocol=pulse", "-u", user, host])
#machines/framework/overlays/pulsevpn/src/setup.py

#!/usr/bin/env python

from setuptools import setup, find_packages

setup(name='pulsevpn',
      version='1.0',
      # Modules to import from other scripts:
      packages=find_packages(),
      # Executables
      scripts=["openconnect.py"],
      )

And the overlay

#machines/framework/overlays/pulsevpn/default.nix

{
  config,
  pkgs,
  lib,
  ...
}: {
  nixpkgs.overlays = [
    (final: prev: {
      pulsevpn = pkgs.python3Packages.buildPythonApplication rec {
        pname = "pulsevpn";
        version = "0.1";

        src = ./src;

        propagatedBuildInputs = with pkgs.python3Packages; [selenium pkgs.chromedriver];

        installPhase = ''
          mkdir -p $out/bin
          cp ${src}/openconnect.py $out/bin/pulsevpn
          chmod +x $out/bin/pulsevpn
        '';

        meta = with lib; {
          description = "Poor Man's Pulse VPN";
          platforms = platforms.linux;
        };
      };
    })
  ];
}

I also tried out what you came up with and slightly polished it to add a wrapper instead to remove some of the annoying Qt5 related warnings and to force the use of Qt6. Here it is as an overlay in case you have the same warnings

#machines/framework/overlays/pulse-vpn/default.nix

{
  config,
  pkgs,
  lib,
  ...
}: {
  nixpkgs.overlays = [
    (final: prev: {
      pulse-vpn = let
        pulse-cookie = pkgs.python3Packages.buildPythonApplication rec {
          pname = "pulse-cookie";
          version = "1.0";

          src = pkgs.fetchPypi {
            inherit pname version;
            sha256 = "sha256-ZURSXfChq2k8ktKO6nc6AuVaAMS3eOcFkiKahpq4ebU=";
          };

          propagatedBuildInputs = with pkgs.python3Packages; [
            pyqt6
            pyqt6-webengine
            setuptools
          ];

          preBuild = ''
            cat > setup.py << EOF
            from setuptools import setup

            setup(
              name='pulse-cookie',
              packages=['pulse_cookie'],
              package_dir={"": 'src'},
              version='1.0',
              author='Raj Magesh Gauthaman',
              description='wrapper around openconnect allowing user to log in through a webkit window for mfa',
              install_requires=[
                'PyQt6-WebEngine',
              ],
              entry_points={
                'console_scripts': ['get-pulse-cookie=pulse_cookie._cli:main']
              },
            )
            EOF
          '';

          meta = with lib; {
            homepage = "https://pypi.org/project/pulse-cookie/";
            description = "wrapper around openconnect allowing user to log in through a webkit window for mfa";
            license = licenses.gpl3;
          };
        };

        pulse-cookie-wrapper = pkgs.runCommand "pulse-cookie-wrapper" {
          buildInputs = [ pkgs.makeWrapper ];
        } ''
          makeWrapper ${pulse-cookie}/bin/get-pulse-cookie $out/bin/get-pulse-cookie \
            --set QT_PLUGIN_PATH "${pkgs.lib.getLib pkgs.qt6.qtbase}/lib/qt-6.2/plugins" \
            --set QML2_IMPORT_PATH "${pkgs.qt6.qtbase}/qml"
        '';

        pulse-vpn-shell-script = pkgs.writeShellScriptBin "pulse-vpn" ''
          export QTWEBENGINE_CHROMIUM_FLAGS="--disable-logging"
          HOST=https://your.vpn.host
          DSID=$(${pulse-cookie-wrapper}/bin/get-pulse-cookie -n DSID $HOST)
          sudo ${pkgs.openconnect}/bin/openconnect --protocol nc -C DSID=$DSID $HOST
        '';
      in
        final.buildEnv {
          name = "pulse-vpn";
          paths = [pulse-cookie-wrapper pulse-vpn-shell-script];
        };
    })
  ];
}

And thanks everyone. This community is amazing.

this is great, thank you. I think I’m gonna switch back to chromedriver as it doesn’t appear that the qt browser supports a yubi key. I’m curious if the embedded gtk webkit browser supports yubi keys - might try that as well…

optimal would be to just use the existing browser so that I can use existing login credentials instead of having to log in every time, but chromedriver doesn’t support that (officially at least).

I was just very happy to have gotten this to work that I didn’t even think about reusing an existing Session. Thanks for the idea.

Given it’s selenium+chromedriver, I thought of automating the user signin via keyring, but instead opted to just look at the Cookie and see if I could just store the whole session. Which is what I ended up doing and it appears to work. I initially thought that the DSID cookie would have an expiry set. But, it is set to last the entire Session (at least in my case and made it much simpler). Here is the updated code in case you want to try

machines/framework/overlays/pulsevpn/src/openconnect.py

#!/usr/bin/env python

import os
import logging
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
import subprocess

host = "your.vpn.server"
user = "username"
chrome_profile_dir = "/home/username/.config/chromedriver/pulsevpn"


if not os.path.exists(chrome_profile_dir):
    os.makedirs(chrome_profile_dir)

logging.basicConfig(level=logging.INFO)
chrome_options = Options()
chrome_options.add_argument("user-data-dir=" + chrome_profile_dir)

def is_dsid_valid(dsid):
    # Expiry is set to Session
    return dsid is not None and 'value' in dsid

def handle_authentication(driver):
    logging.info('User needs to authenticate.')
    wait = WebDriverWait(driver, 60)
    driver.get("https://" + host)
    return wait.until(lambda driver: driver.get_cookie("DSID"))

driver = webdriver.Chrome("chromedriver", options=chrome_options)
logging.info('Starting browser.')

dsid = driver.get_cookie("DSID")

# Check if DSID is invalid or doesn't exist and handle authentication. Perhaps this needs a revisit if the server
# starts to return 403 for invalid DSID.
if not is_dsid_valid(dsid):
    dsid = handle_authentication(driver)

driver.quit()
# Cookie doesn't seem to have an expiry set. Perhaps recycle the profile every 24-48 hours?
logging.info('DSID cookie: %s', dsid)

# Run a shell command to openconnect only if DSID is valid
if is_dsid_valid(dsid):
    logging.info('Launching openconnect.')
    subprocess.run(["sudo", "openconnect", "-C", dsid["value"], "--protocol=pulse", "-u", user, host])
else:
    logging.error('No valid DSID cookie found. Could not launch openconnect.')

Nice! I was wondering what to do about the VPN asking if I wanted to kill previous sessions every time I logged in again.

Hello @erahhal
i tried your script the popup open
but i am stuck with the message “Host checker”
after this nothing happend i dont know why
do i have to install something else?