If you come from i3, you might be missing Xephyr or Xnest-like functionalities in Sway - that is, the ability to run another desktop session as another user inside your current desktop.

In i3, I log into my test desktops all the time without leaving my main desktop, and that’s something I really miss in Sway / Wayland. So I spent some time putting a script together to do that seamlessly in Sway too. You may find it useful.

In fairness, Sway - or more precisely wlroots - can already run nested natively without any modification. You can test that by opening a terminal and typing sway in it: you’ll get a second, identical desktop inside your current one.

The problems come when you want to run another user’s desktop within yours, for the following reasons:

  1. Wayland makes the incredibly restrictive assumption that the Wayland compositor and clients always run as the same user, and therefore puts the Wayland socket in the user’s XDG_RUNTIME_DIR (usually /run/user/<userid>/).

    That’s a problem if you want a Wayland application running as another user to connect to that Wayland socket, because other users can’t access your XDG_RUNTIME_DIR, and you really don’t want to open it up to other users just to be able to access the socket because it’s full of sensitive files pertaining to your running session.

    Moreover, since XDG_RUNTIME_DIR is usually a mounted tmpfs, you can’t symlink the socket outside the directory either because sockets can’t be symlinked across filesystems in Linux.

    In other words, again, Wayland makes it extra difficult to do something simple for no good reason.

  2. Sway requires a full login environment - and particularly XDG_RUNTIME_DIR - to be set in the environment, which usually implies that it should also be setup and mounted in /run/user.

    Unfortunately, you can’t just sudo into the account you want to run your nested Sway desktop as and start Sway because PAM explicitely doesn’t set XDG when su’ing or sudo’ing, and doing it manually is a recipe for problems.

To solve 1., we use a clever piece of software called filterway, which conveniently solves two problems:

  • It acts as a sort of gateway: it connects to a Wayland socket on one side, creates its own socket on the other side and links the two. This functionality is used to expose the primary Wayland socket securely without compromising XDG_RUNTIME_DIR.

  • It replaces the app ID of the top Wayland client that connects to it - which is really its primary party trick. In this use case, that’s useful to track the state of the nested Sway session in the primary session’s tree.

There is no package for filterway so you have to clone the Github repo, build the binary and install it somewhere in your PATH. Fortunately, it’s just a small utility so building it is really simple.

To solve 2., we use systemd-run to setup the target user’s environment as if it was a full login, then run Sway with the correct setup to connect to the primary Wayland display’s socket.

The following script ties everything together: it starts filterway, starts Sway as the other user, then takes care of stopping Sway and filterway and cleaning things up when the session is closed. Alll you need is to add your name to the sudoers.

#!/bin/sh

# Make sure we run in Wayland
if [ ! "${WAYLAND_DISPLAY}" ]; then
  echo "$0 must be run in a Wayland environment"
  exit
fi

# Make sure we run in Sway
if [ ! "${SWAYSOCK}" ]; then
  echo "$0 must be run in a Sway session"
  exit
fi

# Pass the nested session's user as first argument
if [ ! "$1" ]; then
  echo "Usage: $0 username"
  exit
fi
NUSER=$1

# Make sure the nested session's user exists
if ! grep -q "^${NUSER}:" /etc/passwd; then
  echo "User ${NUSER} doesn't exist"
  exit
fi

# Make sure filterway is installed
if ! which -s filterway; then
  echo "filterway not found in the PATH."
  echo "Please install if from https://github.com/andrewbaxter/filterway"
  exit
fi

# Get a unique ID for this nested session
UUID=$(uuidgen)

# Figure out where our Wayland socket is and make sure it exists
if echo ${WAYLAND_DISPLAY} | grep -q "^/"; then 
  RSOCKPATH=${WAYLAND_DISPLAY}
else
  RSOCKPATH=${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}
fi
if ! [ -S ${RSOCKPATH} ]; then 
  echo "Socket file ${RSOCKPATH} for this Wayland display \"${WAYLAND_DISPLAY}\" doesn't exist!?"
  echo "Giving up..."
  exit
fi

# Unique nested session's Wayland display name
NWDISPLAY=wayland-nested-${UUID}

# Unique filespec for the nested session's Wayland socket
NSOCKPATH=/tmp/${NWDISPLAY}

# Unique filespec for the nested Sway socket
NSWAYSOCK=/tmp/sway-nested-ipc.${NUSER}.${UUID}.sock

# Run filterway in the background to expose our private Wayland socket in ${XDG_RUNTIME_DIR} (which is most likely a tmpfs-mounted directory that can't be shared outside without compromising the private $(XDG_RUNTIME_DIR}) and to rename the nested session's app ID
rm -f ${NSOCKPATH}
filterway --upstream ${RSOCKPATH} --downstream ${NSOCKPATH} --app-id "Nested Sway - ${NUSER} ($UUID)" &
FILTERWAY_PID=$!

# Wait until filterway has created the socket and associated lock files for the nested session
RETRY=3
while [ ${RETRY} -gt 0 ] && ! ( [ -S ${NSOCKPATH} ] && [ -f ${NSOCKPATH}.lock ] ); do
  sleep 1
  RETRY=$((RETRY-1))
done

# If filterway somehow didn't start, try to kill it and clean up its files for good measure
if [ ${RETRY} = 0 ]; then
  kill ${FILTERWAY_PID}
  rm -f ${NSOCKPATH} ${NSOCKPATH}.lock
fi

# Fix up the permissions of the socket and associated lock files for the nested session so it's only accessible to the owner
chmod 600 ${NSOCKPATH} ${NSOCKPATH}.lock

# Become root
sudo -s -- << EOF

  # Give the socket and associated lock files to the nested session's user
  chown ${NUSER}: ${NSOCKPATH} ${NSOCKPATH}.lock

  # Remove stale symlinks then start Sway as that user in a new session in the background
  systemd-run --pipe --machine ${NUSER}@ --setenv=WAYLAND_DISPLAY=${NWDISPLAY} --setenv=SWAYSOCK=${NSWAYSOCK} --user /bin/sh -c '[ "\${XDG_RUNTIME_DIR}" ] && (find \${XDG_RUNTIME_DIR} -maxdepth 1 -name "wayland-nested-*" -xtype l -exec rm -f {} \; || true) && rm -f \${XDG_RUNTIME_DIR}/${NWDISPLAY} && ln -s ${NSOCKPATH} \${XDG_RUNTIME_DIR}/${NWDISPLAY} && sway' &

  # Wait for the Sway container to appear within 3 seconds after starting Sway, then wait for it to disappear for more than 5 seconds afterwards
  export SWAYSOCK=${SWAYSOCK}
  COUNTDOWN=3
  while [ \${COUNTDOWN} -gt 0 ]; do
    if swaymsg -t get_tree | grep -q 'app_id.*${UUID}'; then
      COUNTDOWN=5
    fi
    sleep 1
    COUNTDOWN=\$((COUNTDOWN-1))
  done

  # Stop the nested Sway
  SWAYSOCK=${NSWAYSOCK} swaymsg exit

  # Kill filterway
  kill ${FILTERWAY_PID}

  # Remove the filterway socket and socket lock files
  rm -f ${NUSER}: ${NSOCKPATH} ${NSOCKPATH}.lock

EOF

I called it nest_sway.sh and it lives in my ~/scripts directory, which is in my PATH. Whenever I want to start a desktop as another user within my desktop, I simply type

$ nest_sway.sh <username>

and hey-presto, the desktop appears. Just like with Xephy.

  • ExtremeDullard@lemmy.sdf.orgOP
    link
    fedilink
    arrow-up
    1
    ·
    edit-2
    10 hours ago

    And here’s a version that uses pkexec instead of sudo, if you want to run the script without a terminal and enter your login in a login popup (using lxpolkit for example):

    #!/bin/sh
    
    # Are we running as a normal user?
    if [ $(whoami) != root ]; then
    
      # Make sure we run in Wayland
      if [ ! "${WAYLAND_DISPLAY}" ]; then
        echo "$0 must be run in a Wayland environment"
        exit
      fi
      
      # Make sure we run in Sway
      if [ ! "${SWAYSOCK}" ]; then
        echo "$0 must be run in a Sway session"
        exit
      fi
      
      # Get the nested session's user as first argument
      if [ ! "$1" ]; then
        echo "Usage: $0 username"
        exit
      fi
      NUSER=$1
      
      # Make sure the nested session's user exists
      if ! grep -q "^${NUSER}:" /etc/passwd; then
        echo "User ${NUSER} doesn't exist"
        exit
      fi
      
      # Make sure filterway is installed
      if ! which -s filterway; then
        echo "filterway not found in the PATH."
        echo "Please install if from https://github.com/andrewbaxter/filterway"
        exit
      fi
      
      # Get a unique ID for this nested session
      UUID=$(uuidgen)
      
      # Figure out where our Wayland socket is and make sure it exists
      if echo ${WAYLAND_DISPLAY} | grep -q "^/"; then 
        RSOCKPATH=${WAYLAND_DISPLAY}
      else
        RSOCKPATH=${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}
      fi
      if ! [ -S ${RSOCKPATH} ]; then 
        echo "Socket file ${RSOCKPATH} for this Wayland display \"${WAYLAND_DISPLAY}\" doesn't exist!?"
        echo "Giving up..."
        exit
      fi
      
      # Unique nested session's Wayland display name
      NWDISPLAY=wayland-nested-${UUID}
      
      # Unique filespec for the nested session's Wayland socket
      NSOCKPATH=/tmp/${NWDISPLAY}
      
      # Unique filespec for the nested Sway socket
      NSWAYSOCK=/tmp/sway-nested-ipc.${NUSER}.${UUID}.sock
      
      # Run filterway in the background to expose our private Wayland socket in ${XDG_RUNTIME_DIR} (which is most likely a tmpfs-mounted directory that can't be shared outside without compromising the private $(XDG_RUNTIME_DIR}) and to rename the nested session's app ID
      rm -f ${NSOCKPATH}
      filterway --upstream ${RSOCKPATH} --downstream ${NSOCKPATH} --app-id "Nested Sway - ${NUSER} ($UUID)" &
      FILTERWAY_PID=$!
      
      # Wait until filterway has created the socket and associated lock files for the nested session
      RETRY=3
      while [ ${RETRY} -gt 0 ] && ! ( [ -S ${NSOCKPATH} ] && [ -f ${NSOCKPATH}.lock ] ); do
        sleep 1
        RETRY=$((RETRY-1))
      done
      
      # If filterway somehow didn't start, try to kill it and clean up its files for good measure
      if [ ${RETRY} = 0 ]; then
        kill ${FILTERWAY_PID}
        rm -f ${NSOCKPATH} ${NSOCKPATH}.lock
      fi
      
      # Fix up the permissions of the socket and associated lock files for the nested session so it's only accessible to the owner
      chmod 600 ${NSOCKPATH} ${NSOCKPATH}.lock
      
      # Re-run ourselves as root to perform the rest of the operations to spawn the nested session
      VARFILE=${XDG_RUNTIME_DIR}/$(echo $UUID | cut -d- -f1).nested_sway_vars
    
      echo "NUSER=${NUSER}" > ${VARFILE}
      echo "NWDISPLAY=${NWDISPLAY}" >> ${VARFILE}
      echo "UUID=${UUID}" >> ${VARFILE}
      echo "NSOCKPATH=${NSOCKPATH}" >> ${VARFILE}
      echo "SWAYSOCK=${SWAYSOCK}" >> ${VARFILE}
      echo "NSWAYSOCK=${NSWAYSOCK}" >> ${VARFILE}
      echo "FILTERWAY_PID=${FILTERWAY_PID}" >> ${VARFILE}
    
      pkexec $0 ${VARFILE} || rm ${VARFILE}
    
    # We run as root
    else
    
      # Source the file containing the variables we need created by the non-root version of ourselves
      if [ ! "$1" ]; then
        echo "$0 (running as root): missing variable file needed"
        exit
      fi
      VARFILE=$1
      . ${VARFILE}
    
      # Check that we were passed all the variables we need and assign them
      if ! [ "$NUSER" ]; then
        echo "$0 (running as root): missing NUSER variable"
        exit
      fi
      if ! [ "$NWDISPLAY" ]; then
        echo "$0 (running as root): missing NWDISPLAY variable"
        exit
      fi
      if ! [ "$UUID" ]; then
        echo "$0 (running as root): missing UUID variable"
        exit
      fi
      if ! [ "$NSOCKPATH" ]; then
        echo "$0 (running as root): missing NSOCKPATH variable"
        exit
      fi
      if ! [ "$SWAYSOCK" ]; then
        echo "$0 (running as root): missing SWAYSOCK variable"
        exit
      fi
      if ! [ "$NSWAYSOCK" ]; then
        echo "$0 (running as root): missing NSWAYSOCK variable"
        exit
      fi
      if ! [ "$FILTERWAY_PID" ]; then
        echo "$0 (running as root): missing FILTERWAY_PID variable"
        exit
      fi
    
      # Remove the variables file
      rm ${VARFILE}
    
      # Give the socket and associated lock files to the nested session's user
      chown ${NUSER}: ${NSOCKPATH} ${NSOCKPATH}.lock
      
      # Remove stale symlinks then start Sway as that user in a new session in the background
      systemd-run --pipe --machine ${NUSER}@ --setenv=WAYLAND_DISPLAY=${NWDISPLAY} --setenv=NSOCKPATH=${NSOCKPATH} --setenv=SWAYSOCK=${NSWAYSOCK} --user /bin/sh -c '[ "${XDG_RUNTIME_DIR}" ] && (find ${XDG_RUNTIME_DIR} -maxdepth 1 -name "wayland-nested-*" -xtype l -exec rm -f {} \; || true) && rm -f ${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY} && ln -s ${NSOCKPATH} ${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY} && unset NSOCKPATH && sway' &
    
      # Wait for the Sway container to appear within 3 seconds after starting Sway, then wait for it to disappear for more than 5 seconds afterwards
      COUNTDOWN=3
      while [ ${COUNTDOWN} -gt 0 ]; do
        if SWAYSOCK=${SWAYSOCK} swaymsg -t get_tree | grep -q "app_id.*${UUID}"; then
          COUNTDOWN=5
        fi
        sleep 1
        COUNTDOWN=$((COUNTDOWN-1))
      done
      
      # Stop the nested Sway
      SWAYSOCK=${NSWAYSOCK} swaymsg exit
    
      # Kill filterway
      kill ${FILTERWAY_PID}
    
      # Remove the filterway socket and socket lock files
      rm -f ${NUSER}: ${NSOCKPATH} ${NSOCKPATH}.lock
    
    fi
    

    It’s a bit more complicated because pkexec can’t run a shell script directly, so the script has to re-run itself as root and pass itself the proper variables. But essentially it’s the same thing.

    With this version, you can define a desktop file to open the desktop of a particular user in your favorite application launcher without opening a terminal first.

    For example, if you create /.local/share/applications/test-desktop.desktop with the following content (or similar):

    [Desktop Entry]
    Name=test desktop
    Exec=nest_sway.sh test
    Icon=gnome-remote-desktop
    Type=Application
    Keywords=sway;nest;nested;
    

    then you’ll be able to open the desktop of user test in a nested Sway from the application launcher