├── .gitignore ├── .mailmap ├── COPYING.md ├── README.md ├── backend ├── dinit ├── dinit.conf ├── meson.build ├── runit ├── runit.conf └── turnstile-update-runit-env.in ├── data ├── dinit │ └── turnstiled └── pam │ └── turnstiled ├── include └── turnstile.h ├── meson.build ├── meson_options.txt ├── src ├── cfg_utils.cc ├── exec_utils.cc ├── fs_utils.cc ├── lib_api.c ├── lib_api.h ├── lib_backend_none.c ├── lib_backend_none.h ├── lib_backend_turnstile.c ├── pam_turnstile.8.scd ├── pam_turnstile.cc ├── protocol.hh ├── turnstiled.8.scd ├── turnstiled.cc ├── turnstiled.hh ├── utils.cc └── utils.hh ├── turnstiled.conf.5.scd.in └── turnstiled.conf.in /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | # add yourself here if name/email changes 2 | # 3 | # format: 4 | # 5 | # propername commitname 6 | 7 | q66 Daniel Kolesa 8 | q66 Daniel Kolesa 9 | q66 Daniel Kolesa 10 | q66 q66 11 | -------------------------------------------------------------------------------- /COPYING.md: -------------------------------------------------------------------------------- 1 | Copyright 2021-2024 q66 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 16 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 17 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 18 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 19 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 20 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 21 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 22 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # turnstile 2 | 3 | Turnstile is a work in progress effort to create a session/login tracker to 4 | serve as a fully featured alternative to the logind subproject from systemd, 5 | and to provide a neutral API to both our session tracker and to logind itself. 6 | 7 | It is: 8 | 9 | * a session/login tracker 10 | * a service-manager-agnostic way to manage per-user service managers 11 | for user services 12 | 13 | it is not: 14 | 15 | * a seat tracker (you want [seatd](https://git.sr.ht/~kennylevinsen/seatd) for 16 | that) 17 | 18 | it is not yet: 19 | 20 | * a library to examine session information 21 | 22 | ## History 23 | 24 | Its original name was dinit-userservd and it was created as a way to auto-spawn 25 | user instances of [Dinit](https://github.com/davmac314/dinit) upon login and 26 | shut them down upon logout, to allow for clean management of user services. 27 | 28 | Soon after it outgrew its original responsibilities and gained adjacent 29 | functionality such as handling of `XDG_RUNTIME_DIR`. At that point, it was 30 | decided that it would be worthwhile to expand the overall scope, as most of 31 | the effort was already there. 32 | 33 | ## Purpose 34 | 35 | Its ultimate goal is to provide a fully featured replacement for the `logind` 36 | component of systemd, solving the current status quo where `logind` is the 37 | de-facto standard, but at the same time very much tied to systemd. 38 | 39 | While there are workarounds such as elogind, these are far from ideal. For 40 | instance, elogind is just a stubbed out version of upstream logind, and 41 | only provides the bare minimum, so systems using it are left without support 42 | for user services and other useful functionality. 43 | 44 | This goal has not yet been accomplished, as at the moment Turnstile is only 45 | a daemon and does not provide any API. This will change in the future. This 46 | API will provide a way to access the session information, but will not deal 47 | with seat management. You will be able to use the library together with 48 | `libseat` without conflicting. The API will expose the bare minimum needed 49 | for the two libraries to interoperate. 50 | 51 | Turnstile is designed to not care about what service manager it is used with. 52 | None of the daemon code cares, instead leaving this to separate backends. 53 | 54 | ## Backends 55 | 56 | Turnstile is capable of supporting multiple service managers, and the code 57 | makes no assumptions about what service manager one is using to handle user 58 | instances. 59 | 60 | That said, right now the only available backend is for Dinit, which also 61 | serves as an example for implementation of other backends. There is also 62 | the built-in `none` backend, which does not handle user services at all 63 | and lets the daemon do only session tracking and auxiliary tasks. The 64 | used backend is configured in `turnstiled.conf`. 65 | 66 | A backend is a very trivial shell script. Its responsibility is to launch 67 | the service manager and ensure that the daemon is notified of its readiness, 68 | which is handled with a special file descriptor. 69 | 70 | ## How it works 71 | 72 | There are three parts. 73 | 74 | 1) The daemon, `turnstiled`. 75 | 2) The PAM module, `pam_turnstile.so`. 76 | 3) The chosen backend. 77 | 78 | The daemon needs to be running in some way. Usually you will spawn it as a 79 | system-wide service. It needs to be running as the superuser. The daemon is 80 | what keeps track of the session state, and what launches the user service 81 | manager through the backend. 82 | 83 | The PAM module needs to be in your login path. This will differ per-distro, 84 | but typically it will involve a line like this: 85 | 86 | ``` 87 | session optional pam_turnstile.so 88 | ``` 89 | 90 | When the daemon starts, it opens a Unix domain socket. This is where it listens 91 | for connections. When a user tries to log in, the PAM module will open one such 92 | connection and communicate the information to the daemon using a custom internal 93 | protocol. 94 | 95 | Once the handshake is done and all the state is properly negotiated, the daemon 96 | will try to spawn the service manager for the user. It does so through the 97 | backend, which is tasked with the `run` action. 98 | 99 | The backend is a little helper program that can be written in any language, it 100 | can e.g. be a shell script. It is started with a clean environment with many 101 | of the common environment variables, such as `HOME`, `USER`, `LOGNAME`, `SHELL`, 102 | `PATH` and others, freshly initialized. Typically it is expected to source 103 | the system `/etc/profile` for `/bin/sh`. Additionally, it runs within a PAM 104 | session (without authentication), which persists for the lifetime of the 105 | login, so PAM environment, resource limits and so on are also set up. 106 | It may also be a good idea to put `pam_elogind` or `pam_systemd` in there in 107 | order to have `logind` recognize the `turnstile` user session as a session 108 | (which allows it to be tracked by things using it, e.g. `polkitd`). 109 | 110 | Note that if you use `pam_systemd` or `pam_elogind` in `turnstiled` PAM 111 | script to register it as a session, it will be treated as a session without 112 | a seat. That means things like `polkit` may treat anything running within 113 | `turnstile` as a non-local session, and may not authenticate the processes. 114 | There is no way to get around this limitation outside of patching `polkit`, 115 | see Chimera's patches for reference. The alternative is not registering it 116 | at all, which will not make `polkit` work, as the session tracking logic in 117 | it will not be able to assign the processes to any UID and things will not 118 | work either. Systemd user services are treated specially by `systemd`, as 119 | they are recognized by the service manager, but are explicitly not considered 120 | to be a part of any session (as they are shared); that means `polkit` will 121 | fall back to looking up whether any seated session for the UID exists. 122 | 123 | After performing some initial preparation (which is backend-specific), the 124 | backend will simply replace itself with the desired service manager. There 125 | is a special file descriptor that is passed to the backend. The service 126 | manager (or possibly even the backend itself) can write a string of data 127 | in there when it's ready enough to accept outside commands. 128 | 129 | Once that has happened, the daemon will invoke the backend once more, this 130 | time with the `ready` action and as a regular (non-login) shell script, without 131 | any special environment setup. It passes the previously received string as 132 | an argument. The backend then has the responsibility to wait as long as it 133 | takes (or until a timeout is reached) for the initial user services to start 134 | up. 135 | 136 | Afterwards, the daemon will send a message back to the PAM module, allowing 137 | the login to proceed. This ensures that by the time the user gets their login 138 | terminal, the autostarted user services are already up. 139 | 140 | When the user logs out (or rather, when the last login of the user has logged 141 | out), this service manager will shut down by default. However, it can also be 142 | configured to linger. 143 | 144 | ### Auxiliary tasks 145 | 146 | The daemon can also perform various adjacent tasks. As it can be configured 147 | through `turnstiled.conf`, many of these can be enabled or disabled as needed. 148 | 149 | #### Rundir management 150 | 151 | The environment variable `XDG_RUNTIME_DIR` is by default set in the user's 152 | login environment. Typically it is something like `/run/user/$UID`. 153 | 154 | Turnstile can also create this directory. Whether it creates it by default 155 | comes down to how the build is configured. Environments using stock `logind` 156 | will want to keep it off in order to avoid conflicting, while others may 157 | want to turn it on. 158 | 159 | Regardless of the default behavior, it can be altered in the configuration file. 160 | 161 | #### Session persistence 162 | 163 | It is possible to configure the sessions to linger, so the user services will 164 | remain up even after logout. This can be done either per-user, or globally. 165 | 166 | Note that session persistence relies on rundir creation being enabled, as in 167 | the other case the daemon cannot know whether the other management solution 168 | is not deleting the rundir, and many user services rely on its existence. 169 | This can be manually overridden with an environment variable, at your own 170 | risk. 171 | 172 | #### D-Bus session bus address 173 | 174 | By default, the address of the D-Bus session bus will be exported into the 175 | login environment and set to something like `unix:path=$XDG_RUNTIME_DIR/bus`, 176 | if that socket exists and is valid in that path. 177 | 178 | This allows the D-Bus session bus to be managed as a user service, to get 179 | systemd-style behavior with a single session bus shared between user logins. 180 | It can be explicitly disabled if necessary, but mostly there is no need to 181 | as the variable will not be exported if the bus does not exist there. 182 | 183 | Note that this does not mean the bus address is exported into the activation 184 | environment, as turnstile does not know about it. The user service that spawns 185 | the session bus needs to take care of that, e.g. with `dinitctl setenv` for 186 | Dinit. Only this way will other user services know about the session bus. 187 | 188 | ## Setup 189 | 190 | Build and install the project. It uses [Meson](https://mesonbuild.com/) and 191 | follows the standard Meson workflow. Example: 192 | 193 | ``` 194 | $ mkdir build && cd build 195 | $ meson .. --prefix=/usr 196 | $ ninja all 197 | $ sudo ninja install 198 | ``` 199 | 200 | The dependencies are: 201 | 202 | 1) A POSIX-compliant OS (Chimera Linux is the reference platform) 203 | 2) A C++17 compiler 204 | 3) Meson and Ninja (to build) 205 | 5) PAM 206 | 207 | The Dinit backend requires at least Dinit 0.16 or newer, older versions will 208 | not work. The project also installs an example Dinit service for starting 209 | the daemon. 210 | 211 | ## Support for other service managers 212 | 213 | If you write a new backend or other functionality related to other service 214 | managers, it would be appreciated if you could submit it upstream (i.e. here). 215 | This way we can ensure that other backends stay aligned with the upstream 216 | design goals and will not break over time. 217 | 218 | Additionally, you can get review here, which should ultimately result in 219 | more consistent and better quality code. Turnstile is specifically designed 220 | to help distro interoperability. 221 | 222 | Support for other operating systems (such as the BSDs) is also welcome. While 223 | the project tries to be portable, it is being tested solely on Linux. Therefore, 224 | testing on other operating systems and potential fixes (please send patches) 225 | are very helpful. Ultimately I would like the project to serve as a vendor-neutral 226 | interface on all Unix-like systems, so that desktop environments and other 227 | projects have a quality baseline to target. 228 | -------------------------------------------------------------------------------- /backend/dinit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This is the turnstile dinit backend. It accepts the action as its first 4 | # argument, which is either "ready", "run", or "stop". The backend can be 5 | # written in any language, in this case the shebang is used to run it. 6 | # The system profile (but not user profile) for /bin/sh is sourced before 7 | # anything is run, in order to include profile.d snippets into the 8 | # activation environment. 9 | # 10 | # It also serves as an example of how to implement such backend. 11 | # 12 | # Arguments for "ready": 13 | # 14 | # socket: the path to dinit's control socket; it is the string that is 15 | # written by dinit into ready_fd for the "run" part of the process 16 | # 17 | # Arguments for "run": 18 | # 19 | # ready_p: path to named pipe (fifo) that should be poked with a string; this 20 | # will be passed to the "ready" script of the sequence as its sole 21 | # argument (here this is a control socket path) 22 | # srvdir: an internal directory that can be used by the service manager 23 | # for any purpose (usually to keep track of its state) 24 | # confdir: the path where turnstile's configuration data reside, used 25 | # to source the configuration file 26 | # 27 | # Arguments for "stop": 28 | # 29 | # pid: the PID of the service manager to stop (gracefully); it should 30 | # terminate the services it's running and then stop itself 31 | # 32 | # How the script manages its configuration and so on is up to the script. 33 | # 34 | # Note that the script *must* exec the service manager directly, i.e. the 35 | # service manager must fully replace the shell process for this to work. 36 | # 37 | # Copyright 2023 q66 38 | # License: BSD-2-Clause 39 | # 40 | 41 | case "$1" in 42 | run) ;; 43 | ready) 44 | if [ -z "$2" -o ! -S "$2" ]; then 45 | # must be a control socket 46 | echo "dinit: invalid control socket '$2'" >&2 47 | exit 69 48 | fi 49 | exec dinitctl --socket-path "$2" start login.target 50 | ;; 51 | stop) 52 | exec kill -s TERM "$2" 53 | ;; 54 | graphical-notify) 55 | if [ -z "$DINIT_CS_FD" ]; then 56 | # must have a control socket 57 | echo "dinit: control socket not given" >&2 58 | exit 69 59 | fi 60 | # this is not invoked by turnstile, but by the monitor service 61 | exec dinitctl trigger graphical.target 62 | ;; 63 | *) 64 | exit 32 65 | ;; 66 | esac 67 | 68 | DINIT_READY_PIPE="$2" 69 | DINIT_DIR="$3" 70 | DINIT_CONF="$4/dinit.conf" 71 | 72 | if [ ! -p "$DINIT_READY_PIPE" -o ! -d "$DINIT_DIR" ]; then 73 | echo "dinit: invalid input argument(s)" >&2 74 | exit 69 75 | fi 76 | 77 | if [ -z "$HOME" -o ! -d "$HOME" ]; then 78 | echo "dinit: invalid home directory" >&2 79 | exit 70 80 | fi 81 | 82 | shift $# 83 | 84 | # source system profile mainly for profile.d 85 | # do it before switching to set -e etc. 86 | [ -r /etc/profile ] && . /etc/profile 87 | 88 | # be strict 89 | set -e 90 | 91 | # source the conf 92 | [ -r "$DINIT_CONF" ] && . "$DINIT_CONF" 93 | 94 | # set a bunch of defaults in case the conf cannot be read or is mangled 95 | 96 | [ -z "$boot_dir" ] && boot_dir="${HOME}/.config/dinit.d/boot.d" 97 | [ -z "$system_boot_dir" ] && system_boot_dir="/usr/lib/dinit.d/user/boot.d" 98 | 99 | if [ -z "$services_dir1" ]; then 100 | services_dir1="${HOME}/.config/dinit.d" 101 | services_dir2="/etc/dinit.d/user" 102 | services_dir3="/usr/local/lib/dinit.d/user" 103 | services_dir4="/usr/lib/dinit.d/user" 104 | fi 105 | 106 | # translate service dirs to arguments; we pass them to dinit at the end 107 | seqn=1 108 | while :; do 109 | eval curserv="\$services_dir$seqn" 110 | [ -n "$curserv" ] || break 111 | set -- "$@" --services-dir "$curserv" 112 | seqn=$(($seqn + 1)) 113 | done 114 | 115 | # create boot dir, but make it not a failure if we can't 116 | mkdir -p "${boot_dir}" > /dev/null 2>&1 || : 117 | 118 | # this must succeed 119 | cat << EOF > "${DINIT_DIR}/boot" 120 | type = internal 121 | depends-on = system 122 | waits-for.d = ${boot_dir} 123 | depends-on = login.target 124 | depends-ms = graphical.monitor 125 | depends-ms = graphical.target 126 | EOF 127 | 128 | # this must also succeed 129 | cat << EOF > "${DINIT_DIR}/system" 130 | type = internal 131 | waits-for.d = ${system_boot_dir} 132 | EOF 133 | 134 | # monitor service to watch for environment changes 135 | cat << EOF > "${DINIT_DIR}/graphical.monitor" 136 | type = process 137 | depends-on = login.target 138 | options = pass-cs-fd 139 | command = /usr/bin/dinit-monitor -E -c "$0 graphical-notify" WAYLAND_DISPLAY DISPLAY 140 | EOF 141 | 142 | # this is needed for login to proceed 143 | cat << EOF > "${DINIT_DIR}/login.target" 144 | type = internal 145 | EOF 146 | 147 | # this is not necessary to have started for login to proceed 148 | cat << EOF > "${DINIT_DIR}/graphical.target" 149 | type = triggered 150 | depends-on = graphical.monitor 151 | depends-on = login.target 152 | EOF 153 | 154 | exec dinit --user --ready-fd 3 --services-dir "$DINIT_DIR" "$@" 3>"$DINIT_READY_PIPE" 155 | -------------------------------------------------------------------------------- /backend/dinit.conf: -------------------------------------------------------------------------------- 1 | # This is the configuration file for turnstile's dinit backend. 2 | # 3 | # It follows the POSIX shell syntax (being sourced into a script). 4 | # The complete launch environment available to dinit can be used. 5 | # 6 | # It is a low-level configuration file. In most cases, it should 7 | # not be modified by the user. 8 | # 9 | 10 | # The directory containing service links that must be 11 | # started in order for the login to proceed. Can be 12 | # empty, in which case nothing is waited for. 13 | # 14 | boot_dir="${HOME}/.config/dinit.d/boot.d" 15 | 16 | # This is just like boot_dir, but not controlled by the 17 | # user. Instead, the system installs links there, and 18 | # they are started for all users universally. 19 | # 20 | system_boot_dir="/usr/lib/dinit.d/user/boot.d" 21 | 22 | # A directory user service files are read from. Every 23 | # additional directory needs to have its number incremented. 24 | # The numbering matters (defines the order) and there must be 25 | # no gaps (it starts with 1, ends at the last undefined). 26 | # 27 | # If no services directory is defined (i.e. the first one 28 | # is not defined), a built-in list will be used (which is 29 | # equal to the one defined here). 30 | # 31 | services_dir1="${HOME}/.config/dinit.d" 32 | services_dir2="/etc/dinit.d/user" 33 | services_dir3="/usr/local/lib/dinit.d/user" 34 | services_dir4="/usr/lib/dinit.d/user" 35 | -------------------------------------------------------------------------------- /backend/meson.build: -------------------------------------------------------------------------------- 1 | # dinit backend 2 | 3 | if have_dinit 4 | install_data( 5 | 'dinit', 6 | install_dir: join_paths(get_option('libexecdir'), 'turnstile'), 7 | install_mode: 'rwxr-xr-x' 8 | ) 9 | 10 | install_data( 11 | 'dinit.conf', 12 | install_dir: join_paths(get_option('sysconfdir'), 'turnstile/backend'), 13 | install_mode: 'rw-r--r--' 14 | ) 15 | endif 16 | 17 | # runit backend 18 | 19 | if have_runit 20 | install_data( 21 | 'runit', 22 | install_dir: join_paths(get_option('libexecdir'), 'turnstile'), 23 | install_mode: 'rwxr-xr-x' 24 | ) 25 | 26 | install_data( 27 | 'runit.conf', 28 | install_dir: join_paths(get_option('sysconfdir'), 'turnstile/backend'), 29 | install_mode: 'rw-r--r--' 30 | ) 31 | 32 | configure_file( 33 | input: 'turnstile-update-runit-env.in', 34 | output: 'turnstile-update-runit-env', 35 | configuration: conf_data, 36 | install: true, 37 | install_dir: get_option('bindir'), 38 | install_mode: 'rwxr-xr-x' 39 | ) 40 | endif 41 | -------------------------------------------------------------------------------- /backend/runit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # This is the turnstile runit backend. It accepts the action as its first 4 | # argument, which is either "ready", "run", or "stop". In case of "run", it's 5 | # invoked directly through /bin/sh as if it was a login shell, and therefore 6 | # it has acccess to shell profile, and the shebang is functionally useless but 7 | # should be preserved as a convention. For "ready", it's a regular shell. 8 | # 9 | # Arguments for "ready": 10 | # 11 | # ready_sv: path to the readiness service 12 | # 13 | # Arguments for "run": 14 | # 15 | # ready_p: readiness pipe (fifo). has the path to the ready service written to it. 16 | # srvdir: unused 17 | # confdir: the path where turnstile's configuration data resides, used 18 | # to source the configuration file 19 | # 20 | # Arguments for "stop": 21 | # 22 | # pid: the PID of the service manager to stop (gracefully); it should 23 | # terminate the services it's running and then stop itself 24 | # 25 | # Copyright 2023 classabbyamp 26 | # License: BSD-2-Clause 27 | 28 | case "$1" in 29 | run) ;; 30 | ready) 31 | if [ -z "$2" ] || [ ! -d "$2" ]; then 32 | echo "runit: invalid readiness service '$2'" >&2 33 | exit 69 34 | fi 35 | exec sv start "$2" >&2 36 | ;; 37 | stop) 38 | # If runsvdir receives a HUP signal, it sends a TERM signal to each 39 | # runsv(8) process it is monitoring and then exits with 111. 40 | exec kill -s HUP "$2" 41 | ;; 42 | *) 43 | exit 32 44 | ;; 45 | esac 46 | 47 | RUNIT_READY_PIPE="$2" 48 | RUNIT_CONF="$4/runit.conf" 49 | 50 | if [ ! -p "$RUNIT_READY_PIPE" ]; then 51 | echo "runit: invalid input argument(s)" >&2 52 | exit 69 53 | fi 54 | 55 | if [ -z "$HOME" ] || [ ! -d "$HOME" ]; then 56 | echo "runit: invalid home directory" >&2 57 | exit 70 58 | fi 59 | 60 | shift $# 61 | 62 | # source system profile mainly for profile.d 63 | # do it before switching to set -e etc. 64 | [ -r /etc/profile ] && . /etc/profile 65 | 66 | # be strict 67 | set -e 68 | 69 | # source the conf 70 | [ -r "$RUNIT_CONF" ] && . "$RUNIT_CONF" 71 | 72 | # set some defaults in case the conf cannot be read or is mangled 73 | : "${ready_sv:="turnstile-ready"}" 74 | : "${services_dir:="${HOME}/.config/service"}" 75 | : "${service_env_dir:="${HOME}/.config/service-env"}" 76 | 77 | mkdir -p "${services_dir}/${ready_sv}" > /dev/null 2>&1 78 | mkdir -p "${service_env_dir}" > /dev/null 2>&1 79 | 80 | # this must succeed 81 | cat << EOF > "${services_dir}/${ready_sv}/run" 82 | #!/bin/sh 83 | [ -r ./conf ] && . ./conf 84 | [ -n "\$core_services" ] && SVDIR=".." sv start \$core_services 85 | [ -p "$RUNIT_READY_PIPE" ] && printf "${services_dir}/${ready_sv}" > "$RUNIT_READY_PIPE" 86 | exec pause 87 | EOF 88 | chmod +x "${services_dir}/${ready_sv}/run" 89 | 90 | exec env TURNSTILE_ENV_DIR="$service_env_dir" \ 91 | runsvdir -P "$services_dir" \ 92 | 'log: ...........................................................................................................................................................................................................................................................................................................................................................................................................' 93 | -------------------------------------------------------------------------------- /backend/runit.conf: -------------------------------------------------------------------------------- 1 | # This is the configuration file for turnstile's runit backend. 2 | # 3 | # It follows the POSIX shell syntax (being sourced into a script). 4 | # The complete launch environment available to dinit can be used. 5 | # 6 | # It is a low-level configuration file. In most cases, it should 7 | # not be modified by the user. 8 | 9 | # the name of the service that turnstile will check for login readiness 10 | ready_sv="turnstile-ready" 11 | 12 | # the directory user service files are read from. 13 | services_dir="${HOME}/.config/service" 14 | 15 | # the environment variable directory user service files can read from. 16 | service_env_dir="${HOME}/.config/service-env" 17 | -------------------------------------------------------------------------------- /backend/turnstile-update-runit-env.in: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright 2023 classabbyamp 3 | # License: BSD-2-Clause 4 | 5 | usage() { 6 | cat <<-EOF 7 | turnstile-update-runit-env [VAR] ... 8 | Updates values in the shared chpst(8) env dir. 9 | 10 | If VAR is a variable name, the value is taken from the environment. 11 | If VAR is VAR=VAL, sets VAR to VAL. 12 | EOF 13 | } 14 | 15 | . @CONF_PATH@/backend/runit.conf 16 | 17 | if [ $# -eq 0 ] || [ "$1" = "-h" ]; then 18 | usage 19 | exit 0 20 | fi 21 | 22 | for var; do 23 | case "$var" in 24 | *=*) 25 | eval echo "${var#*=}" > "$service_env_dir/${var%%=*}" 26 | ;; 27 | *) 28 | eval echo '$'"$var" > "$service_env_dir/$var" 29 | ;; 30 | esac 31 | done 32 | -------------------------------------------------------------------------------- /data/dinit/turnstiled: -------------------------------------------------------------------------------- 1 | # turnstiled service 2 | 3 | type = process 4 | before = login.target 5 | depends-on = local.target 6 | command = /usr/bin/turnstiled 7 | logfile = /var/log/turnstiled.log 8 | -------------------------------------------------------------------------------- /data/pam/turnstiled: -------------------------------------------------------------------------------- 1 | auth sufficient pam_rootok.so 2 | session optional pam_keyinit.so force revoke 3 | session optional pam_umask.so usergroups umask=022 4 | -session optional pam_elogind.so 5 | session required pam_turnstile.so turnstiled 6 | session required pam_limits.so 7 | -------------------------------------------------------------------------------- /include/turnstile.h: -------------------------------------------------------------------------------- 1 | /* @file turnstile.h 2 | * 3 | * @brief The libturnstile public API 4 | * 5 | * This is the public API of libturnstile, an abstraction library for 6 | * session tracking. 7 | * 8 | * The API is not safe to access from multiple threads. Use a lock if 9 | * you wish to do so. Using multiple turnstiles within a process is 10 | * permitted, and they can be used independently without a lock. Using 11 | * global APIs without a turnstile object does not require locking. 12 | * 13 | * @copyright See the attached COPYING.md for more information. 14 | */ 15 | 16 | #ifndef TURNSTILE_H 17 | #define TURNSTILE_H 18 | 19 | #if defined(__GNUC__) && (__GNUC__ >= 4) 20 | # define TURNSTILE_API __attribute__((visibility("default"))) 21 | #else 22 | # define TURNSTILE_API 23 | #endif 24 | 25 | #ifdef __cplusplus 26 | extern "C" { 27 | #endif 28 | 29 | /** @brief The turnstile. 30 | * 31 | * The turnstile is a handle hich contains all the client-local session 32 | * tracking state. Some APIs require a connected turnstile, while some 33 | * allow dual operation (passing NULL is allowed). 34 | * 35 | * APIs in connection mode need an event/dispatch loop and receive data 36 | * from a connected peer. Global APIs, on the other hand, rely on publicly 37 | * available out-of-process data, and thus do not require any further state, 38 | * connection, or a loop. 39 | */ 40 | typedef struct turnstile turnstile; 41 | 42 | typedef enum turnstile_event { 43 | TURNSTILE_EVENT_LOGIN_NEW = 1, 44 | TURNSTILE_EVENT_LOGIN_REMOVED, 45 | TURNSTILE_EVENT_LOGIN_CHANGED, 46 | TURNSTILE_EVENT_SESSION_NEW, 47 | TURNSTILE_EVENT_SESSION_REMOVED, 48 | TURNSTILE_EVENT_SESSION_CHANGED, 49 | } turnstile_event; 50 | 51 | /** @brief The turnstile event callback. 52 | * 53 | * A callback may be registered with turnstile_watch_events(). 54 | * The turnstile is passed, along with the event type, the id of the 55 | * affected object, and custom data provided during callback registration. 56 | * 57 | * For forward-compatible use, you should always filter for the specific 58 | * event type you require. 59 | */ 60 | typedef void (*turnstile_event_callback)(turnstile *ts, int event, unsigned long id, void *data); 61 | 62 | /** @brief Initialize a turnstile backend. 63 | * 64 | * Calling this will result in a backend being chosen for the lifetime of 65 | * the program. The available backends depend on what is compiled into the 66 | * library, and follow a priority order, with a fallback null backend being 67 | * always last. 68 | * 69 | * Calling this API with an already chosen backend does nothing. 70 | */ 71 | TURNSTILE_API void turnstile_init(void); 72 | 73 | /** @brief Create a new turnstile. 74 | * 75 | * Creating a new turnstile will connect to a backend. If no backend has 76 | * been chosen yet (via turnstile_init()), it will be chosen now. Note that 77 | * to actually use other APIs, a backend needs to be chosen, and they will 78 | * not choose it for you. 79 | * 80 | * Afterwards, you will want to either integrate it with your event loop 81 | * by getting a file descriptor with turnstile_get_fd(), polling it and 82 | * dispatching with turnstile_dispatch(), or if you don't have an event 83 | * loop, you can create your own dispatch loop (and don't need to poll). 84 | * 85 | * @return A turnstile, or NULL on error (errno set). 86 | */ 87 | TURNSTILE_API turnstile *turnstile_new(void); 88 | 89 | /** @brief Release the given turnstile. 90 | * 91 | * This will free the client-local state. Connection will be closed. 92 | * 93 | * @param ts The turnstile. 94 | * @return Zero on success, a negative value on error (errno set). 95 | */ 96 | TURNSTILE_API void turnstile_free(turnstile *ts); 97 | 98 | /** @brief Get a pollable file descriptor for the given turnstile. 99 | * 100 | * This can be used for integration into event loops. You should poll the 101 | * resulting file descriptor in your event loop and call turnstile_dispatch() 102 | * upon availability of data. 103 | * 104 | * The client does not own the file descriptor, so it does not need to close 105 | * it manually. 106 | * 107 | * @param ts The turnstile. 108 | * @return A pollable fd, or a negative value on error (errno set). 109 | */ 110 | TURNSTILE_API int turnstile_get_fd(turnstile *ts); 111 | 112 | /** @brief Dispatch the given turnstile. 113 | * 114 | * Upon reception of data (availability known through turnstile_get_fd() 115 | * descriptor), process the data. Registered callbacks and other things 116 | * will be triggered during the process. 117 | * 118 | * The timeout specifies how long to wait for data. Specifying the value of 0 119 | * means that no timeout will be given, -1 means potentially infinite timeout, 120 | * and a positive value is in milliseconds. Synchronous systems may want a 121 | * potentially infinite timeout (and no blocking) while async systems will 122 | * want to dispatch only what they have to avoid main loop stalls. 123 | * 124 | * @param ts The turnstile. 125 | * @param timeout The timeout. 126 | * @return A number of messages processed, or a negative value (errno set). 127 | */ 128 | TURNSTILE_API int turnstile_dispatch(turnstile *ts, int timeout); 129 | 130 | /** @brief Add a callback to watch for turnstile events. 131 | * 132 | * Upon an event (received through turnstile_dispatch()), the given callback 133 | * will be called. Events may include new logins, sessions, session state 134 | * changes, session drops, and so on. The details can be filtered by checking 135 | * the callback parameters. You can pass custom data with the extra parameter. 136 | * 137 | * @param ts The turnstile. 138 | * @param data Extra data to always pass to the callback. 139 | * @return Zero on success, a negative value on error (errno set). 140 | */ 141 | TURNSTILE_API int turnstile_watch_events(turnstile *ts, turnstile_event_callback cb, void *data); 142 | 143 | #ifdef __cplusplus 144 | } 145 | #endif 146 | 147 | #endif 148 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'turnstile', 3 | ['cpp', 'c'], 4 | version: '0.1.10', 5 | default_options: [ 6 | 'cpp_std=c++17', 'c_std=c11', 'warning_level=3', 7 | 'buildtype=debugoptimized', 8 | ], 9 | license: 'BSD-2-Clause' 10 | ) 11 | 12 | cpp = meson.get_compiler('cpp') 13 | 14 | pam_dep = dependency('pam', required: true) 15 | # could be openpam, in which case pam_misc is not present 16 | pam_misc_dep = dependency('pam_misc', required: false) 17 | rt_dep = cpp.find_library('rt', required: false) 18 | 19 | scdoc_dep = dependency( 20 | 'scdoc', version: '>=1.10', 21 | required: get_option('man'), 22 | native: true 23 | ) 24 | 25 | have_dinit = get_option('dinit').enabled() 26 | have_runit = get_option('runit').enabled() 27 | 28 | conf_data = configuration_data() 29 | conf_data.set_quoted('RUN_PATH', get_option('rundir')) 30 | conf_data.set_quoted('CONF_PATH', join_paths( 31 | get_option('prefix'), get_option('sysconfdir'), 'turnstile' 32 | )) 33 | conf_data.set10('MANAGE_RUNDIR', get_option('manage_rundir')) 34 | 35 | conf_data.set('HAVE_PAM_MISC', pam_misc_dep.found()) 36 | 37 | statepath = join_paths( 38 | get_option('prefix'), get_option('localstatedir'), 39 | get_option('statedir') 40 | ) 41 | lingerpath = join_paths(statepath, 'linger') 42 | 43 | conf_data.set_quoted('STATE_PATH', statepath) 44 | conf_data.set_quoted('LINGER_PATH', lingerpath) 45 | conf_data.set_quoted('LIBEXEC_PATH', join_paths( 46 | get_option('prefix'), get_option('libexecdir'), 'turnstile' 47 | )) 48 | 49 | configure_file(output: 'config.hh', configuration: conf_data) 50 | 51 | extra_inc = [include_directories('src')] 52 | 53 | add_project_arguments('-D_BSD_SOURCE', language: ['c', 'cpp']) 54 | 55 | if get_option('library').enabled() 56 | lib_sources = [ 57 | 'src/lib_api.c', 58 | 'src/lib_backend_none.c', 59 | 'src/lib_backend_turnstile.c', 60 | ] 61 | 62 | lib = library( 63 | 'turnstile', lib_sources, 64 | version: meson.project_version(), 65 | include_directories: extra_inc + [include_directories('include')], 66 | install: true, 67 | gnu_symbol_visibility: 'hidden', 68 | ) 69 | 70 | install_headers('include/turnstile.h') 71 | endif 72 | 73 | daemon_sources = [ 74 | 'src/turnstiled.cc', 75 | 'src/fs_utils.cc', 76 | 'src/cfg_utils.cc', 77 | 'src/exec_utils.cc', 78 | 'src/utils.cc', 79 | ] 80 | 81 | daemon = executable( 82 | 'turnstiled', daemon_sources, 83 | include_directories: extra_inc, 84 | install: true, 85 | dependencies: [rt_dep, pam_dep, pam_misc_dep], 86 | gnu_symbol_visibility: 'hidden' 87 | ) 88 | 89 | pam_moddir = get_option('pam_moddir') 90 | pamdir = get_option('pamdir') 91 | 92 | if pam_moddir == '' 93 | pam_moddir = join_paths( 94 | pam_dep.get_variable('libdir', default_value: get_option('libdir')), 95 | 'security' 96 | ) 97 | message('Detected PAM module directory:', pam_moddir) 98 | endif 99 | 100 | if pamdir == '' 101 | pamdir = join_paths(get_option('sysconfdir'), 'pam.d') 102 | endif 103 | 104 | pam_mod = shared_module( 105 | 'pam_turnstile', ['src/pam_turnstile.cc', 'src/utils.cc'], 106 | include_directories: extra_inc, 107 | install: true, 108 | install_dir: pam_moddir, 109 | name_prefix: '', 110 | dependencies: [pam_dep], 111 | gnu_symbol_visibility: 'hidden' 112 | ) 113 | 114 | if have_dinit 115 | install_data( 116 | 'data/dinit/turnstiled', 117 | install_dir: join_paths(get_option('sysconfdir'), 'dinit.d'), 118 | install_mode: 'rw-r--r--' 119 | ) 120 | endif 121 | 122 | install_data( 123 | 'data/pam/turnstiled', 124 | install_dir: pamdir, 125 | install_mode: 'rw-r--r--' 126 | ) 127 | 128 | # decide the default backend 129 | default_backend = get_option('default_backend') 130 | if default_backend == '' 131 | if have_dinit 132 | default_backend = 'dinit' 133 | elif have_runit 134 | default_backend = 'runit' 135 | else 136 | default_backend = 'none' 137 | endif 138 | endif 139 | 140 | uconf_data = configuration_data() 141 | 142 | uconf_data.set('RUN_PATH', get_option('rundir')) 143 | uconf_data.set('LINGER_PATH', lingerpath) 144 | uconf_data.set('DEFAULT_BACKEND', default_backend) 145 | 146 | if get_option('manage_rundir') 147 | uconf_data.set('MANAGE_RUNDIR', 'yes') 148 | else 149 | uconf_data.set('MANAGE_RUNDIR', 'no') 150 | endif 151 | 152 | configure_file( 153 | input: 'turnstiled.conf.in', 154 | output: 'turnstiled.conf', 155 | configuration: uconf_data, 156 | install: true, 157 | install_dir: join_paths(get_option('sysconfdir'), 'turnstile'), 158 | install_mode: 'rw-r--r--' 159 | ) 160 | 161 | cscd = configure_file( 162 | input: 'turnstiled.conf.5.scd.in', 163 | output: 'turnstiled.conf.5.scd', 164 | configuration: uconf_data 165 | ) 166 | 167 | fs = import('fs') 168 | 169 | if get_option('man') 170 | scdoc_prog = find_program( 171 | scdoc_dep.get_pkgconfig_variable('scdoc'), 172 | native: true 173 | ) 174 | sh = find_program('sh', native: true) 175 | mandir = get_option('mandir') 176 | 177 | man_files = [ 178 | 'src/turnstiled.8.scd', 179 | 'src/pam_turnstile.8.scd', 180 | cscd, 181 | ] 182 | 183 | foreach fobj: man_files 184 | filename = fs.name(fobj) 185 | output = fs.replace_suffix(filename, '') 186 | section = output.split('.')[-1] 187 | 188 | custom_target( 189 | output, 190 | input: fobj, 191 | capture: true, 192 | output: output, 193 | command: [ 194 | sh, '-c', '@0@ < @INPUT@'.format(scdoc_prog.path()) 195 | ], 196 | install: true, 197 | install_dir: '@0@/man@1@'.format(mandir, section) 198 | ) 199 | endforeach 200 | endif 201 | 202 | subdir('backend') 203 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('dinit', 2 | type: 'feature', value: 'enabled', 3 | description: 'Whether to install Dinit-related backend and data' 4 | ) 5 | 6 | option('runit', 7 | type: 'feature', value: 'disabled', 8 | description: 'Whether to install runit-related backend and data' 9 | ) 10 | 11 | option('default_backend', 12 | type: 'string', value: '', 13 | description: 'Override the default backend' 14 | ) 15 | 16 | option('rundir', 17 | type: 'string', value: '/run', 18 | description: 'Where the base directory will be located' 19 | ) 20 | 21 | option('statedir', 22 | type: 'string', value: 'lib/turnstiled', 23 | description: 'The state directory relative to localstatedir' 24 | ) 25 | 26 | option('pamdir', 27 | type: 'string', value: '', 28 | description: 'Override the path where PAM files go' 29 | ) 30 | 31 | option('pam_moddir', 32 | type: 'string', value: '', 33 | description: 'Where to install the PAM module (leave empty to autodetect)' 34 | ) 35 | 36 | option('manage_rundir', 37 | type: 'boolean', value: false, 38 | description: 'Whether to manage rundir by default' 39 | ) 40 | 41 | option('man', 42 | type: 'boolean', value: true, 43 | description: 'Whether to generate manpages' 44 | ) 45 | 46 | option('library', 47 | type: 'feature', value: 'disabled', 48 | description: 'Whether to build the library' 49 | ) 50 | -------------------------------------------------------------------------------- /src/cfg_utils.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "turnstiled.hh" 8 | 9 | static void read_bool(char const *name, char const *value, bool &val) { 10 | if (!std::strcmp(value, "yes")) { 11 | val = true; 12 | } else if (!std::strcmp(value, "no")) { 13 | val = false; 14 | } else { 15 | syslog( 16 | LOG_WARNING, 17 | "Invalid configuration value '%s' for '%s' (expected yes/no)", 18 | value, name 19 | ); 20 | } 21 | } 22 | 23 | void cfg_read(char const *cfgpath) { 24 | char buf[1024]; 25 | 26 | auto *f = std::fopen(cfgpath, "r"); 27 | if (!f) { 28 | syslog( 29 | LOG_NOTICE, "No configuration file '%s', using defaults", cfgpath 30 | ); 31 | return; 32 | } 33 | 34 | while (std::fgets(buf, sizeof(buf), f)) { 35 | auto slen = strlen(buf); 36 | /* ditch the rest of the line if needed */ 37 | if ((buf[slen - 1] != '\n')) { 38 | while (!std::feof(f)) { 39 | auto c = std::fgetc(f); 40 | if (c == '\n') { 41 | std::fgetc(f); 42 | break; 43 | } 44 | } 45 | } 46 | char *bufp = buf; 47 | /* drop trailing whitespace */ 48 | while (std::isspace(bufp[slen - 1])) { 49 | bufp[--slen] = '\0'; 50 | } 51 | /* drop leading whitespace */ 52 | while (std::isspace(*bufp)) { 53 | ++bufp; 54 | } 55 | /* comment or empty line */ 56 | if (!*bufp || (*bufp == '#')) { 57 | continue; 58 | } 59 | /* find the assignment */ 60 | char *ass = strchr(bufp, '='); 61 | /* invalid */ 62 | if (!ass || (ass == bufp)) { 63 | syslog(LOG_WARNING, "Malformed configuration line: %s", bufp); 64 | continue; 65 | } 66 | *ass = '\0'; 67 | /* find the name */ 68 | char *preass = (ass - 1); 69 | while (std::isspace(*preass)) { 70 | *preass-- = '\0'; 71 | } 72 | /* empty name */ 73 | if (preass == bufp) { 74 | syslog(LOG_WARNING, "Invalid configuration line name: %s", bufp); 75 | continue; 76 | } 77 | /* find the value */ 78 | while (std::isspace(*++ass)) { 79 | continue; 80 | } 81 | /* supported config lines */ 82 | if (!std::strcmp(bufp, "debug")) { 83 | read_bool("debug", ass, cdata->debug); 84 | } else if (!std::strcmp(bufp, "debug_stderr")) { 85 | read_bool("debug_stderr", ass, cdata->debug_stderr); 86 | } else if (!std::strcmp(bufp, "manage_rundir")) { 87 | read_bool("manage_rundir", ass, cdata->manage_rdir); 88 | } else if (!std::strcmp(bufp, "export_dbus_address")) { 89 | read_bool("export_dbus_address", ass, cdata->export_dbus); 90 | } else if (!std::strcmp(bufp, "root_session")) { 91 | read_bool("root_session", ass, cdata->root_session); 92 | } else if (!std::strcmp(bufp, "linger")) { 93 | if (!std::strcmp(ass, "maybe")) { 94 | cdata->linger = false; 95 | cdata->linger_never = false; 96 | } else { 97 | read_bool("linger", ass, cdata->linger); 98 | cdata->linger_never = !cdata->linger; 99 | } 100 | } else if (!std::strcmp(bufp, "backend")) { 101 | if (!std::strcmp(ass, "none")) { 102 | cdata->backend.clear(); 103 | cdata->disable = true; 104 | } else if (!std::strlen(ass)) { 105 | syslog( 106 | LOG_WARNING, 107 | "Invalid config value for '%s' (must be non-empty)", bufp 108 | ); 109 | } else { 110 | cdata->backend = ass; 111 | } 112 | } else if (!std::strcmp(bufp, "rundir_path")) { 113 | std::string rp = ass; 114 | if (!rp.empty() && ((rp.back() == '/') || (rp.front() != '/'))) { 115 | syslog( 116 | LOG_WARNING, 117 | "Invalid config value for '%s' (%s)", bufp, rp.data() 118 | ); 119 | } else { 120 | cdata->rdir_path = std::move(rp); 121 | } 122 | } else if (!std::strcmp(bufp, "login_timeout")) { 123 | char *endp = nullptr; 124 | auto tout = std::strtoul(ass, &endp, 10); 125 | if (*endp || (endp == ass)) { 126 | syslog( 127 | LOG_WARNING, 128 | "Invalid config value '%s' for '%s' (expected integer)", 129 | ass, bufp 130 | ); 131 | } else { 132 | cdata->login_timeout = time_t(tout); 133 | } 134 | } 135 | } 136 | } 137 | 138 | void cfg_expand_rundir( 139 | std::string &dest, char const *tmpl, unsigned int uid, unsigned int gid 140 | ) { 141 | char buf[32]; 142 | while (*tmpl) { 143 | auto mark = std::strchr(tmpl, '%'); 144 | if (!mark) { 145 | /* no formatting mark in the rest of the string, copy all */ 146 | dest += tmpl; 147 | break; 148 | } 149 | /* copy up to mark */ 150 | auto rlen = std::size_t(mark - tmpl); 151 | if (rlen) { 152 | dest.append(tmpl, rlen); 153 | } 154 | /* trailing % or %%, just copy it as is */ 155 | if (!mark[1] || ((mark[1] == '%') && !mark[2])) { 156 | dest.push_back('%'); 157 | break; 158 | } 159 | ++mark; 160 | unsigned int wid; 161 | switch (*mark) { 162 | case 'u': 163 | wid = uid; 164 | goto writenum; 165 | case 'g': 166 | wid = gid; 167 | writenum: 168 | std::snprintf(buf, sizeof(buf), "%u", wid); 169 | dest += buf; 170 | break; 171 | case '%': 172 | dest.push_back(*mark); 173 | break; 174 | default: 175 | dest.push_back('%'); 176 | dest.push_back(*mark); 177 | break; 178 | } 179 | tmpl = mark + 1; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/exec_utils.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "turnstiled.hh" 14 | 15 | #include 16 | #include 17 | #ifdef HAVE_PAM_MISC 18 | # include 19 | # define PAM_CONV_FUNC misc_conv 20 | #else 21 | # include 22 | # define PAM_CONV_FUNC openpam_ttyconv 23 | #endif 24 | 25 | static bool exec_backend( 26 | char const *backend, char const *arg, char const *data, 27 | unsigned int uid, unsigned int gid, pid_t &outpid 28 | ) { 29 | auto pid = fork(); 30 | if (pid < 0) { 31 | /* unrecoverable */ 32 | return false; 33 | } 34 | if (pid != 0) { 35 | /* parent process */ 36 | outpid = pid; 37 | return true; 38 | } 39 | if (!backend) { 40 | /* if service manager is not managed, simply succeed immediately */ 41 | exit(0); 42 | return true; 43 | } 44 | /* child process */ 45 | if (getuid() == 0) { 46 | if (setgid(gid) != 0) { 47 | perror("srv: failed to set gid"); 48 | exit(1); 49 | } 50 | if (setuid(uid) != 0) { 51 | perror("srv: failed to set uid"); 52 | exit(1); 53 | } 54 | } 55 | char buf[sizeof(LIBEXEC_PATH) + 128]; 56 | std::snprintf(buf, sizeof(buf), LIBEXEC_PATH "/%s", backend); 57 | execl(buf, buf, arg, data, nullptr); 58 | exit(1); 59 | return true; 60 | } 61 | 62 | bool srv_boot(login &lgn, char const *backend) { 63 | print_dbg("srv: startup (ready)"); 64 | if (!exec_backend( 65 | backend, "ready", lgn.srvstr.data(), lgn.uid, lgn.gid, lgn.start_pid 66 | )) { 67 | print_err("srv: fork failed (%s)", strerror(errno)); 68 | return false; 69 | } 70 | return true; 71 | } 72 | 73 | static bool dpam_setup_groups( 74 | pam_handle_t *pamh, char const *user, unsigned int gid 75 | ) { 76 | if (initgroups(user, gid) != 0) { 77 | perror("srv: failed to set supplementary groups"); 78 | return false; 79 | } 80 | auto pst = pam_setcred(pamh, PAM_ESTABLISH_CRED); 81 | if (pst != PAM_SUCCESS) { 82 | fprintf(stderr, "srv: pam_setcred: %s", pam_strerror(pamh, pst)); 83 | pam_end(pamh, pst); 84 | return false; 85 | } 86 | return true; 87 | } 88 | 89 | static pam_handle_t *dpam_begin(char const *user, unsigned int gid) { 90 | pam_conv cnv = { 91 | PAM_CONV_FUNC, 92 | nullptr 93 | }; 94 | pam_handle_t *pamh = nullptr; 95 | auto pst = pam_start(DPAM_SERVICE, user, &cnv, &pamh); 96 | if (pst != PAM_SUCCESS) { 97 | fprintf(stderr, "srv: pam_start: %s", pam_strerror(pamh, pst)); 98 | return nullptr; 99 | } 100 | if (!dpam_setup_groups(pamh, user, gid)) { 101 | return nullptr; 102 | } 103 | return pamh; 104 | } 105 | 106 | static void sanitize_limits() { 107 | struct rlimit l{0, 0}; 108 | 109 | print_dbg("srv: sanitize rlimits"); 110 | 111 | setrlimit(RLIMIT_NICE, &l); 112 | setrlimit(RLIMIT_RTPRIO, &l); 113 | 114 | l.rlim_cur = RLIM_INFINITY; 115 | l.rlim_max = RLIM_INFINITY; 116 | setrlimit(RLIMIT_FSIZE, &l); 117 | setrlimit(RLIMIT_AS, &l); 118 | 119 | getrlimit(RLIMIT_NOFILE, &l); 120 | if (l.rlim_cur != FD_SETSIZE) { 121 | l.rlim_cur = FD_SETSIZE; 122 | setrlimit(RLIMIT_NOFILE, &l); 123 | } 124 | } 125 | 126 | static bool dpam_open(pam_handle_t *pamh) { 127 | if (!pamh) { 128 | return false; 129 | } 130 | 131 | /* before opening session, do not rely on just PAM and sanitize a bit */ 132 | sanitize_limits(); 133 | 134 | print_dbg("srv: open pam session"); 135 | 136 | auto pst = pam_open_session(pamh, 0); 137 | if (pst != PAM_SUCCESS) { 138 | fprintf(stderr, "srv: pam_open_session: %s", pam_strerror(pamh, pst)); 139 | pam_setcred(pamh, PAM_DELETE_CRED | PAM_SILENT); 140 | pam_end(pamh, pst); 141 | return false; 142 | } 143 | 144 | return true; 145 | } 146 | 147 | static void dpam_finalize(pam_handle_t *pamh) { 148 | if (!pamh) { 149 | /* when not doing PAM, at least restore umask to user default, 150 | * otherwise the PAM configuration will do it (pam_umask.so) 151 | */ 152 | umask(022); 153 | return; 154 | } 155 | /* end with success */ 156 | pam_end(pamh, PAM_SUCCESS | PAM_DATA_SILENT); 157 | } 158 | 159 | static int sigpipe[2] = {-1, -1}; 160 | 161 | static void sig_handler(int sign) { 162 | write(sigpipe[1], &sign, sizeof(sign)); 163 | } 164 | 165 | static void fork_and_wait( 166 | pam_handle_t *pamh, char const *backend, 167 | unsigned int uid, unsigned int gid 168 | ) { 169 | int pst, status; 170 | int term_count = 0; 171 | struct pollfd pfd; 172 | struct sigaction sa{}; 173 | sigset_t mask; 174 | pid_t p; 175 | /* set up event loop bits, before fork for simpler cleanup */ 176 | if (pipe(sigpipe) < 0) { 177 | perror("srv: pipe failed"); 178 | goto fail; 179 | } 180 | pfd.fd = sigpipe[0]; 181 | pfd.events = POLLIN; 182 | pfd.revents = 0; 183 | /* fork */ 184 | p = fork(); 185 | if (p == 0) { 186 | /* child, return to exec */ 187 | close(sigpipe[0]); 188 | close(sigpipe[1]); 189 | return; 190 | } else if (p < 0) { 191 | perror("srv: fork failed"); 192 | goto fail; 193 | } 194 | /* ignore signals */ 195 | sigfillset(&mask); 196 | sigdelset(&mask, SIGTERM); 197 | sigdelset(&mask, SIGCHLD); 198 | sigprocmask(SIG_SETMASK, &mask, nullptr); 199 | /* set up handlers for non-ignored signals */ 200 | sa.sa_handler = sig_handler; 201 | sa.sa_flags = SA_RESTART; 202 | sigemptyset(&sa.sa_mask); 203 | sigaction(SIGCHLD, &sa, nullptr); 204 | sigaction(SIGTERM, &sa, nullptr); 205 | /* our own little event loop */ 206 | for (;;) { 207 | auto pret = poll(&pfd, 1, -1); 208 | if (pret < 0) { 209 | /* interrupted by signal */ 210 | if (errno == EINTR) { 211 | continue; 212 | } 213 | perror("srv: poll failed"); 214 | goto fail; 215 | } else if (pret == 0) { 216 | continue; 217 | } 218 | int sign; 219 | if (read(pfd.fd, &sign, sizeof(sign)) != sizeof(sign)) { 220 | perror("srv: signal read failed"); 221 | goto fail; 222 | } 223 | if (sign == SIGTERM) { 224 | char buf[32]; 225 | pid_t outp; 226 | int st; 227 | if ((term_count++ > 1) || !backend) { 228 | /* hard kill */ 229 | kill(p, SIGKILL); 230 | continue; 231 | } 232 | std::snprintf(buf, sizeof(buf), "%zu", size_t(p)); 233 | /* otherwise run the stop part */ 234 | if (!exec_backend(backend, "stop", buf, uid, gid, outp)) { 235 | /* failed? */ 236 | perror("srv: stop exec failed, fall back to TERM"); 237 | kill(p, SIGTERM); 238 | } 239 | /* wait for it to end */ 240 | do { 241 | pid_t w = waitpid(outp, &st, 0); 242 | if (w < 0) { 243 | if (errno == EINTR) { 244 | continue; 245 | } 246 | perror("srv: stop exec wait failed"); 247 | break; 248 | } 249 | } while (!WIFEXITED(st) && !WIFSIGNALED(st)); 250 | continue; 251 | } 252 | /* SIGCHLD */ 253 | int wpid; 254 | while ((wpid = waitpid(-1, &status, WNOHANG)) > 0) { 255 | if (wpid != p) { 256 | continue; 257 | } 258 | goto done; 259 | } 260 | } 261 | done: 262 | /* close session */ 263 | if (!pamh) { 264 | goto estatus; 265 | } 266 | pst = pam_close_session(pamh, 0); 267 | if (pst != PAM_SUCCESS) { 268 | fprintf(stderr, "srv: pam_close_session: %s", pam_strerror(pamh, pst)); 269 | pam_end(pamh, pst); 270 | goto fail; 271 | } 272 | /* finalize */ 273 | pam_setcred(pamh, PAM_DELETE_CRED); 274 | pam_end(pamh, PAM_SUCCESS); 275 | estatus: 276 | /* propagate exit status */ 277 | exit(WIFEXITED(status) ? WEXITSTATUS(status) : (WTERMSIG(status) + 128)); 278 | fail: 279 | exit(1); 280 | } 281 | 282 | /* dummy "service manager" child process with none backend */ 283 | static void srv_dummy(unsigned int uid) { 284 | /* block all signals except the ones we need to terminate */ 285 | sigset_t mask; 286 | sigfillset(&mask); 287 | /* kill/stop are ignored, but term is not */ 288 | sigdelset(&mask, SIGTERM); 289 | sigprocmask(SIG_SETMASK, &mask, nullptr); 290 | /* mark as ready */ 291 | char path[4096]; 292 | std::snprintf( 293 | path, sizeof(path), "%s/%s/%u/ready", RUN_PATH, SOCK_DIR, uid 294 | ); 295 | FILE *ready = std::fopen(path, "w"); 296 | if (!ready) { 297 | perror("srv: could not open readiness fifo"); 298 | exit(1); 299 | } 300 | std::fprintf(ready, "boop\n"); 301 | std::fclose(ready); 302 | /* this will sleep until a termination signal wakes it */ 303 | pause(); 304 | /* in which case just exit */ 305 | exit(0); 306 | } 307 | 308 | void srv_child(login &lgn, char const *backend, bool make_rundir) { 309 | pam_handle_t *pamh = nullptr; 310 | bool is_root = (getuid() == 0); 311 | /* create a new session */ 312 | if (setsid() < 0) { 313 | perror("srv: setsid failed"); 314 | } 315 | /* begin pam session setup */ 316 | if (is_root) { 317 | print_dbg("srv: establish pam"); 318 | pamh = dpam_begin(lgn.username.data(), lgn.gid); 319 | if (!dpam_open(pamh)) { 320 | return; 321 | } 322 | } 323 | /* make rundir if needed, we want to make it as late as possible, ideally 324 | * after the PAM session setup is already finalized (so that nothing gets 325 | * the idea to nuke it), but before we fork and drop privileges 326 | */ 327 | if (make_rundir) { 328 | print_dbg("srv: setup rundir for %u", lgn.uid); 329 | if (!rundir_make(lgn.rundir.data(), lgn.uid, lgn.gid)) { 330 | return; 331 | } 332 | } 333 | print_dbg("srv: forking for service manager exec"); 334 | /* handle the parent/child logic here 335 | * if we're forking, only child makes it past this func 336 | */ 337 | fork_and_wait(pamh, backend, lgn.uid, lgn.gid); 338 | /* drop privs */ 339 | if (is_root) { 340 | /* change identity */ 341 | if (setgid(lgn.gid) != 0) { 342 | perror("srv: failed to set gid"); 343 | return; 344 | } 345 | if (setuid(lgn.uid) != 0) { 346 | perror("srv: failed to set uid"); 347 | return; 348 | } 349 | } 350 | /* dummy service manager if requested */ 351 | if (!backend) { 352 | srv_dummy(lgn.uid); 353 | return; 354 | } 355 | /* change directory to home, fall back to / or error */ 356 | if ((chdir(lgn.homedir.data()) < 0) && (chdir("/") < 0)) { 357 | perror("srv: failed to change directory"); 358 | return; 359 | } 360 | /* set up service manager tempdir after we drop privileges */ 361 | char tdirn[38]; 362 | std::snprintf( 363 | tdirn, sizeof(tdirn), "srv.%lu", 364 | static_cast(getpid()) 365 | ); 366 | int tdirfd = dir_make_at(lgn.dirfd, tdirn, 0700); 367 | if (tdirfd < 0) { 368 | perror("srv: failed to create state dir"); 369 | return; 370 | } 371 | close(tdirfd); 372 | /* stringify the uid/gid */ 373 | char uidbuf[32], gidbuf[32]; 374 | std::snprintf(uidbuf, sizeof(uidbuf), "%u", lgn.uid); 375 | std::snprintf(gidbuf, sizeof(gidbuf), "%u", lgn.gid); 376 | /* build up env and args list */ 377 | std::vector execs{}; 378 | std::size_t argc = 0, nexec = 0; 379 | auto add_str = [&execs, &nexec](auto &&...s) { 380 | (execs.insert(execs.end(), s, s + std::strlen(s)), ...); 381 | execs.push_back('\0'); 382 | ++nexec; 383 | }; 384 | /* path to run script, argv starts here */ 385 | add_str(LIBEXEC_PATH, "/", backend); 386 | /* arg1: action */ 387 | add_str("run"); 388 | /* arg1: ready pipe */ 389 | add_str(RUN_PATH, "/", SOCK_DIR, "/", uidbuf, "/ready"); 390 | /* arg2: srvdir */ 391 | add_str(RUN_PATH, "/", SOCK_DIR, "/", uidbuf, "/", tdirn); 392 | /* arg3: confdir */ 393 | add_str(CONF_PATH, "/backend"); 394 | argc = nexec; 395 | /* pam env vars take preference */ 396 | bool have_env_shell = false, have_env_user = false, 397 | have_env_logname = false, have_env_home = false, 398 | have_env_uid = false, have_env_gid = false, 399 | have_env_path = false, have_env_rundir = false; 400 | /* get them and loop */ 401 | if (pamh) { 402 | /* this is a copy, but we exec so it's fine to leak */ 403 | char **penv = pam_getenvlist(pamh); 404 | while (penv && *penv) { 405 | /* ugly but it's not like putenv actually does anything else */ 406 | if (!strncmp(*penv, "SHELL=", 6)) { 407 | have_env_shell = true; 408 | } else if (!strncmp(*penv, "USER=", 5)) { 409 | have_env_user = true; 410 | } else if (!strncmp(*penv, "LOGNAME=", 8)) { 411 | have_env_logname = true; 412 | } else if (!strncmp(*penv, "HOME=", 5)) { 413 | have_env_home = true; 414 | } else if (!strncmp(*penv, "UID=", 4)) { 415 | have_env_uid = true; 416 | } else if (!strncmp(*penv, "GID=", 4)) { 417 | have_env_gid = true; 418 | } else if (!strncmp(*penv, "PATH=", 5)) { 419 | have_env_path = true; 420 | } else if (!strncmp(*penv, "XDG_RUNTIME_DIR=", 16)) { 421 | have_env_rundir = true; 422 | } 423 | add_str(*penv++); 424 | } 425 | } 426 | /* add our environment defaults if not already set */ 427 | if (!have_env_shell) { 428 | add_str("SHELL=", lgn.shell.data()); 429 | } 430 | if (!have_env_user) { 431 | add_str("USER=", lgn.username.data()); 432 | } 433 | if (!have_env_logname) { 434 | add_str("LOGNAME=", lgn.username.data()); 435 | } 436 | if (!have_env_home) { 437 | add_str("HOME=", lgn.homedir.data()); 438 | } 439 | if (!have_env_uid) { 440 | add_str("UID=", uidbuf); 441 | } 442 | if (!have_env_gid) { 443 | add_str("GID=", gidbuf); 444 | } 445 | if (!have_env_path) { 446 | add_str("PATH=" _PATH_DEFPATH); 447 | } 448 | if (!lgn.rundir.empty() && !have_env_rundir) { 449 | add_str("XDG_RUNTIME_DIR=", lgn.rundir.data()); 450 | } 451 | /* make up env and arg arrays */ 452 | std::vector argp{}; 453 | { 454 | char const *execsp = execs.data(); 455 | argp.reserve(nexec + 2); 456 | for (std::size_t i = 0; i < argc; ++i) { 457 | argp.push_back(execsp); 458 | execsp += std::strlen(execsp) + 1; 459 | } 460 | argp.push_back(nullptr); 461 | for (std::size_t i = argc; i < nexec; ++i) { 462 | argp.push_back(execsp); 463 | execsp += std::strlen(execsp) + 1; 464 | } 465 | argp.push_back(nullptr); 466 | } 467 | /* finish pam before execing */ 468 | dpam_finalize(pamh); 469 | /* fire */ 470 | auto *argv = const_cast(&argp[0]); 471 | execve(argv[0], argv, argv + argc + 1); 472 | } 473 | -------------------------------------------------------------------------------- /src/fs_utils.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "turnstiled.hh" 12 | 13 | int dir_make_at(int dfd, char const *dname, mode_t mode) { 14 | int sdfd = openat(dfd, dname, O_RDONLY | O_NOFOLLOW); 15 | struct stat st; 16 | int reterr = 0; 17 | int omask = umask(0); 18 | if (fstat(sdfd, &st) || !S_ISDIR(st.st_mode)) { 19 | close(sdfd); 20 | if (mkdirat(dfd, dname, mode)) { 21 | goto ret_err; 22 | } 23 | sdfd = openat(dfd, dname, O_RDONLY | O_NOFOLLOW); 24 | if ((sdfd < 0) || (fstat(sdfd, &st) < 0)) { 25 | goto ret_err; 26 | } 27 | if (!S_ISDIR(st.st_mode)) { 28 | reterr = ENOTDIR; 29 | goto ret_err; 30 | } 31 | } else { 32 | /* dir_clear_contents closes the descriptor, we need to keep it */ 33 | int nfd; 34 | if ((fchmod(sdfd, mode) < 0) || ((nfd = dup(sdfd)) < 0)) { 35 | goto ret_err; 36 | } 37 | if (!dir_clear_contents(nfd)) { 38 | reterr = ENOTEMPTY; 39 | goto ret_err; 40 | } 41 | } 42 | umask(omask); 43 | return sdfd; 44 | ret_err: 45 | umask(omask); 46 | if (sdfd >= 0) { 47 | close(sdfd); 48 | } 49 | if (reterr) { 50 | errno = reterr; 51 | } 52 | return -1; 53 | } 54 | 55 | bool rundir_make(char *rundir, unsigned int uid, unsigned int gid) { 56 | struct stat dstat; 57 | int bfd = open("/", O_RDONLY | O_NOFOLLOW); 58 | if (bfd < 0) { 59 | print_err("rundir: failed to open root (%s)", strerror(errno)); 60 | return false; 61 | } 62 | char *dirbase = rundir + 1; 63 | char *sl = std::strchr(dirbase, '/'); 64 | print_dbg("rundir: make directory %s", rundir); 65 | /* recursively create all parent paths */ 66 | mode_t omask = umask(022); 67 | while (sl) { 68 | *sl = '\0'; 69 | print_dbg("rundir: try make parent %s", rundir); 70 | int cfd = openat(bfd, dirbase, O_RDONLY | O_NOFOLLOW); 71 | if (cfd < 0) { 72 | if (mkdirat(bfd, dirbase, 0755) == 0) { 73 | cfd = openat(bfd, dirbase, O_RDONLY | O_NOFOLLOW); 74 | } 75 | } 76 | if (cfd < 0 || fstat(cfd, &dstat) < 0) { 77 | print_err( 78 | "rundir: failed to make parent %s (%s)", 79 | rundir, strerror(errno) 80 | ); 81 | close(bfd); 82 | close(cfd); 83 | umask(omask); 84 | return false; 85 | } 86 | if (!S_ISDIR(dstat.st_mode)) { 87 | print_err("rundir: non-directory encountered at %s", rundir); 88 | close(bfd); 89 | close(cfd); 90 | umask(omask); 91 | return false; 92 | } 93 | close(bfd); 94 | bfd = cfd; 95 | *sl = '/'; 96 | dirbase = sl + 1; 97 | sl = std::strchr(dirbase, '/'); 98 | } 99 | umask(omask); 100 | /* now create rundir or at least sanitize its perms */ 101 | if ( 102 | (fstatat(bfd, dirbase, &dstat, AT_SYMLINK_NOFOLLOW) < 0) || 103 | !S_ISDIR(dstat.st_mode) 104 | ) { 105 | if (mkdirat(bfd, dirbase, 0700) < 0) { 106 | print_err( 107 | "rundir: failed to make rundir %s (%s)", 108 | rundir, strerror(errno) 109 | ); 110 | close(bfd); 111 | return false; 112 | } 113 | } else if (fchmodat(bfd, dirbase, 0700, AT_SYMLINK_NOFOLLOW) < 0) { 114 | print_err("rundir: fchmodat failed for rundir (%s)", strerror(errno)); 115 | close(bfd); 116 | return false; 117 | } 118 | if (fchownat(bfd, dirbase, uid, gid, AT_SYMLINK_NOFOLLOW) < 0) { 119 | print_err("rundir: fchownat failed for rundir (%s)", strerror(errno)); 120 | close(bfd); 121 | return false; 122 | } 123 | close(bfd); 124 | return true; 125 | } 126 | 127 | void rundir_clear(char const *rundir) { 128 | struct stat dstat; 129 | print_dbg("rundir: clear directory %s", rundir); 130 | int dfd = open(rundir, O_RDONLY | O_NOFOLLOW); 131 | /* non-existent */ 132 | if (dfd < 0) { 133 | return; 134 | } 135 | /* an error? */ 136 | if (fstat(dfd, &dstat)) { 137 | print_dbg("rundir: could not stat %s (%s)", rundir, strerror(errno)); 138 | close(dfd); 139 | return; 140 | } 141 | /* not a directory */ 142 | if (!S_ISDIR(dstat.st_mode)) { 143 | print_dbg("rundir: %s is not a directory", rundir); 144 | close(dfd); 145 | return; 146 | } 147 | if (dir_clear_contents(dfd)) { 148 | /* was empty */ 149 | rmdir(rundir); 150 | } else { 151 | print_dbg("rundir: failed to clear contents of %s", rundir); 152 | } 153 | } 154 | 155 | bool dir_clear_contents(int dfd) { 156 | if (dfd < 0) { 157 | /* silently return if an invalid file descriptor */ 158 | return false; 159 | } 160 | DIR *d = fdopendir(dfd); 161 | if (!d) { 162 | print_err("dir_clear: fdopendir failed (%s)", strerror(errno)); 163 | close(dfd); 164 | return false; 165 | } 166 | 167 | unsigned char buf[offsetof(struct dirent, d_name) + NAME_MAX + 1]; 168 | unsigned char *bufp = buf; 169 | 170 | struct dirent *dentb = nullptr, *dent = nullptr; 171 | std::memcpy(&dentb, &bufp, sizeof(dent)); 172 | 173 | for (;;) { 174 | if (readdir_r(d, dentb, &dent) < 0) { 175 | print_err("dir_clear: readdir_r failed (%s)", strerror(errno)); 176 | closedir(d); 177 | return false; 178 | } 179 | if (!dent) { 180 | break; 181 | } 182 | if ( 183 | !std::strcmp(dent->d_name, ".") || 184 | !std::strcmp(dent->d_name, "..") 185 | ) { 186 | continue; 187 | } 188 | 189 | print_dbg("dir_clear: clear %s at %d", dent->d_name, dfd); 190 | int efd = openat(dfd, dent->d_name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK); 191 | int ufl = 0; 192 | 193 | if (efd < 0) { 194 | /* this may fail e.g. for invalid sockets, we don't care */ 195 | goto do_unlink; 196 | } 197 | 198 | struct stat st; 199 | if (fstat(efd, &st) < 0) { 200 | print_err("dir_clear: fstat failed (%s)", strerror(errno)); 201 | closedir(d); 202 | return false; 203 | } 204 | 205 | if (S_ISDIR(st.st_mode)) { 206 | if (!dir_clear_contents(efd)) { 207 | closedir(d); 208 | return false; 209 | } 210 | ufl = AT_REMOVEDIR; 211 | } else { 212 | close(efd); 213 | } 214 | 215 | do_unlink: 216 | if (unlinkat(dfd, dent->d_name, ufl) < 0) { 217 | print_err("dir_clear: unlinkat failed (%s)", strerror(errno)); 218 | closedir(d); 219 | return false; 220 | } 221 | } 222 | 223 | closedir(d); 224 | return true; 225 | } 226 | -------------------------------------------------------------------------------- /src/lib_api.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "lib_api.h" 10 | 11 | extern struct backend_api backend_api_turnstile; 12 | extern struct backend_api backend_api_none; 13 | 14 | /* the "current" backend is chosen once per client */ 15 | static struct backend_api *backend_api_current; 16 | 17 | /* THE API STUBS */ 18 | 19 | TURNSTILE_API void turnstile_init(void) { 20 | if (backend_api_current) { 21 | return; 22 | } 23 | if (backend_api_turnstile.active()) { 24 | backend_api_current = &backend_api_turnstile; 25 | return; 26 | } 27 | backend_api_current = &backend_api_none; 28 | } 29 | 30 | TURNSTILE_API turnstile *turnstile_new(void) { 31 | turnstile_init(); 32 | return backend_api_current->create(); 33 | } 34 | 35 | TURNSTILE_API void turnstile_free(turnstile *ts) { 36 | backend_api_current->destroy(ts); 37 | } 38 | 39 | TURNSTILE_API int turnstile_get_fd(turnstile *ts) { 40 | return backend_api_current->get_fd(ts); 41 | } 42 | 43 | TURNSTILE_API int turnstile_dispatch(turnstile *ts, int timeout) { 44 | return backend_api_current->dispatch(ts, timeout); 45 | } 46 | 47 | TURNSTILE_API int turnstile_watch_events( 48 | turnstile *ts, turnstile_event_callback cb, void *data 49 | ) { 50 | return backend_api_current->watch_events(ts, cb, data); 51 | } 52 | -------------------------------------------------------------------------------- /src/lib_api.h: -------------------------------------------------------------------------------- 1 | #ifndef LIB_API_HH 2 | #define LIB_API_HH 3 | 4 | #include 5 | 6 | #include 7 | 8 | struct backend_api { 9 | bool (*active)(void); 10 | turnstile *(*create)(void); 11 | void (*destroy)(turnstile *ts); 12 | 13 | int (*get_fd)(turnstile *ts); 14 | int (*dispatch)(turnstile *ts, int timeout); 15 | int (*watch_events)(turnstile *ts, turnstile_event_callback cb, void *data); 16 | }; 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /src/lib_backend_none.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "lib_api.h" 4 | 5 | typedef struct turnstile_none { 6 | int p; 7 | } turnstile_none; 8 | 9 | static bool backend_none_active(void) { 10 | return true; 11 | } 12 | 13 | static turnstile *backend_none_create(void) { 14 | turnstile_none *ret = malloc(sizeof(turnstile_none)); 15 | return (turnstile *)ret; 16 | } 17 | 18 | static void backend_none_destroy(turnstile *ts) { 19 | free(ts); 20 | } 21 | 22 | static int backend_none_get_fd(turnstile *ts) { 23 | (void)ts; 24 | return -1; 25 | } 26 | 27 | static int backend_none_dispatch(turnstile *ts, int timeout) { 28 | (void)ts; 29 | (void)timeout; 30 | return 0; 31 | } 32 | 33 | static int backend_none_watch_events( 34 | turnstile *ts, turnstile_event_callback cb, void *data 35 | ) { 36 | (void)ts; 37 | (void)cb; 38 | (void)data; 39 | return 0; 40 | } 41 | 42 | struct backend_api backend_api_none = { 43 | .active = backend_none_active, 44 | .create = backend_none_create, 45 | .destroy = backend_none_destroy, 46 | 47 | .get_fd = backend_none_get_fd, 48 | .dispatch = backend_none_dispatch, 49 | .watch_events = backend_none_watch_events, 50 | }; 51 | -------------------------------------------------------------------------------- /src/lib_backend_none.h: -------------------------------------------------------------------------------- 1 | #ifndef LIB_BACKEND_NONE_H 2 | #define LIB_BACKEND_NONE_H 3 | 4 | #include "lib_api.h" 5 | 6 | extern backend_api backend_api_none; 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /src/lib_backend_turnstile.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | // actually a C header too 10 | #include "protocol.hh" 11 | 12 | #include "lib_api.h" 13 | 14 | typedef struct turnstile_ts { 15 | int p_fd; 16 | } turnstile_ts; 17 | 18 | static int ts_connect(void) { 19 | struct sockaddr_un saddr; 20 | 21 | int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); 22 | if (sock < 0) { 23 | return -1; 24 | } 25 | 26 | memset(&saddr, 0, sizeof(saddr)); 27 | 28 | saddr.sun_family = AF_UNIX; 29 | memcpy(saddr.sun_path, DAEMON_SOCK, sizeof(DAEMON_SOCK)); 30 | 31 | if (connect(sock, (struct sockaddr const *)&saddr, sizeof(saddr)) < 0) { 32 | return -1; 33 | } 34 | 35 | return sock; 36 | } 37 | 38 | static bool nts_connect(turnstile_ts *ts) { 39 | return ((ts->p_fd = ts_connect()) >= 0); 40 | } 41 | 42 | static bool backend_ts_active(void) { 43 | int sock = ts_connect(); 44 | if (sock < 0) { 45 | return false; 46 | } 47 | close(sock); 48 | return true; 49 | } 50 | 51 | static void backend_ts_destroy(turnstile *ts) { 52 | turnstile_ts *nts = (turnstile_ts *)ts; 53 | if (nts->p_fd >= 0) { 54 | close(nts->p_fd); 55 | } 56 | free(ts); 57 | } 58 | 59 | static turnstile *backend_ts_create(void) { 60 | turnstile_ts *ret = malloc(sizeof(turnstile_ts)); 61 | if (!ret) { 62 | return NULL; 63 | } 64 | ret->p_fd = -1; 65 | 66 | if (!nts_connect(ret)) { 67 | int serrno = errno; 68 | backend_ts_destroy((turnstile *)ret); 69 | errno = serrno; 70 | return NULL; 71 | } 72 | 73 | return (turnstile *)ret; 74 | } 75 | 76 | static int backend_ts_get_fd(turnstile *ts) { 77 | return ((turnstile_ts *)ts)->p_fd; 78 | } 79 | 80 | static int backend_ts_dispatch(turnstile *ts, int timeout) { 81 | (void)ts; 82 | (void)timeout; 83 | return 0; 84 | } 85 | 86 | static int backend_ts_watch_events( 87 | turnstile *ts, turnstile_event_callback cb, void *data 88 | ) { 89 | (void)ts; 90 | (void)cb; 91 | (void)data; 92 | return 0; 93 | } 94 | 95 | struct backend_api backend_api_turnstile = { 96 | .active = backend_ts_active, 97 | .create = backend_ts_create, 98 | .destroy = backend_ts_destroy, 99 | 100 | .get_fd = backend_ts_get_fd, 101 | .dispatch = backend_ts_dispatch, 102 | .watch_events = backend_ts_watch_events, 103 | }; 104 | -------------------------------------------------------------------------------- /src/pam_turnstile.8.scd: -------------------------------------------------------------------------------- 1 | pam_turnstile(8) 2 | 3 | # NAME 4 | 5 | pam\_turnstile - register user sessions in *turnstiled*(8) 6 | 7 | # SYNOPSIS 8 | 9 | pam\_turnstile.so 10 | 11 | # DESCRIPTION 12 | 13 | *pam\_turnstile* registers user sessions with the main daemon, 14 | which allows them to be tracked. 15 | 16 | It communicates with the daemon over its control socket. Upon login, 17 | it opens a connection to it, and this connection lasts for as long as 18 | the login lasts. 19 | 20 | By keeping track of the connections, the daemon can be aware of the 21 | full lifetime of the session. 22 | 23 | The login will only proceed in one of the following cases: 24 | 25 | - The daemon has replied with a success. 26 | - The daemon has replied with a failure. 27 | - The connection was closed. 28 | 29 | Upon success, the daemon will have already started all user services. 30 | If that is the case, it may also initialize some environment variables: 31 | 32 | . _$DBUS\_SESSION\_BUS\_ADDRESS_ is exported assuming 'RUNDIR/bus' exists 33 | and is a valid socket, where 'RUNDIR' is the runtime directory the 34 | daemon is potentially managing. The value of the environment variable 35 | becomes _unix:path=/path/to/bus_. 36 | . _$XDG\_RUNTIME\_DIR_ is exported if the daemon's _manage\_rundir_ is 37 | enabled in the configuration. 38 | 39 | Upon success, the module returns _PAM\_SUCCESS_. In any other case, the 40 | module returns _PAM\_SESSION\_ERR_. 41 | 42 | # OPTIONS 43 | 44 | The module takes no options. 45 | -------------------------------------------------------------------------------- /src/pam_turnstile.cc: -------------------------------------------------------------------------------- 1 | /* pam_turnstile: the client part of turnstiled 2 | * 3 | * it connects to its socket and requests logins/logouts, 4 | * communicating over a rudimentary protocol 5 | * 6 | * the PAM session opens a persistent connection, which also 7 | * takes care of tracking when a session needs ending on the 8 | * daemon side (once all connections are gone) 9 | * 10 | * Copyright 2021 q66 11 | * License: BSD-2-Clause 12 | */ 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include 30 | #include 31 | #include 32 | 33 | #include "protocol.hh" 34 | #include "utils.hh" 35 | 36 | #define PAMAPI __attribute__((visibility ("default"))) 37 | 38 | static void free_sock(pam_handle_t *, void *data, int) { 39 | int sock = *static_cast(data); 40 | if (sock != -1) { 41 | close(sock); 42 | } 43 | free(data); 44 | } 45 | 46 | static bool open_session( 47 | pam_handle_t *pamh, 48 | unsigned int uid, 49 | char const *service, 50 | char const *stype, 51 | char const *sclass, 52 | char const *sdesktop, 53 | char const *sseat, 54 | char const *tty, 55 | char const *display, 56 | char const *ruser, 57 | char const *rhost, 58 | unsigned long vtnr, 59 | bool remote, 60 | unsigned int &elen, 61 | char *&ebuf, 62 | bool debug 63 | ) { 64 | if (debug) { 65 | pam_syslog(pamh, LOG_DEBUG, "open session"); 66 | } 67 | 68 | int *sock = static_cast(std::malloc(sizeof(int))); 69 | if (!sock) { 70 | return false; 71 | } 72 | 73 | /* blocking socket and a simple protocol */ 74 | *sock = socket(AF_UNIX, SOCK_STREAM, 0); 75 | if (*sock == -1) { 76 | return false; 77 | } 78 | 79 | /* associate the socket with the session */ 80 | if (pam_set_data( 81 | pamh, "pam_turnstile_session", sock, free_sock 82 | ) != PAM_SUCCESS) { 83 | return false; 84 | } 85 | 86 | sockaddr_un saddr; 87 | std::memset(&saddr, 0, sizeof(saddr)); 88 | 89 | saddr.sun_family = AF_UNIX; 90 | std::memcpy(saddr.sun_path, DAEMON_SOCK, sizeof(DAEMON_SOCK)); 91 | 92 | auto send_full = [sock](void const *buf, std::size_t len) -> bool { 93 | auto *cbuf = static_cast(buf); 94 | while (len) { 95 | auto n = write(*sock, cbuf, len); 96 | if (n < 0) { 97 | if (errno == EINTR) { 98 | continue; 99 | } 100 | return false; 101 | } 102 | cbuf += n; 103 | len -= n; 104 | } 105 | return true; 106 | }; 107 | auto send_msg = [&send_full](unsigned char msg) -> bool { 108 | return send_full(&msg, sizeof(msg)); 109 | }; 110 | auto send_str = [&send_full](char const *str) -> bool { 111 | std::size_t slen = str ? strlen(str) : 0; 112 | if (!send_full(&slen, sizeof(slen))) { 113 | return false; 114 | } 115 | return send_full(str, slen); 116 | }; 117 | 118 | if (connect( 119 | *sock, reinterpret_cast(&saddr), sizeof(saddr) 120 | ) < 0) { 121 | goto err; 122 | } 123 | 124 | if (!send_msg(MSG_START)) { 125 | goto err; 126 | } 127 | /* send all the arguments */ 128 | if (!send_full(&uid, sizeof(uid))) { 129 | goto err; 130 | } 131 | if (!send_full(&vtnr, sizeof(vtnr))) { 132 | goto err; 133 | } 134 | if (!send_full(&remote, sizeof(remote))) { 135 | goto err; 136 | } 137 | if (!send_str(service)) { 138 | goto err; 139 | } 140 | if (!send_str(stype)) { 141 | goto err; 142 | } 143 | if (!send_str(sclass)) { 144 | goto err; 145 | } 146 | if (!send_str(sdesktop)) { 147 | goto err; 148 | } 149 | if (!send_str(sseat)) { 150 | goto err; 151 | } 152 | if (!send_str(tty)) { 153 | goto err; 154 | } 155 | if (!send_str(display)) { 156 | goto err; 157 | } 158 | if (!send_str(ruser)) { 159 | goto err; 160 | } 161 | if (!send_str(rhost)) { 162 | goto err; 163 | } 164 | 165 | /* main message loop */ 166 | { 167 | unsigned char msg; 168 | unsigned char state = 0; 169 | 170 | /* read an entire known-size buffer in one go */ 171 | auto recv_full = [sock](void *buf, size_t len) -> bool { 172 | auto *cbuf = static_cast(buf); 173 | while (len) { 174 | auto n = recv(*sock, cbuf, len, 0); 175 | if (n < 0) { 176 | if (errno == EINTR) { 177 | continue; 178 | } 179 | return false; 180 | } else if (n == 0) { 181 | /* eof; connection closed by peer */ 182 | return false; 183 | } 184 | cbuf += n; 185 | len -= n; 186 | } 187 | return true; 188 | }; 189 | 190 | for (;;) { 191 | if (!recv_full(&msg, sizeof(msg))) { 192 | goto err; 193 | } 194 | switch (state) { 195 | case 0: 196 | case MSG_OK_WAIT: 197 | /* if started, get the rundir back; else block 198 | * 199 | * if we previously waited and now got another message, 200 | * it means either an error or that the system is now 201 | * fully ready 202 | */ 203 | if (msg == MSG_OK_DONE) { 204 | state = msg; 205 | if (!send_msg(MSG_REQ_ENV)) { 206 | goto err; 207 | } 208 | continue; 209 | } else if ((state == 0) && (msg == MSG_OK_WAIT)) { 210 | state = msg; 211 | continue; 212 | } 213 | /* bad message */ 214 | goto err; 215 | case MSG_OK_DONE: { 216 | if (msg != MSG_ENV) { 217 | goto err; 218 | } 219 | /* after MSG_OK_DONE, we should receive the environment 220 | * length first; if zero, it means we are completely done 221 | */ 222 | if (!recv_full(&elen, sizeof(elen))) { 223 | goto err; 224 | } 225 | /* alloc the buffer */ 226 | if (elen) { 227 | ebuf = static_cast(std::malloc(elen)); 228 | if (!ebuf) { 229 | goto err; 230 | } 231 | /* followed by the environment block */ 232 | if (!recv_full(ebuf, elen)) { 233 | goto err; 234 | } 235 | } 236 | return true; 237 | } 238 | default: 239 | goto err; 240 | } 241 | } 242 | } 243 | 244 | return true; 245 | 246 | err: 247 | std::free(ebuf); 248 | close(*sock); 249 | *sock = -1; 250 | return false; 251 | } 252 | 253 | /* this may get used later for something */ 254 | static int open_session_turnstiled(pam_handle_t *pamh, bool debug) { 255 | if (debug) { 256 | pam_syslog(pamh, LOG_DEBUG, "pam_turnstile init session"); 257 | } 258 | return PAM_SUCCESS; 259 | } 260 | 261 | static unsigned long get_x_vtnr(char const *display) { 262 | /* get the server number, drop if non-local */ 263 | if (display[0] != ':') { 264 | return 0; 265 | } 266 | char *endp = nullptr; 267 | unsigned long xnum = std::strtoul(display + 1, &endp, 10); 268 | if (endp && *endp) { 269 | return 0; 270 | } 271 | 272 | int sock = socket(AF_UNIX, SOCK_STREAM, 0); 273 | if (sock < 0) { 274 | return 0; 275 | } 276 | 277 | sockaddr_un saddr; 278 | std::memset(&saddr, 0, sizeof(saddr)); 279 | 280 | saddr.sun_family = AF_UNIX; 281 | /* try abstract socket first, linux only but harmless */ 282 | std::snprintf( 283 | saddr.sun_path, sizeof(saddr.sun_path), "@/tmp/.X11-unix/X%lu", xnum 284 | ); 285 | 286 | auto *sa = reinterpret_cast(&saddr); 287 | if (connect(sock, sa, sizeof(saddr)) < 0) { 288 | /* try non-abstract socket */ 289 | std::memmove( 290 | saddr.sun_path, saddr.sun_path + 1, sizeof(saddr.sun_path ) - 1 291 | ); 292 | /* if that fails too, drop */ 293 | if (connect(sock, sa, sizeof(saddr)) < 0) { 294 | close(sock); 295 | return 0; 296 | } 297 | } 298 | 299 | /* the xserver PID */ 300 | pid_t xpid = -1; 301 | get_peer_cred(sock, nullptr, nullptr, &xpid); 302 | 303 | close(sock); 304 | 305 | if (xpid < 0) { 306 | return 0; 307 | } 308 | 309 | return get_pid_vtnr(xpid); 310 | } 311 | 312 | static void parse_args( 313 | pam_handle_t *pamh, int argc, char const **argv, bool &debug, bool &sess, 314 | char const **cl, char const **dtop, char const **type 315 | ) { 316 | for (int i = 0; i < argc; ++i) { 317 | /* is in-session invocation */ 318 | if (!std::strcmp(argv[i], DPAM_SERVICE)) { 319 | sess = true; 320 | continue; 321 | } 322 | /* debug */ 323 | if (!std::strcmp(argv[i], "debug")) { 324 | debug = true; 325 | continue; 326 | } 327 | /* provided class */ 328 | if (!std::strncmp(argv[i], "class=", 6)) { 329 | if (cl) { 330 | *cl = argv[i] + 6; 331 | } 332 | continue; 333 | } 334 | /* provided desktop */ 335 | if (!std::strncmp(argv[i], "desktop=", 8)) { 336 | if (dtop) { 337 | *dtop = argv[i] + 8; 338 | } 339 | continue; 340 | } 341 | /* provided type */ 342 | if (!std::strncmp(argv[i], "type=", 5)) { 343 | if (type) { 344 | *type = argv[i] + 5; 345 | } 346 | continue; 347 | } 348 | /* unknown */ 349 | pam_syslog(pamh, LOG_WARNING, "unknown parameter '%s'", argv[i]); 350 | } 351 | } 352 | 353 | static char const *getenv_pam(pam_handle_t *pamh, char const *key) { 354 | auto *v = pam_getenv(pamh, key); 355 | if (v && *v) { 356 | return v; 357 | } 358 | v = getenv(key); 359 | if (v && *v) { 360 | return v; 361 | } 362 | return nullptr; 363 | } 364 | 365 | extern "C" PAMAPI int pam_sm_open_session( 366 | pam_handle_t *pamh, int, int argc, char const **argv 367 | ) { 368 | /* optional args */ 369 | bool debug = false, in_sess = false; 370 | char const *pclass = nullptr; 371 | char const *pdesktop = nullptr; 372 | char const *ptype = nullptr; 373 | /* parse the args */ 374 | parse_args(pamh, argc, argv, debug, in_sess, &pclass, &pdesktop, &ptype); 375 | 376 | /* debug */ 377 | if (debug) { 378 | pam_syslog(pamh, LOG_DEBUG, "pam_turnstile init"); 379 | } 380 | 381 | /* dual purpose */ 382 | if (in_sess) { 383 | return open_session_turnstiled(pamh, debug); 384 | } 385 | 386 | /* obtain the user */ 387 | char const *puser = nullptr; 388 | if (pam_get_user(pamh, &puser, nullptr) != PAM_SUCCESS) { 389 | pam_syslog(pamh, LOG_ERR, "could not get PAM user"); 390 | return PAM_SESSION_ERR; 391 | } 392 | passwd *pwd = getpwnam(puser); 393 | if (!pwd) { 394 | pam_syslog(pamh, LOG_ERR, "getpwnam failed (%s)", strerror(errno)); 395 | return PAM_SESSION_ERR; 396 | } 397 | 398 | /* get some pam session data */ 399 | auto get_pamitem = [pamh](int itype, char const *name, char const **item) { 400 | void const *itemv = nullptr; 401 | auto r = pam_get_item(pamh, itype, &itemv); 402 | if ((r == PAM_SUCCESS) || (r == PAM_BAD_ITEM)) { 403 | if (itemv) { 404 | *item = static_cast(itemv); 405 | } 406 | return true; 407 | } 408 | pam_syslog( 409 | pamh, LOG_ERR, "could not get PAM item: %s (%s)", 410 | name, pam_strerror(pamh, r) 411 | ); 412 | return false; 413 | }; 414 | char const *service = nullptr; 415 | if (!get_pamitem(PAM_SERVICE, "PAM_SERVICE", &service)) { 416 | return PAM_SESSION_ERR; 417 | } 418 | char const *display = nullptr; 419 | if (!get_pamitem(PAM_XDISPLAY, "PAM_XDISPLAY", &display)) { 420 | return PAM_SESSION_ERR; 421 | } 422 | char const *tty = nullptr; 423 | if (!get_pamitem(PAM_TTY, "PAM_TTY", &tty)) { 424 | return PAM_SESSION_ERR; 425 | } 426 | char const *remote_user = nullptr; 427 | if (!get_pamitem(PAM_RUSER, "PAM_RUSER", &remote_user)) { 428 | return PAM_SESSION_ERR; 429 | } 430 | char const *remote_host = nullptr; 431 | if (!get_pamitem(PAM_RHOST, "PAM_RHOST", &remote_host)) { 432 | return PAM_SESSION_ERR; 433 | } 434 | 435 | /* try obtain from environment */ 436 | char const *xclass = getenv_pam(pamh, "XDG_SESSION_CLASS"); 437 | if (!xclass) { 438 | xclass = pclass; 439 | } 440 | char const *xdesktop = getenv_pam(pamh, "XDG_SESSION_DESKTOP"); 441 | if (!xdesktop) { 442 | xdesktop = pdesktop; 443 | } 444 | char const *xtype = getenv_pam(pamh, "XDG_SESSION_TYPE"); 445 | if (!xtype) { 446 | xtype = ptype; 447 | } 448 | char const *xseat = getenv_pam(pamh, "XDG_SEAT"); 449 | char const *xvtnr = getenv_pam(pamh, "XDG_VTNR"); 450 | 451 | /* this more or less mimics logind for compatibility */ 452 | if (tty) { 453 | if (std::strchr(tty, ':')) { 454 | /* X11 display */ 455 | if (!display || !*display) { 456 | display = tty; 457 | } 458 | tty = nullptr; 459 | } else if (!std::strcmp(tty, "cron")) { 460 | xtype = "unspecified"; 461 | xclass = "background"; 462 | tty = nullptr; 463 | } else if (!std::strcmp(tty, "ssh")) { 464 | xtype = "tty"; 465 | xclass = "user"; 466 | tty = nullptr; 467 | } else if (!std::strncmp(tty, "/dev/", 5)) { 468 | tty += 5; 469 | } 470 | } 471 | 472 | unsigned long vtnr = 0; 473 | if (xvtnr) { 474 | char *endp = nullptr; 475 | vtnr = std::strtoul(xvtnr, &endp, 10); 476 | if (endp && *endp) { 477 | vtnr = 0; 478 | } 479 | } 480 | 481 | /* get vtnr from X display if possible */ 482 | if (display && *display && !vtnr) { 483 | if (!xseat || !*xseat) { 484 | /* assign default seat for X sessions if not set */ 485 | xseat = "seat0"; 486 | } 487 | vtnr = get_x_vtnr(display); 488 | } 489 | 490 | /* get vtnr from tty number if possible */ 491 | if (tty && !std::strncmp(tty, "tty", 3) && !vtnr) { 492 | char *endp = nullptr; 493 | vtnr = strtoul(tty + 3, &endp, 10); 494 | if (endp && *endp) { 495 | /* tty != "ttyN" */ 496 | vtnr = 0; 497 | } 498 | if (vtnr && (!xseat || !*xseat)) { 499 | /* assign default seat for console sessions if not set */ 500 | xseat = "seat0"; 501 | } 502 | } 503 | 504 | /* other-seat sessions cannot have vtnr */ 505 | if (xseat && std::strcmp(xseat, "seat0") && vtnr) { 506 | vtnr = 0; 507 | } 508 | 509 | if (!xtype || !*xtype) { 510 | xtype = (display && *display) ? "x11" : ( 511 | (tty && *tty) ? "tty" : "unspecified" 512 | ); 513 | } 514 | if (!xclass || !*xclass) { 515 | xclass = !std::strcmp(xtype, "unspecified") ? "background" : "user"; 516 | } 517 | 518 | bool remote = false; 519 | if (remote_host && *remote_host) { 520 | char buf[32]; 521 | auto hlen = std::strlen(remote_host); 522 | if (hlen >= sizeof(buf)) { 523 | std::memcpy(buf, remote_host + hlen - sizeof(buf) + 1, sizeof(buf)); 524 | hlen = sizeof(buf) - 1; 525 | } else { 526 | std::memcpy(buf, remote_host, hlen + 1); 527 | } 528 | /* strip trailing dot */ 529 | if (buf[hlen - 1] == '.') { 530 | buf[hlen - 1] = '\0'; 531 | } 532 | char *rdot = std::strrchr(buf, '.'); 533 | if (rdot && !strcasecmp(rdot + 1, "localdomain")) { 534 | *rdot = '\0'; 535 | } 536 | if (!strcasecmp(buf, "localhost")) { 537 | remote = true; 538 | } else { 539 | rdot = std::strrchr(buf, '.'); 540 | if (rdot && !strcasecmp(rdot + 1, "localhost")) { 541 | remote = true; 542 | } 543 | } 544 | } 545 | 546 | char *ebuf = nullptr; 547 | unsigned int elen = 0; 548 | 549 | if (!open_session( 550 | pamh, 551 | pwd->pw_uid, 552 | service, 553 | xtype, 554 | xclass, 555 | xdesktop, 556 | xseat, 557 | tty, 558 | display, 559 | remote_user, 560 | remote_host, 561 | vtnr, 562 | remote, 563 | /* output and misc parameters */ 564 | elen, 565 | ebuf, 566 | debug 567 | )) { 568 | return PAM_SESSION_ERR; 569 | } 570 | 571 | for (char *ecur = ebuf; elen;) { 572 | if (pam_putenv(pamh, ecur) != PAM_SUCCESS) { 573 | std::free(ebuf); 574 | return PAM_SESSION_ERR; 575 | } 576 | /* includes null terminator */ 577 | auto clen = std::strlen(ecur) + 1; 578 | if (elen >= clen) { 579 | ecur += clen; 580 | elen -= clen; 581 | } else { 582 | std::free(ebuf); 583 | return PAM_SESSION_ERR; 584 | } 585 | } 586 | std::free(ebuf); 587 | 588 | return PAM_SUCCESS; 589 | } 590 | 591 | extern "C" PAMAPI int pam_sm_close_session( 592 | pam_handle_t *pamh, int, int, char const ** 593 | ) { 594 | void const *data; 595 | /* there is nothing we can do here */ 596 | if (pam_get_data(pamh, "pam_turnstile_session", &data) != PAM_SUCCESS) { 597 | return PAM_SUCCESS; 598 | } 599 | int sock = *static_cast(data); 600 | if (sock < 0) { 601 | return PAM_SUCCESS; 602 | } 603 | /* close the session */ 604 | close(sock); 605 | return PAM_SUCCESS; 606 | } 607 | -------------------------------------------------------------------------------- /src/protocol.hh: -------------------------------------------------------------------------------- 1 | /* defines the simple protocol between the daemon and the PAM module 2 | * 3 | * Copyright 2021 q66 4 | * License: BSD-2-Clause 5 | */ 6 | 7 | #ifndef TURNSTILED_PROTOCOL_HH 8 | #define TURNSTILED_PROTOCOL_HH 9 | 10 | #include 11 | 12 | #include "config.hh" 13 | 14 | #ifndef RUN_PATH 15 | #error "No RUN_PATH is defined" 16 | #endif 17 | 18 | #define DPAM_SERVICE "turnstiled" 19 | 20 | #define SOCK_DIR DPAM_SERVICE 21 | #define DAEMON_SOCK RUN_PATH "/" SOCK_DIR "/control.sock" 22 | 23 | /* protocol messages 24 | * 25 | * this is a simple stream protocol; there are messages which fit within 26 | * a single byte, optionally followed by message-specific data bytes 27 | * 28 | * turnstiled is the server; the pam module is the client 29 | * 30 | * the client connects to DAEMON_SOCK 31 | * 32 | * from there, the following sequence happens: 33 | * 34 | * CLIENT: sends MSG_START, followed by uid (unsigned int), and enters a 35 | * message loop (state machine) 36 | * SERVER: if service manager for the user is already running, responds 37 | * with MSG_OK_DONE; else initiates startup and responds MSG_OK_WAIT 38 | * CLIENT: if MSG_OK_WAIT was received, waits for another message 39 | * SERVER: once service manager starts, MSG_OK_DONE is sent 40 | * CLIENT: sends MSG_REQ_ENV 41 | * SERVER: responds with MSG_ENV, followed by length of the environment 42 | * block (unsigned int) followed by the environment data, which 43 | * is a sequence of null-terminated strings 44 | * CLIENT: finishes startup, exports each variable in the received env 45 | * block and finalizes session 46 | */ 47 | 48 | /* byte-sized message identifiers */ 49 | enum { 50 | MSG_OK_WAIT = 0x1, /* login, wait */ 51 | MSG_OK_DONE, /* ready, proceed */ 52 | MSG_REQ_ENV, /* session environment request */ 53 | MSG_ENV, 54 | MSG_START, 55 | /* sent by server on errors */ 56 | MSG_ERR, 57 | }; 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /src/turnstiled.8.scd: -------------------------------------------------------------------------------- 1 | turnstiled(8) 2 | 3 | # NAME 4 | 5 | turnstiled - the main session management daemon 6 | 7 | # SYNOPSIS 8 | 9 | *turnstiled* [config_path] 10 | 11 | # DESCRIPTION 12 | 13 | *turnstiled* is a daemon that tracks user sessions and optionally spawns 14 | and manages service managers for them. 15 | 16 | For configuration, see *turnstiled.conf*(5). 17 | 18 | Upon user login, it spawns an instance of the chosen service manager for the 19 | user, while upon last logout, it shuts down this instance (unless configured 20 | to longer). 21 | 22 | User logins and logouts are communicated via *pam\_turnstile*(8). 23 | 24 | The daemon itself takes no options other than possibly a configuration 25 | file path as its sole argument. If not provided, the default path is 26 | used, typically _/etc/turnstile/turnstiled.conf_. 27 | 28 | # XDG\_RUNTIME\_DIR MANAGEMENT 29 | 30 | The daemon can also serve as the manager of the _$XDG\_RUNTIME\_DIR_ 31 | environment variable and directory. 32 | 33 | # ENVIRONMENT 34 | 35 | *TURNSTILED\_LINGER\_ENABLE\_FORCE* 36 | If set during daemon startup (to any value), enable lingering even if 37 | rundir management is disabled. This is primarily for people who want 38 | to use the linger functionality and have worked around the problem in 39 | their own rundir management system. Enabling this without having worked 40 | around the problem may lead to unfortunate consequences. 41 | 42 | Of course, lingering still has to be enabled in the configuration. 43 | -------------------------------------------------------------------------------- /src/turnstiled.cc: -------------------------------------------------------------------------------- 1 | /* turnstiled: handle incoming login requests and start (or 2 | * stop) service manager instances as necessary 3 | * 4 | * the daemon should never exit under "normal" circumstances 5 | * 6 | * Copyright 2021 q66 7 | * License: BSD-2-Clause 8 | */ 9 | 10 | #ifndef _GNU_SOURCE 11 | #define _GNU_SOURCE /* accept4 */ 12 | #endif 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | #include "turnstiled.hh" 34 | #include "utils.hh" 35 | 36 | #ifndef CONF_PATH 37 | #error "No CONF_PATH is defined" 38 | #endif 39 | 40 | /* we accept connections from non-root 41 | * 42 | * this relies on non-portable credentials checking, 43 | * so it must be implemented for every system separately 44 | * 45 | * it would be nice to get this implemented on other systems 46 | */ 47 | #define CSOCK_MODE 0666 48 | 49 | #define DEFAULT_CFG_PATH CONF_PATH "/turnstiled.conf" 50 | 51 | /* when stopping service manager, we first do a SIGTERM and set up this 52 | * timeout, if it fails to quit within that period, we issue a SIGKILL 53 | * and try this timeout again, after that it is considered unrecoverable 54 | */ 55 | static constexpr std::time_t kill_timeout = 60; 56 | 57 | /* global */ 58 | cfg_data *cdata = nullptr; 59 | 60 | /* the file descriptor for the base directory */ 61 | static int dirfd_base = -1; 62 | /* the file descriptor for the users directory */ 63 | static int dirfd_users = -1; 64 | /* the file descriptor for the sessions directory */ 65 | static int dirfd_sessions = -1; 66 | 67 | static bool write_udata(login const &lgn); 68 | static bool write_sdata(session const &sess); 69 | static void drop_udata(login const &lgn); 70 | static void drop_sdata(session const &sess); 71 | 72 | login::login() { 73 | timer_sev.sigev_notify = SIGEV_SIGNAL; 74 | timer_sev.sigev_signo = SIGALRM; 75 | timer_sev.sigev_value.sival_ptr = this; 76 | srvstr.reserve(256); 77 | } 78 | 79 | void login::remove_sdir() { 80 | char buf[32]; 81 | std::snprintf(buf, sizeof(buf), "%u", this->uid); 82 | unlinkat(dirfd_base, buf, AT_REMOVEDIR); 83 | /* just in case, we know this is a named pipe */ 84 | unlinkat(this->dirfd, "ready", 0); 85 | dir_clear_contents(this->dirfd); 86 | this->dirfd = -1; 87 | } 88 | 89 | bool login::arm_timer(std::time_t timeout) { 90 | if (timer_create(CLOCK_MONOTONIC, &timer_sev, &timer) < 0) { 91 | print_err("timer: timer_create failed (%s)", strerror(errno)); 92 | return false; 93 | } 94 | itimerspec tval{}; 95 | tval.it_value.tv_sec = timeout; 96 | if (timer_settime(timer, 0, &tval, nullptr) < 0) { 97 | print_err("timer: timer_settime failed (%s)", strerror(errno)); 98 | timer_delete(timer); 99 | return false; 100 | } 101 | timer_armed = true; 102 | return true; 103 | } 104 | 105 | void login::disarm_timer() { 106 | if (!timer_armed) { 107 | return; 108 | } 109 | timer_delete(timer); 110 | timer_armed = false; 111 | } 112 | 113 | static std::vector logins; 114 | 115 | /* file descriptors for poll */ 116 | static std::vector fds; 117 | /* connections pending a session */ 118 | static std::vector pending_sess; 119 | /* number of pipes we are polling on */ 120 | static std::size_t npipes = 0; 121 | /* control IPC socket */ 122 | static int ctl_sock; 123 | /* signal self-pipe */ 124 | static int sigpipe[2] = {-1, -1}; 125 | /* session counter, each session gets a new number (i.e. numbers never 126 | * get reused even if the session of that number dies); session numbers 127 | * are unique even across logins 128 | */ 129 | static unsigned long idbase = 0; 130 | 131 | /* start the service manager instance for a login */ 132 | static bool srv_start(login &lgn) { 133 | /* prepare some strings */ 134 | char uidbuf[32]; 135 | std::snprintf(uidbuf, sizeof(uidbuf), "%u", lgn.uid); 136 | /* mark as waiting */ 137 | lgn.srv_wait = true; 138 | /* set up login dir */ 139 | print_dbg("srv: create login dir for %u", lgn.uid); 140 | /* make the directory itself */ 141 | lgn.dirfd = dir_make_at(dirfd_base, uidbuf, 0700); 142 | if (lgn.dirfd < 0) { 143 | print_err( 144 | "srv: failed to make login dir for %u (%s)", 145 | lgn.uid, strerror(errno) 146 | ); 147 | return false; 148 | } 149 | /* ensure it's owned by the user */ 150 | if (fchownat( 151 | dirfd_base, uidbuf, lgn.uid, lgn.gid, AT_SYMLINK_NOFOLLOW 152 | ) || fcntl(lgn.dirfd, F_SETFD, FD_CLOEXEC)) { 153 | print_err( 154 | "srv: login dir setup failed for %u (%s)", 155 | lgn.uid, strerror(errno) 156 | ); 157 | lgn.remove_sdir(); 158 | return false; 159 | } 160 | print_dbg("srv: create readiness pipe"); 161 | unlinkat(lgn.dirfd, "ready", 0); 162 | if (mkfifoat(lgn.dirfd, "ready", 0700) < 0) { 163 | print_err("srv: failed to make ready pipe (%s)", strerror(errno)); 164 | return false; 165 | } 166 | /* ensure it's owned by user too, and open in nonblocking mode */ 167 | if (fchownat( 168 | lgn.dirfd, "ready", lgn.uid, lgn.gid, AT_SYMLINK_NOFOLLOW 169 | ) || ((lgn.userpipe = openat( 170 | lgn.dirfd, "ready", O_NONBLOCK | O_RDONLY 171 | )) < 0)) { 172 | print_err( 173 | "srv: failed to set up ready pipe (%s)", strerror(errno) 174 | ); 175 | unlinkat(lgn.dirfd, "ready", 0); 176 | lgn.remove_sdir(); 177 | return false; 178 | } 179 | /* set up the timer, issue SIGLARM when it fires */ 180 | print_dbg("srv: timer set"); 181 | if (cdata->login_timeout > 0) { 182 | if (!lgn.arm_timer(cdata->login_timeout)) { 183 | return false; 184 | } 185 | } else { 186 | print_dbg("srv: no timeout"); 187 | } 188 | /* launch service manager */ 189 | print_dbg("srv: launch"); 190 | auto pid = fork(); 191 | if (pid == 0) { 192 | /* reset signals from parent */ 193 | struct sigaction sa{}; 194 | sa.sa_handler = SIG_DFL; 195 | sa.sa_flags = SA_RESTART; 196 | sigemptyset(&sa.sa_mask); 197 | sigaction(SIGCHLD, &sa, nullptr); 198 | sigaction(SIGALRM, &sa, nullptr); 199 | sigaction(SIGTERM, &sa, nullptr); 200 | sigaction(SIGINT, &sa, nullptr); 201 | /* close some descriptors, these can be reused */ 202 | close(lgn.userpipe); 203 | close(dirfd_base); 204 | close(sigpipe[0]); 205 | close(sigpipe[1]); 206 | /* and run the login */ 207 | bool has_backend = !cdata->disable && ( 208 | (lgn.uid != 0) || cdata->root_session 209 | ); 210 | srv_child( 211 | lgn, 212 | has_backend ? cdata->backend.data() : nullptr, 213 | cdata->manage_rdir 214 | ); 215 | exit(1); 216 | } else if (pid < 0) { 217 | print_err("srv: fork failed (%s)", strerror(errno)); 218 | return false; 219 | } 220 | /* close the write end on our side */ 221 | lgn.srv_pending = false; 222 | lgn.srv_pid = pid; 223 | if (lgn.userpipe < 0) { 224 | /* disabled */ 225 | return srv_boot(lgn, nullptr); 226 | } 227 | /* otherwise queue the pipe */ 228 | lgn.pipe_queued = true; 229 | return true; 230 | } 231 | 232 | static session *get_session(int fd) { 233 | for (auto &lgn: logins) { 234 | for (auto &sess: lgn.sessions) { 235 | if (fd == sess.fd) { 236 | return &sess; 237 | } 238 | } 239 | } 240 | print_dbg("msg: no session for %d", fd); 241 | return nullptr; 242 | } 243 | 244 | static login *login_populate(unsigned int uid) { 245 | login *lgn = nullptr; 246 | for (auto &lgnr: logins) { 247 | if (lgnr.uid == uid) { 248 | if (!lgnr.repopulate) { 249 | print_dbg("msg: using existing login %u", uid); 250 | return &lgnr; 251 | } 252 | lgn = &lgnr; 253 | break; 254 | } 255 | } 256 | auto *pwd = getpwuid(uid); 257 | if (!pwd) { 258 | print_err("msg: failed to get pwd for %u (%s)", uid, strerror(errno)); 259 | return nullptr; 260 | } 261 | if (pwd->pw_dir[0] != '/') { 262 | print_err( 263 | "msg: homedir of %s (%u) is not absolute (%s)", pwd->pw_name, 264 | uid, pwd->pw_dir 265 | ); 266 | return nullptr; 267 | } 268 | if (lgn) { 269 | print_dbg("msg: repopulate login %u", pwd->pw_uid); 270 | } else { 271 | print_dbg("msg: init login %u", pwd->pw_uid); 272 | lgn = &logins.emplace_back(); 273 | } 274 | /* fill in initial login details */ 275 | lgn->uid = pwd->pw_uid; 276 | lgn->gid = pwd->pw_gid; 277 | lgn->username = pwd->pw_name; 278 | lgn->homedir = pwd->pw_dir; 279 | lgn->shell = pwd->pw_shell; 280 | lgn->rundir.clear(); 281 | /* somewhat heuristical */ 282 | lgn->rundir.reserve(cdata->rdir_path.size() + 8); 283 | cfg_expand_rundir(lgn->rundir, cdata->rdir_path.data(), lgn->uid, lgn->gid); 284 | lgn->manage_rdir = cdata->manage_rdir && !lgn->rundir.empty(); 285 | lgn->repopulate = false; 286 | return lgn; 287 | } 288 | 289 | static session *handle_session_new(int fd, unsigned int uid) { 290 | /* check for credential mismatch */ 291 | uid_t puid; 292 | pid_t lpid; 293 | if (!get_peer_cred(fd, &puid, nullptr, &lpid)) { 294 | print_dbg("msg: could not get peer credentials"); 295 | return nullptr; 296 | } 297 | if (puid != 0) { 298 | print_dbg("msg: can't set up session (permission denied)"); 299 | return nullptr; 300 | } 301 | /* acknowledge the login */ 302 | print_dbg("msg: welcome %u", uid); 303 | auto *lgn = login_populate(uid); 304 | if (!lgn) { 305 | return nullptr; 306 | } 307 | /* check the sessions */ 308 | for (auto &sess: lgn->sessions) { 309 | if (sess.fd == fd) { 310 | print_dbg("msg: already have session for %u/%d", lgn->uid, fd); 311 | return nullptr; 312 | } 313 | } 314 | print_dbg("msg: new session for %u/%d", lgn->uid, fd); 315 | /* create a new session */ 316 | auto &sess = lgn->sessions.emplace_back(); 317 | sess.fd = fd; 318 | sess.id = ++idbase; 319 | sess.lgn = lgn; 320 | sess.lpid = lpid; 321 | /* initial message */ 322 | sess.needed = 1; 323 | /* reply */ 324 | return &sess; 325 | } 326 | 327 | static bool write_udata(login const &lgn) { 328 | char uname[32], tmpname[32]; 329 | std::snprintf(tmpname, sizeof(tmpname), "%u.tmp", lgn.uid); 330 | std::snprintf(uname, sizeof(uname), "%u", lgn.uid); 331 | int omask = umask(0); 332 | int lgnfd = openat( 333 | dirfd_users, tmpname, O_CREAT | O_TRUNC | O_WRONLY, 0644 334 | ); 335 | if (lgnfd < 0) { 336 | print_err("msg: user tmpfile failed (%s)", strerror(errno)); 337 | umask(omask); 338 | return false; 339 | } 340 | umask(omask); 341 | auto *lgnf = fdopen(lgnfd, "w"); 342 | if (!lgnf) { 343 | print_err("msg: user fdopen failed (%s)", strerror(errno)); 344 | close(lgnfd); 345 | return false; 346 | } 347 | std::fprintf( 348 | lgnf, 349 | "NAME=%s\n" 350 | "RUNTIME=%s\n", 351 | lgn.username.data(), 352 | lgn.rundir.data() 353 | ); 354 | std::fprintf(lgnf, "SESSIONS="); 355 | bool first = true; 356 | for (auto &s: lgn.sessions) { 357 | if (!first) { 358 | std::fprintf(lgnf, " "); 359 | } 360 | std::fprintf(lgnf, "%lu", s.id); 361 | first = false; 362 | } 363 | std::fprintf(lgnf, "\nSEATS="); 364 | first = true; 365 | for (auto &s: lgn.sessions) { 366 | if (!first) { 367 | std::fprintf(lgnf, " "); 368 | } 369 | if (s.s_seat.empty()) { 370 | continue; 371 | } 372 | std::fprintf(lgnf, "%s", s.s_seat.data()); 373 | first = false; 374 | } 375 | std::fprintf(lgnf, "\n"); 376 | /* done writing */ 377 | std::fclose(lgnf); 378 | /* now rename to real file */ 379 | if (renameat(dirfd_users, tmpname, dirfd_users, uname) < 0) { 380 | print_err("msg: user renameat failed (%s)", strerror(errno)); 381 | unlinkat(dirfd_users, tmpname, 0); 382 | return false; 383 | } 384 | return true; 385 | } 386 | 387 | static bool write_sdata(session const &sess) { 388 | char sessname[32], tmpname[32]; 389 | std::snprintf(tmpname, sizeof(tmpname), "%lu.tmp", sess.id); 390 | std::snprintf(sessname, sizeof(sessname), "%lu", sess.id); 391 | auto &lgn = *sess.lgn; 392 | int omask = umask(0); 393 | int sessfd = openat( 394 | dirfd_sessions, tmpname, O_CREAT | O_TRUNC | O_WRONLY, 0644 395 | ); 396 | if (sessfd < 0) { 397 | print_err("msg: session tmpfile failed (%s)", strerror(errno)); 398 | umask(omask); 399 | return false; 400 | } 401 | umask(omask); 402 | auto *sessf = fdopen(sessfd, "w"); 403 | if (!sessf) { 404 | print_err("msg: session fdopen failed (%s)", strerror(errno)); 405 | close(sessfd); 406 | return false; 407 | } 408 | /* now write all the session data */ 409 | std::fprintf( 410 | sessf, 411 | "UID=%u\n" 412 | "USER=%s\n", 413 | lgn.uid, 414 | lgn.username.data() 415 | ); 416 | if (sess.vtnr) { 417 | std::fprintf(sessf, "IS_DISPLAY=1\n"); 418 | } 419 | std::fprintf(sessf, "REMOTE=%d\n", int(sess.remote)); 420 | std::fprintf(sessf, "TYPE=%s\n", sess.s_type.data()); 421 | std::fprintf(sessf, "ORIGINAL_TYPE=%s\n", sess.s_type.data()); 422 | std::fprintf(sessf, "CLASS=%s\n", sess.s_class.data()); 423 | if (!sess.s_seat.empty()) { 424 | std::fprintf(sessf, "SEAT=%s\n", sess.s_seat.data()); 425 | } 426 | if (!sess.s_tty.empty()) { 427 | std::fprintf(sessf, "TTY=%s\n", sess.s_tty.data()); 428 | } 429 | if (!sess.s_service.empty()) { 430 | std::fprintf(sessf, "SERVICE=%s\n", sess.s_service.data()); 431 | } 432 | if (sess.vtnr) { 433 | std::fprintf(sessf, "VTNR=%lu\n", sess.vtnr); 434 | } 435 | std::fprintf(sessf, "LEADER=%ld\n", long(sess.lpid)); 436 | /* done writing */ 437 | std::fclose(sessf); 438 | /* now rename to real file */ 439 | if (renameat(dirfd_sessions, tmpname, dirfd_sessions, sessname) < 0) { 440 | print_err("msg: session renameat failed (%s)", strerror(errno)); 441 | unlinkat(dirfd_sessions, tmpname, 0); 442 | return false; 443 | } 444 | return write_udata(lgn); 445 | } 446 | 447 | static void drop_udata(login const &lgn) { 448 | char lgname[64]; 449 | std::snprintf(lgname, sizeof(lgname), "%u", lgn.uid); 450 | unlinkat(dirfd_users, lgname, 0); 451 | } 452 | 453 | static void drop_sdata(session const &sess) { 454 | char sessname[64]; 455 | std::snprintf(sessname, sizeof(sessname), "%lu", sess.id); 456 | unlinkat(dirfd_sessions, sessname, 0); 457 | } 458 | 459 | static bool sock_block(int fd, short events) { 460 | if (errno == EINTR) { 461 | return true; 462 | } else if ((errno != EAGAIN) && (errno != EWOULDBLOCK)) { 463 | return false; 464 | } 465 | /* re-poll */ 466 | struct pollfd pfd; 467 | pfd.fd = fd; 468 | pfd.events = events; 469 | pfd.revents = 0; 470 | for (;;) { 471 | auto pret = poll(&pfd, 1, -1); 472 | if (pret < 0) { 473 | if (errno == EINTR) { 474 | continue; 475 | } 476 | return false; 477 | } else if (pret == 0) { 478 | continue; 479 | } 480 | break; 481 | } 482 | return true; 483 | } 484 | 485 | static bool send_full(int fd, void const *buf, size_t len) { 486 | auto *cbuf = static_cast(buf); 487 | while (len) { 488 | auto ret = send(fd, cbuf, len, 0); 489 | if (ret < 0) { 490 | if (sock_block(fd, POLLOUT)) { 491 | continue; 492 | } 493 | print_err("msg: send failed (%s)", strerror(errno)); 494 | return false; 495 | } 496 | cbuf += ret; 497 | len -= ret; 498 | } 499 | return true; 500 | } 501 | 502 | static bool send_msg(int fd, unsigned char msg) { 503 | if (!send_full(fd, &msg, sizeof(msg))) { 504 | return false; 505 | } 506 | return (msg != MSG_ERR); 507 | } 508 | 509 | static bool recv_val(int fd, void *buf, size_t sz) { 510 | auto ret = recv(fd, buf, sz, 0); 511 | if (ret < 0) { 512 | if (errno == EINTR) { 513 | return recv_val(fd, buf, sz); 514 | } 515 | print_err("msg: recv failed (%s)", strerror(errno)); 516 | } 517 | if (size_t(ret) != sz) { 518 | print_err("msg: partial recv despite peek"); 519 | return false; 520 | } 521 | return true; 522 | } 523 | 524 | static bool recv_str( 525 | session &sess, std::string &outs, unsigned int minlen, unsigned int maxlen 526 | ) { 527 | char buf[1024]; 528 | if (!sess.str_left) { 529 | print_dbg("msg: str start"); 530 | outs.clear(); 531 | size_t slen; 532 | if (!recv_val(sess.fd, &slen, sizeof(slen))) { 533 | return false; 534 | } 535 | if ((slen < minlen) || (slen > maxlen)) { 536 | print_err("msg: invalid string length"); 537 | return false; 538 | } 539 | sess.str_left = slen; 540 | /* we are awaiting string, which may come in arbitrary chunks */ 541 | sess.needed = 0; 542 | return true; 543 | } 544 | auto left = sess.str_left; 545 | if (left > sizeof(buf)) { 546 | left = sizeof(buf); 547 | } 548 | auto ret = recv(sess.fd, buf, left, 0); 549 | if (ret < 0) { 550 | if (errno == EINTR) { 551 | return recv_str(sess, outs, minlen, maxlen); 552 | } else if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { 553 | return true; 554 | } 555 | return false; 556 | } 557 | outs.append(buf, ret); 558 | sess.str_left -= ret; 559 | return true; 560 | } 561 | 562 | static bool handle_read(int fd) { 563 | int sess_needed; 564 | /* try get existing session */ 565 | auto *sess = get_session(fd); 566 | int *pidx = nullptr; 567 | /* no session: initialize one, expect initial data */ 568 | if (!sess) { 569 | sess_needed = sizeof(unsigned char); 570 | for (auto &pfd: pending_sess) { 571 | if (pfd == fd) { 572 | pidx = &pfd; 573 | sess_needed = sizeof(unsigned int); 574 | break; 575 | } 576 | } 577 | } else { 578 | sess_needed = sess->needed; 579 | } 580 | /* check if we have enough data, otherwise re-poll */ 581 | if (sess_needed) { 582 | int avail; 583 | auto ret = ioctl(fd, FIONREAD, &avail); 584 | if (ret < 0) { 585 | print_err("msg: ioctl failed (%s)", strerror(errno)); 586 | return false; 587 | } 588 | if (avail < sess_needed) { 589 | return true; 590 | } 591 | } 592 | /* must be an initial message */ 593 | if (!sess && !pidx) { 594 | unsigned char msg; 595 | if (!recv_val(fd, &msg, sizeof(msg))) { 596 | return false; 597 | } 598 | if (msg != MSG_START) { 599 | /* unexpected message */ 600 | print_err("msg: expected MSG_START, got %u", msg); 601 | return false; 602 | } 603 | pending_sess.push_back(fd); 604 | return true; 605 | } 606 | /* pending a uid */ 607 | if (!sess) { 608 | unsigned int uid; 609 | /* drop from pending */ 610 | pending_sess.erase(pending_sess.begin() + (pidx - &pending_sess[0])); 611 | /* now receive uid */ 612 | if (!recv_val(fd, &uid, sizeof(uid))) { 613 | return false; 614 | } 615 | sess = handle_session_new(fd, uid); 616 | if (!sess) { 617 | return send_msg(fd, MSG_ERR); 618 | } 619 | /* expect vtnr */ 620 | sess->needed = sizeof(unsigned long); 621 | return true; 622 | } 623 | /* handle the right section of handshake */ 624 | if (sess->handshake) { 625 | if (sess->pend_vtnr) { 626 | print_dbg("msg: get session vtnr"); 627 | if (!recv_val(fd, &sess->vtnr, sizeof(sess->vtnr))) { 628 | return false; 629 | } 630 | /* remote */ 631 | sess->needed = sizeof(bool); 632 | sess->pend_vtnr = 0; 633 | return true; 634 | } 635 | if (sess->pend_remote) { 636 | print_dbg("msg: get remote"); 637 | if (!recv_val(fd, &sess->remote, sizeof(sess->remote))) { 638 | return false; 639 | } 640 | /* service str */ 641 | sess->needed = sizeof(size_t); 642 | sess->pend_remote = 0; 643 | return true; 644 | } 645 | #define GET_STR(type, min, max, code) \ 646 | if (sess->pend_##type) { \ 647 | print_dbg("msg: get " #type); \ 648 | if (!recv_str(*sess, sess->s_##type, min, max)) { \ 649 | return false; \ 650 | } \ 651 | if (!sess->str_left) { \ 652 | sess->pend_##type = false; \ 653 | /* we are waiting for length of next string */ \ 654 | sess->needed = sizeof(size_t); \ 655 | print_dbg("msg: got \"%s\"", sess->s_##type.data()); \ 656 | code \ 657 | } \ 658 | return true; \ 659 | } 660 | GET_STR(service, 1, 64,) 661 | GET_STR(type, 1, 16,) 662 | GET_STR(class, 1, 16,) 663 | GET_STR(desktop, 0, 64,) 664 | GET_STR(seat, 0, 32,) 665 | GET_STR(tty, 0, 16,) 666 | GET_STR(display, 0, 16,) 667 | GET_STR(ruser, 0, 256,) 668 | GET_STR(rhost, 0, 256, goto handshake_finish;) 669 | #undef GET_STR 670 | /* should be unreachable */ 671 | print_dbg("msg: unreachable handshake"); 672 | return false; 673 | } 674 | handshake_finish: 675 | if (sess->handshake) { 676 | /* from this point the protocol is byte-sized messages only */ 677 | sess->needed = sizeof(unsigned char); 678 | sess->handshake = 0; 679 | /* finish startup */ 680 | if (!sess->lgn->srv_wait) { 681 | /* already started, reply with ok */ 682 | print_dbg("msg: done"); 683 | /* establish internal session file */ 684 | if (!write_sdata(*sess)) { 685 | return false; 686 | } 687 | if (!send_msg(fd, MSG_OK_DONE)) { 688 | return false; 689 | } 690 | } else { 691 | if (sess->lgn->srv_pid == -1) { 692 | if (sess->lgn->term_pid != -1) { 693 | /* still waiting for old service manager to die */ 694 | print_dbg("msg: still waiting for old srv term"); 695 | sess->lgn->srv_pending = true; 696 | } else { 697 | print_dbg("msg: start service manager"); 698 | if (!srv_start(*sess->lgn)) { 699 | return false; 700 | } 701 | /* establish internal session file */ 702 | if (!write_sdata(*sess)) { 703 | return false; 704 | } 705 | } 706 | } 707 | print_dbg("msg: wait"); 708 | return send_msg(fd, MSG_OK_WAIT); 709 | } 710 | return true; 711 | } 712 | /* get msg */ 713 | unsigned char msg; 714 | if (!recv_val(fd, &msg, sizeof(msg))) { 715 | return false; 716 | } 717 | if (msg != MSG_REQ_ENV) { 718 | print_err("msg: invalid message %u (%d)", msg, fd); 719 | return false; 720 | } 721 | print_dbg("msg: session environment request"); 722 | /* data message */ 723 | if (!send_msg(fd, MSG_ENV)) { 724 | return false; 725 | } 726 | unsigned int rlen = sess->lgn->rundir.size(); 727 | if (!rlen) { 728 | /* no rundir means no env, send a zero */ 729 | print_dbg("msg: no rundir, not sending env"); 730 | return send_full(fd, &rlen, sizeof(rlen)); 731 | } 732 | /* we have a rundir, compute an environment block */ 733 | unsigned int elen = 0; 734 | bool got_bus = false; 735 | /* declare some constants we need */ 736 | char const dpfx[] = "DBUS_SESSION_BUS_ADDRESS=unix:path="; 737 | char const rpfx[] = "XDG_RUNTIME_DIR="; 738 | char const dsfx[] = "/bus"; 739 | /* we can optionally export session bus address */ 740 | if (cdata->export_dbus) { 741 | /* check if the session bus socket exists */ 742 | struct stat sbuf; 743 | /* first get the rundir descriptor */ 744 | int rdirfd = open(sess->lgn->rundir.data(), O_RDONLY | O_NOFOLLOW); 745 | if (rdirfd >= 0) { 746 | if ( 747 | !fstatat(rdirfd, "bus", &sbuf, AT_SYMLINK_NOFOLLOW) && 748 | S_ISSOCK(sbuf.st_mode) 749 | ) { 750 | /* the bus socket exists */ 751 | got_bus = true; 752 | /* includes null terminator */ 753 | elen += sizeof(dpfx) + sizeof(dsfx) - 1; 754 | elen += rlen; 755 | } 756 | close(rdirfd); 757 | } 758 | } 759 | /* we can also export rundir if we're managing it */ 760 | if (cdata->manage_rdir) { 761 | /* includes null terminator */ 762 | elen += sizeof("XDG_RUNTIME_DIR="); 763 | elen += rlen; 764 | } 765 | /* send the total length */ 766 | print_dbg("msg: send len: %u", elen); 767 | if (!send_full(fd, &elen, sizeof(elen))) { 768 | return false; 769 | } 770 | auto &rdir = sess->lgn->rundir; 771 | /* now send rundir if we have it */ 772 | if (cdata->manage_rdir) { 773 | if (!send_full(fd, rpfx, sizeof(rpfx) - 1)) { 774 | return false; 775 | } 776 | /* includes null terminator */ 777 | if (!send_full(fd, rdir.data(), rdir.size() + 1)) { 778 | return false; 779 | } 780 | } 781 | /* now send bus if we have it */ 782 | if (got_bus) { 783 | if (!send_full(fd, dpfx, sizeof(dpfx) - 1)) { 784 | return false; 785 | } 786 | if (!send_full(fd, rdir.data(), rdir.size())) { 787 | return false; 788 | } 789 | /* includes null terminator */ 790 | if (!send_full(fd, dsfx, sizeof(dsfx))) { 791 | return false; 792 | } 793 | } 794 | print_dbg("msg: sent env, done"); 795 | /* we've sent all */ 796 | return true; 797 | } 798 | 799 | struct sig_data { 800 | int sign; 801 | void *datap; 802 | }; 803 | 804 | static void sig_handler(int sign) { 805 | sig_data d; 806 | d.sign = sign; 807 | d.datap = nullptr; 808 | write(sigpipe[1], &d, sizeof(d)); 809 | } 810 | 811 | static void timer_handler(int sign, siginfo_t *si, void *) { 812 | sig_data d; 813 | d.sign = sign; 814 | d.datap = si->si_value.sival_ptr; 815 | write(sigpipe[1], &d, sizeof(d)); 816 | } 817 | 818 | static bool check_linger(login const &lgn) { 819 | if (cdata->linger_never) { 820 | return false; 821 | } 822 | if (cdata->linger) { 823 | return true; 824 | } 825 | int dfd = open(LINGER_PATH, O_RDONLY); 826 | if (dfd < 0) { 827 | return false; 828 | } 829 | struct stat lbuf; 830 | bool ret = (!fstatat( 831 | dfd, lgn.username.data(), &lbuf, AT_SYMLINK_NOFOLLOW 832 | ) && S_ISREG(lbuf.st_mode)); 833 | close(dfd); 834 | return ret; 835 | } 836 | 837 | /* terminate given conn, but only if within login */ 838 | static bool conn_term_login(login &lgn, int conn) { 839 | for (auto cit = lgn.sessions.begin(); cit != lgn.sessions.end(); ++cit) { 840 | if (cit->fd != conn) { 841 | continue; 842 | } 843 | print_dbg("conn: close %d for login %u", conn, lgn.uid); 844 | drop_sdata(*cit); 845 | lgn.sessions.erase(cit); 846 | write_udata(lgn); 847 | /* empty now; shut down login */ 848 | if (lgn.sessions.empty() && !check_linger(lgn)) { 849 | print_dbg("srv: stop"); 850 | if (lgn.srv_pid != -1) { 851 | print_dbg("srv: term"); 852 | kill(lgn.srv_pid, SIGTERM); 853 | lgn.term_pid = lgn.srv_pid; 854 | /* just in case */ 855 | lgn.arm_timer(kill_timeout); 856 | } else { 857 | /* if no service manager, drop the dir early; otherwise 858 | * wait because we need to remove the boot service first 859 | */ 860 | lgn.remove_sdir(); 861 | drop_udata(lgn); 862 | } 863 | lgn.srv_pid = -1; 864 | lgn.start_pid = -1; 865 | lgn.srv_wait = true; 866 | } 867 | close(conn); 868 | return true; 869 | } 870 | return false; 871 | } 872 | 873 | static void conn_term(int conn) { 874 | for (auto &lgn: logins) { 875 | if (conn_term_login(lgn, conn)) { 876 | return; 877 | } 878 | } 879 | /* wasn't a session, may be pending */ 880 | for (auto it = pending_sess.begin(); it != pending_sess.end(); ++it) { 881 | if (*it == conn) { 882 | pending_sess.erase(it); 883 | break; 884 | } 885 | } 886 | /* in any case, close */ 887 | close(conn); 888 | } 889 | 890 | static bool sock_new(char const *path, int &sock, mode_t mode) { 891 | sock = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); 892 | if (sock < 0) { 893 | print_err("socket failed (%s)", strerror(errno)); 894 | return false; 895 | } 896 | 897 | /* set buffers */ 898 | int bufsz = 4096; 899 | 900 | if (setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufsz, sizeof(bufsz)) < 0) { 901 | print_err("setssockopt failed (%s)", strerror(errno)); 902 | } 903 | if (setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufsz, sizeof(bufsz)) < 0) { 904 | print_err("setssockopt failed (%s)", strerror(errno)); 905 | } 906 | 907 | print_dbg("socket: created %d for %s", sock, path); 908 | 909 | sockaddr_un un; 910 | std::memset(&un, 0, sizeof(un)); 911 | un.sun_family = AF_UNIX; 912 | 913 | auto plen = std::strlen(path); 914 | if (plen >= sizeof(un.sun_path)) { 915 | print_err("socket: path name %s too long", path); 916 | close(sock); 917 | return false; 918 | } 919 | 920 | std::memcpy(un.sun_path, path, plen + 1); 921 | /* no need to check this */ 922 | unlink(path); 923 | 924 | if (bind(sock, reinterpret_cast(&un), sizeof(un)) < 0) { 925 | print_err("bind failed (%s)", strerror(errno)); 926 | close(sock); 927 | return false; 928 | } 929 | print_dbg("socket: bound %d for %s", sock, path); 930 | 931 | if (chmod(path, mode) < 0) { 932 | print_err("chmod failed (%s)", strerror(errno)); 933 | goto fail; 934 | } 935 | print_dbg("socket: permissions set"); 936 | 937 | if (listen(sock, SOMAXCONN) < 0) { 938 | print_err("listen failed (%s)", strerror(errno)); 939 | goto fail; 940 | } 941 | print_dbg("socket: listen"); 942 | 943 | print_dbg("socket: done"); 944 | return true; 945 | 946 | fail: 947 | unlink(path); 948 | close(sock); 949 | return false; 950 | } 951 | 952 | static bool drop_login(login &lgn) { 953 | /* terminate all connections belonging to this login */ 954 | print_dbg("turnstiled: drop login %u", lgn.uid); 955 | for (std::size_t j = 2; j < fds.size(); ++j) { 956 | if (conn_term_login(lgn, fds[j].fd)) { 957 | fds[j].fd = -1; 958 | fds[j].revents = 0; 959 | } 960 | } 961 | /* mark the login to repopulate from passwd */ 962 | lgn.repopulate = true; 963 | /* this should never happen unless we have a bug */ 964 | if (!lgn.sessions.empty()) { 965 | print_err("turnstiled: sessions not empty, it should be"); 966 | /* unrecoverable */ 967 | return false; 968 | } 969 | return true; 970 | } 971 | 972 | static bool sig_handle_term() { 973 | print_dbg("turnstiled: term"); 974 | bool succ = true; 975 | /* close the control socket */ 976 | close(ctl_sock); 977 | /* drop logins */ 978 | for (auto &lgn: logins) { 979 | if (!drop_login(lgn)) { 980 | succ = false; 981 | } 982 | } 983 | /* shrink the descriptor list to just signal pipe */ 984 | fds.resize(1); 985 | return succ; 986 | } 987 | 988 | static bool sig_handle_alrm(void *data) { 989 | print_dbg("turnstiled: sigalrm"); 990 | auto &lgn = *static_cast(data); 991 | /* disarm the timer if armed */ 992 | if (lgn.timer_armed) { 993 | print_dbg("turnstiled: drop timer"); 994 | lgn.disarm_timer(); 995 | } else { 996 | print_dbg("turnstiled: spurious alarm, ignoring"); 997 | return true; 998 | } 999 | if (lgn.term_pid != -1) { 1000 | if (lgn.kill_tried) { 1001 | print_err( 1002 | "turnstiled: service manager process %ld refused to die", 1003 | static_cast(lgn.term_pid) 1004 | ); 1005 | return false; 1006 | } 1007 | /* waiting for service manager to die and it did not die, try again 1008 | * this will propagate as SIGKILL in the double-forked process 1009 | */ 1010 | kill(lgn.term_pid, SIGTERM); 1011 | lgn.kill_tried = true; 1012 | /* re-arm the timer, if that fails again, we give up */ 1013 | lgn.arm_timer(kill_timeout); 1014 | return true; 1015 | } 1016 | /* terminate all connections belonging to this login */ 1017 | return drop_login(lgn); 1018 | } 1019 | 1020 | /* this is called upon receiving a SIGCHLD 1021 | * 1022 | * can happen for 3 things: 1023 | * 1024 | * the service manager instance which is still supposed to be running, in 1025 | * which case we attempt to restart it (except if it never signaled readiness, 1026 | * in which case we give up, as we'd likely loop forever) 1027 | * 1028 | * the readiness job, which waits for the bootup to finish, and is run once 1029 | * the service manager has opened its control socket; in those cases we notify 1030 | * all pending connections and disarm the timeout (and mark the login ready) 1031 | * 1032 | * or the service manager instance which has stopped (due to logout typically), 1033 | * in which case we take care of removing the generated service directory and 1034 | * possibly clear the rundir (if managed) 1035 | */ 1036 | static bool srv_reaper(pid_t pid) { 1037 | print_dbg("srv: reap %u", (unsigned int)pid); 1038 | for (auto &lgn: logins) { 1039 | if (pid == lgn.srv_pid) { 1040 | lgn.srv_pid = -1; 1041 | lgn.start_pid = -1; /* we don't care anymore */ 1042 | lgn.disarm_timer(); 1043 | if (lgn.srv_wait) { 1044 | /* failed without ever having signaled readiness 1045 | * let the login proceed but indicate an error 1046 | */ 1047 | print_err("srv: died without notifying readiness"); 1048 | /* clear rundir if needed */ 1049 | if (lgn.manage_rdir) { 1050 | rundir_clear(lgn.rundir.data()); 1051 | lgn.manage_rdir = false; 1052 | } 1053 | return drop_login(lgn); 1054 | } 1055 | return srv_start(lgn); 1056 | } else if (pid == lgn.start_pid) { 1057 | /* reaping service startup jobs */ 1058 | print_dbg("srv: ready notification"); 1059 | for (auto &sess: lgn.sessions) { 1060 | send_msg(sess.fd, MSG_OK_DONE); 1061 | } 1062 | /* disarm an associated timer */ 1063 | print_dbg("srv: disarm timer"); 1064 | lgn.disarm_timer(); 1065 | lgn.start_pid = -1; 1066 | lgn.srv_wait = false; 1067 | } else if (pid == lgn.term_pid) { 1068 | /* if there was a timer on the login, safe to drop it now */ 1069 | lgn.disarm_timer(); 1070 | lgn.remove_sdir(); 1071 | /* clear rundir if needed */ 1072 | if (lgn.manage_rdir) { 1073 | rundir_clear(lgn.rundir.data()); 1074 | lgn.manage_rdir = false; 1075 | } 1076 | /* mark to repopulate if there are no sessions */ 1077 | if (lgn.sessions.empty()) { 1078 | drop_udata(lgn); 1079 | lgn.repopulate = true; 1080 | } 1081 | lgn.term_pid = -1; 1082 | lgn.kill_tried = false; 1083 | if (lgn.srv_pending) { 1084 | return srv_start(lgn); 1085 | } 1086 | } 1087 | } 1088 | return true; 1089 | } 1090 | 1091 | static bool sig_handle_chld() { 1092 | pid_t wpid; 1093 | int status; 1094 | print_dbg("turnstiled: sigchld"); 1095 | /* reap */ 1096 | while ((wpid = waitpid(-1, &status, WNOHANG)) > 0) { 1097 | /* deal with each pid here */ 1098 | if (!srv_reaper(wpid)) { 1099 | print_err( 1100 | "turnstiled: failed to restart service manager (%u)\n", 1101 | static_cast(wpid) 1102 | ); 1103 | /* this is an unrecoverable condition */ 1104 | return false; 1105 | } 1106 | } 1107 | return true; 1108 | } 1109 | 1110 | static bool fd_handle_pipe(std::size_t i) { 1111 | if (fds[i].revents == 0) { 1112 | return true; 1113 | } 1114 | /* find if this is a pipe */ 1115 | login *lgn = nullptr; 1116 | for (auto &lgnr: logins) { 1117 | if (fds[i].fd == lgnr.userpipe) { 1118 | lgn = &lgnr; 1119 | break; 1120 | } 1121 | } 1122 | if (!lgn) { 1123 | /* this should never happen */ 1124 | return false; 1125 | } 1126 | bool done = false; 1127 | if (fds[i].revents & POLLIN) { 1128 | /* read the string from the pipe */ 1129 | for (;;) { 1130 | char c; 1131 | if (read(fds[i].fd, &c, 1) != 1) { 1132 | break; 1133 | } 1134 | if ((c == '\0') || (lgn->srvstr.size() >= PATH_MAX)) { 1135 | /* done receiving */ 1136 | done = true; 1137 | break; 1138 | } 1139 | lgn->srvstr.push_back(c); 1140 | } 1141 | } 1142 | if (done || (fds[i].revents & POLLHUP)) { 1143 | print_dbg("pipe: close"); 1144 | /* kill the pipe, we don't need it anymore */ 1145 | close(lgn->userpipe); 1146 | lgn->userpipe = -1; 1147 | /* just in case */ 1148 | lgn->pipe_queued = false; 1149 | fds[i].fd = -1; 1150 | fds[i].revents = 0; 1151 | --npipes; 1152 | /* unlink the pipe */ 1153 | unlinkat(lgn->dirfd, "ready", 0); 1154 | print_dbg("pipe: gone"); 1155 | /* wait for the boot service to come up */ 1156 | if (!srv_boot(*lgn, cdata->backend.data())) { 1157 | /* this is an unrecoverable condition */ 1158 | return false; 1159 | } 1160 | /* reset the buffer for next time */ 1161 | lgn->srvstr.clear(); 1162 | } 1163 | return true; 1164 | } 1165 | 1166 | static bool fd_handle_conn(std::size_t i) { 1167 | if (fds[i].revents == 0) { 1168 | return true; 1169 | } 1170 | if (fds[i].revents & POLLHUP) { 1171 | print_dbg("conn: hup %d", fds[i].fd); 1172 | conn_term(fds[i].fd); 1173 | fds[i].fd = -1; 1174 | fds[i].revents = 0; 1175 | return true; 1176 | } 1177 | if (fds[i].revents & POLLIN) { 1178 | /* input on connection */ 1179 | try { 1180 | print_dbg("conn: read %d", fds[i].fd); 1181 | if (!handle_read(fds[i].fd)) { 1182 | goto read_fail; 1183 | } 1184 | } catch (std::bad_alloc const &) { 1185 | goto read_fail; 1186 | } 1187 | } 1188 | return true; 1189 | read_fail: 1190 | print_err("read: handler failed (terminate connection)"); 1191 | conn_term(fds[i].fd); 1192 | fds[i].fd = -1; 1193 | fds[i].revents = 0; 1194 | return true; 1195 | } 1196 | 1197 | static void sock_handle_conn() { 1198 | if (!fds[1].revents) { 1199 | return; 1200 | } 1201 | for (;;) { 1202 | auto afd = accept4( 1203 | fds[1].fd, nullptr, nullptr, SOCK_NONBLOCK | SOCK_CLOEXEC 1204 | ); 1205 | if (afd < 0) { 1206 | if (errno != EAGAIN) { 1207 | /* should not happen? disregard the connection */ 1208 | print_err("accept4 failed (%s)", strerror(errno)); 1209 | } 1210 | break; 1211 | } 1212 | auto &rfd = fds.emplace_back(); 1213 | rfd.fd = afd; 1214 | rfd.events = POLLIN | POLLHUP; 1215 | rfd.revents = 0; 1216 | print_dbg("conn: accepted %d for %d", afd, fds[1].fd); 1217 | } 1218 | } 1219 | 1220 | int main(int argc, char **argv) { 1221 | /* establish simple signal handler for sigchld */ 1222 | { 1223 | struct sigaction sa{}; 1224 | sa.sa_handler = sig_handler; 1225 | sa.sa_flags = SA_RESTART; 1226 | sigemptyset(&sa.sa_mask); 1227 | sigaction(SIGCHLD, &sa, nullptr); 1228 | sigaction(SIGTERM, &sa, nullptr); 1229 | sigaction(SIGINT, &sa, nullptr); 1230 | } 1231 | /* establish more complicated signal handler for timers */ 1232 | { 1233 | struct sigaction sa; 1234 | sa.sa_flags = SA_SIGINFO | SA_RESTART; 1235 | sa.sa_sigaction = timer_handler; 1236 | sigemptyset(&sa.sa_mask); 1237 | sigaction(SIGALRM, &sa, nullptr); 1238 | } 1239 | 1240 | /* prealloc a bunch of space */ 1241 | logins.reserve(16); 1242 | fds.reserve(64); 1243 | pending_sess.reserve(16); 1244 | 1245 | openlog("turnstiled", LOG_CONS | LOG_NDELAY, LOG_DAEMON); 1246 | 1247 | syslog(LOG_INFO, "Initializing turnstiled..."); 1248 | 1249 | /* initialize configuration structure */ 1250 | cfg_data cdata_val; 1251 | cdata = &cdata_val; 1252 | 1253 | if (argc >= 2) { 1254 | cfg_read(argv[1]); 1255 | } else { 1256 | cfg_read(DEFAULT_CFG_PATH); 1257 | } 1258 | 1259 | if (!cdata->manage_rdir && !std::getenv( 1260 | "TURNSTILED_LINGER_ENABLE_FORCE" 1261 | )) { 1262 | /* we don't want to linger when we are not in charge of the rundir, 1263 | * because services may be relying on it; we can never really delete 1264 | * the rundir when lingering, and something like elogind might 1265 | * 1266 | * for those who are aware of the consequences and have things handled 1267 | * on their own, they can start the daemon with the env variable 1268 | */ 1269 | cdata->linger_never = true; 1270 | } 1271 | 1272 | print_dbg("turnstiled: init signal fd"); 1273 | 1274 | { 1275 | struct stat pstat; 1276 | int dfd = open(RUN_PATH, O_RDONLY | O_NOFOLLOW); 1277 | /* ensure the base path exists and is a directory */ 1278 | if (fstat(dfd, &pstat) || !S_ISDIR(pstat.st_mode)) { 1279 | print_err("turnstiled base path does not exist"); 1280 | return 1; 1281 | } 1282 | dirfd_base = dir_make_at(dfd, SOCK_DIR, 0755); 1283 | if (dirfd_base < 0) { 1284 | print_err("failed to create base directory (%s)", strerror(errno)); 1285 | return 1; 1286 | } 1287 | dirfd_users = dir_make_at(dirfd_base, "users", 0755); 1288 | if (dirfd_users < 0) { 1289 | print_err("failed to create users directory (%s)", strerror(errno)); 1290 | return 1; 1291 | } 1292 | dirfd_sessions = dir_make_at(dirfd_base, "sessions", 0755); 1293 | if (dirfd_sessions < 0) { 1294 | print_err( 1295 | "failed to create sessions directory (%s)", strerror(errno) 1296 | ); 1297 | return 1; 1298 | } 1299 | close(dfd); 1300 | } 1301 | /* ensure it is not accessible by service manager child processes */ 1302 | if ( 1303 | fcntl(dirfd_base, F_SETFD, FD_CLOEXEC) || 1304 | fcntl(dirfd_users, F_SETFD, FD_CLOEXEC) || 1305 | fcntl(dirfd_sessions, F_SETFD, FD_CLOEXEC) 1306 | ) { 1307 | print_err("fcntl failed (%s)", strerror(errno)); 1308 | return 1; 1309 | } 1310 | 1311 | /* use a strict mask */ 1312 | umask(077); 1313 | 1314 | /* signal pipe */ 1315 | { 1316 | if (pipe(sigpipe) < 0) { 1317 | print_err("pipe failed (%s)", strerror(errno)); 1318 | return 1; 1319 | } 1320 | if ( 1321 | (fcntl(sigpipe[0], F_SETFD, FD_CLOEXEC) < 0) || 1322 | (fcntl(sigpipe[1], F_SETFD, FD_CLOEXEC) < 0) 1323 | ) { 1324 | print_err("fcntl failed (%s)", strerror(errno)); 1325 | return 1; 1326 | } 1327 | auto &pfd = fds.emplace_back(); 1328 | pfd.fd = sigpipe[0]; 1329 | pfd.events = POLLIN; 1330 | pfd.revents = 0; 1331 | } 1332 | 1333 | print_dbg("turnstiled: init control socket"); 1334 | 1335 | /* main control socket */ 1336 | { 1337 | if (!sock_new(DAEMON_SOCK, ctl_sock, CSOCK_MODE)) { 1338 | return 1; 1339 | } 1340 | auto &pfd = fds.emplace_back(); 1341 | pfd.fd = ctl_sock; 1342 | pfd.events = POLLIN; 1343 | pfd.revents = 0; 1344 | } 1345 | 1346 | print_dbg("turnstiled: main loop"); 1347 | 1348 | std::size_t i = 0, curpipes; 1349 | bool term = false; 1350 | 1351 | /* main loop */ 1352 | for (;;) { 1353 | print_dbg("turnstiled: poll"); 1354 | auto pret = poll(fds.data(), fds.size(), -1); 1355 | if (pret < 0) { 1356 | /* interrupted by signal */ 1357 | if (errno == EINTR) { 1358 | goto do_compact; 1359 | } 1360 | print_err("poll failed (%s)", strerror(errno)); 1361 | return 1; 1362 | } else if (pret == 0) { 1363 | goto do_compact; 1364 | } 1365 | /* check signal fd */ 1366 | print_dbg("turnstiled: check signal"); 1367 | if (fds[0].revents == POLLIN) { 1368 | sig_data sd; 1369 | if (read(fds[0].fd, &sd, sizeof(sd)) != sizeof(sd)) { 1370 | print_err("signal read failed (%s)", strerror(errno)); 1371 | goto do_compact; 1372 | } 1373 | if (sd.sign == SIGALRM) { 1374 | if (!sig_handle_alrm(sd.datap)) { 1375 | return 1; 1376 | } 1377 | goto signal_done; 1378 | } 1379 | if ((sd.sign == SIGTERM) || (sd.sign == SIGINT)) { 1380 | if (!sig_handle_term()) { 1381 | return 1; 1382 | } 1383 | term = true; 1384 | goto signal_done; 1385 | } 1386 | /* this is a SIGCHLD */ 1387 | if (!sig_handle_chld()) { 1388 | return 1; 1389 | } 1390 | } 1391 | signal_done: 1392 | print_dbg("turnstiled: check term"); 1393 | if (term) { 1394 | /* check if there are any more live processes */ 1395 | bool die_now = true; 1396 | for (auto &lgn: logins) { 1397 | if ((lgn.srv_pid >= 0) || (lgn.term_pid >= 0)) { 1398 | /* still waiting for something to die */ 1399 | die_now = false; 1400 | break; 1401 | } 1402 | } 1403 | if (die_now) { 1404 | /* no more managed processes */ 1405 | return 0; 1406 | } 1407 | /* the only thing to handle when terminating is signal pipe */ 1408 | continue; 1409 | } 1410 | /* check incoming connections on control socket */ 1411 | print_dbg("turnstiled: check incoming"); 1412 | sock_handle_conn(); 1413 | /* check on pipes; npipes may be changed by fd_handle_pipe */ 1414 | curpipes = npipes; 1415 | print_dbg("turnstiled: check pipes"); 1416 | for (i = 2; i < (curpipes + 2); ++i) { 1417 | try { 1418 | if (!fd_handle_pipe(i)) { 1419 | return 1; 1420 | } 1421 | } catch (std::bad_alloc const &) { 1422 | return 1; 1423 | } 1424 | } 1425 | print_dbg("turnstiled: check conns"); 1426 | /* check on connections */ 1427 | for (; i < fds.size(); ++i) { 1428 | if (!fd_handle_conn(i)) { 1429 | return 1; 1430 | } 1431 | } 1432 | do_compact: 1433 | print_dbg("turnstiled: compact"); 1434 | /* compact the descriptor list */ 1435 | for (auto it = fds.begin(); it != fds.end();) { 1436 | if (it->fd == -1) { 1437 | it = fds.erase(it); 1438 | } else { 1439 | ++it; 1440 | } 1441 | } 1442 | /* queue pipes after control socket */ 1443 | for (auto &lgn: logins) { 1444 | if (!lgn.pipe_queued) { 1445 | continue; 1446 | } 1447 | pollfd pfd; 1448 | pfd.fd = lgn.userpipe; 1449 | pfd.events = POLLIN | POLLHUP; 1450 | pfd.revents = 0; 1451 | /* insert in the pipe area so they are polled before conns */ 1452 | fds.insert(fds.begin() + 2, pfd); 1453 | /* ensure it's not re-queued again */ 1454 | lgn.pipe_queued = false; 1455 | ++npipes; 1456 | } 1457 | } 1458 | for (auto &fd: fds) { 1459 | if (fd.fd >= 0) { 1460 | close(fd.fd); 1461 | } 1462 | } 1463 | return 0; 1464 | } 1465 | -------------------------------------------------------------------------------- /src/turnstiled.hh: -------------------------------------------------------------------------------- 1 | /* shared turnstiled header 2 | * 3 | * Copyright 2022 q66 4 | * License: BSD-2-Clause 5 | */ 6 | 7 | #ifndef TURNSTILED_HH 8 | #define TURNSTILED_HH 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | #include "protocol.hh" 21 | 22 | struct login; 23 | 24 | /* represents a single session within a login */ 25 | struct session { 26 | session(): 27 | str_left{0}, 28 | handshake{1}, 29 | pend_vtnr{1}, 30 | pend_remote{1}, 31 | pend_service{1}, 32 | pend_type{1}, 33 | pend_class{1}, 34 | pend_desktop{1}, 35 | pend_seat{1}, 36 | pend_tty{1}, 37 | pend_display{1}, 38 | pend_ruser{1}, 39 | pend_rhost{1} 40 | {} 41 | /* data strings */ 42 | std::string s_service{}; 43 | std::string s_type{}; 44 | std::string s_class{}; 45 | std::string s_desktop{}; 46 | std::string s_seat{}; 47 | std::string s_tty{}; 48 | std::string s_display{}; 49 | std::string s_ruser{}; 50 | std::string s_rhost{}; 51 | /* the login the session belongs to */ 52 | login *lgn; 53 | /* session id */ 54 | unsigned long id; 55 | /* the session vt number */ 56 | unsigned long vtnr; 57 | /* pid of the login process */ 58 | pid_t lpid; 59 | /* requested amount of data before we can proceed */ 60 | int needed; 61 | /* whether we're remote */ 62 | bool remote; 63 | /* the connection descriptor */ 64 | int fd; 65 | /* stage */ 66 | unsigned int str_left: 16; 67 | unsigned int handshake: 1; 68 | unsigned int pend_vtnr: 1; 69 | unsigned int pend_remote: 1; 70 | unsigned int pend_service: 1; 71 | unsigned int pend_type: 1; 72 | unsigned int pend_class: 1; 73 | unsigned int pend_desktop: 1; 74 | unsigned int pend_seat: 1; 75 | unsigned int pend_tty: 1; 76 | unsigned int pend_display: 1; 77 | unsigned int pend_ruser: 1; 78 | unsigned int pend_rhost: 1; 79 | }; 80 | 81 | /* represents a collection of sessions for a specific user id */ 82 | struct login { 83 | /* a list of connection file descriptors for this login */ 84 | std::vector sessions{}; 85 | /* the username */ 86 | std::string username{}; 87 | /* the string the backend 'run' hands over to 'ready' */ 88 | std::string srvstr{}; 89 | /* the user's shell */ 90 | std::string shell{}; 91 | /* the user's home directory */ 92 | std::string homedir{}; 93 | /* the XDG_RUNTIME_DIR */ 94 | std::string rundir{}; 95 | /* the PID of the service manager process we are currently managing */ 96 | pid_t srv_pid = -1; 97 | /* the PID of the backend "ready" process that reports final readiness */ 98 | pid_t start_pid = -1; 99 | /* the PID of the service manager process that is currently dying */ 100 | pid_t term_pid = -1; 101 | /* login timer; there can be only one per login */ 102 | timer_t timer{}; 103 | sigevent timer_sev{}; 104 | /* user and group IDs read off the first connection */ 105 | unsigned int uid = 0; 106 | unsigned int gid = 0; 107 | /* the read end of the pipe that the service manager uses to signal 108 | * command readiness 109 | */ 110 | int userpipe = -1; 111 | /* login directory descriptor */ 112 | int dirfd = -1; 113 | /* whether the login should be repopulated on next session */ 114 | bool repopulate = true; 115 | /* true unless srv_pid has completely finished starting */ 116 | bool srv_wait = true; 117 | /* false unless waiting for term_pid to quit before starting again */ 118 | bool srv_pending = false; 119 | /* whether to manage XDG_RUNTIME_DIR (typically false) */ 120 | bool manage_rdir = false; 121 | /* whether the timer is actually currently set up */ 122 | bool timer_armed = false; 123 | /* whether a SIGKILL was attempted */ 124 | bool kill_tried = false; 125 | /* whether a pipe is queued */ 126 | bool pipe_queued = false; 127 | 128 | login(); 129 | void remove_sdir(); 130 | bool arm_timer(std::time_t); 131 | void disarm_timer(); 132 | }; 133 | 134 | /* filesystem utilities */ 135 | int dir_make_at(int dfd, char const *dname, mode_t mode); 136 | bool rundir_make(char *rundir, unsigned int uid, unsigned int gid); 137 | void rundir_clear(char const *rundir); 138 | bool dir_clear_contents(int dfd); 139 | 140 | /* config file related utilities */ 141 | void cfg_read(char const *cfgpath); 142 | void cfg_expand_rundir( 143 | std::string &dest, char const *tmpl, unsigned int uid, unsigned int gid 144 | ); 145 | 146 | /* service manager utilities */ 147 | void srv_child(login &sess, char const *backend, bool make_rundir); 148 | bool srv_boot(login &sess, char const *backend); 149 | 150 | struct cfg_data { 151 | time_t login_timeout = 60; 152 | bool debug = false; 153 | bool disable = false; 154 | bool debug_stderr = false; 155 | bool manage_rdir = MANAGE_RUNDIR; 156 | bool export_dbus = true; 157 | bool linger = false; 158 | bool linger_never = false; 159 | bool root_session = false; 160 | std::string backend = "dinit"; 161 | std::string rdir_path = RUN_PATH "/user/%u"; 162 | }; 163 | 164 | extern cfg_data *cdata; 165 | 166 | /* these are macros for a simple reason; making them functions will trigger 167 | * format-security warnings (even though it's technically always safe for 168 | * us, there is no way to bypass that portably) and making it a C-style 169 | * vararg function is not possible (because vsyslog is not standard) 170 | * 171 | * in a macro we just pass things through, so it's completely safe 172 | */ 173 | 174 | #define print_dbg(...) \ 175 | if (cdata->debug) { \ 176 | if (cdata->debug_stderr) { \ 177 | fprintf(stderr, __VA_ARGS__); \ 178 | fputc('\n', stderr); \ 179 | } \ 180 | syslog(LOG_DEBUG, __VA_ARGS__); \ 181 | } 182 | 183 | #define print_err(...) \ 184 | if (cdata->debug_stderr) { \ 185 | fprintf(stderr, __VA_ARGS__); \ 186 | fputc('\n', stderr); \ 187 | } \ 188 | syslog(LOG_ERR, __VA_ARGS__); 189 | 190 | #endif 191 | -------------------------------------------------------------------------------- /src/utils.cc: -------------------------------------------------------------------------------- 1 | /* shared non-portable utilities 2 | * 3 | * Copyright 2022 q66 4 | * License: BSD-2-Clause 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #if defined(__sun) || defined(sun) 16 | # if __has_include() 17 | # include 18 | # else 19 | # include 20 | # endif 21 | #endif 22 | 23 | #include "utils.hh" 24 | 25 | bool get_peer_cred(int fd, uid_t *uid, gid_t *gid, pid_t *pid) { 26 | #if defined(SO_PEERCRED) 27 | /* Linux or OpenBSD */ 28 | #ifdef __OpenBSD 29 | struct sockpeercred cr; 30 | #else 31 | struct ucred cr; 32 | #endif 33 | socklen_t crl = sizeof(cr); 34 | if (!getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cr, &crl) && (crl == sizeof(cr))) { 35 | if (uid) { 36 | *uid = cr.uid; 37 | } 38 | if (gid) { 39 | *gid = cr.gid; 40 | } 41 | if (pid) { 42 | *pid = cr.pid; 43 | } 44 | return true; 45 | } 46 | #elif defined(LOCAL_PEERCRED) 47 | /* FreeBSD or Darwin */ 48 | struct xucred cr; 49 | socklen_t crl = sizeof(cr); 50 | if ( 51 | !getsockopt(fd, SOL_LOCAL, LOCAL_PEERCRED, &cr, &crl) && (crl == sizeof(cr)) && 52 | (cr.cr_version == XUCRED_VERSION) 53 | ) { 54 | if (uid) { 55 | *uid = cr.cr_uid; 56 | } 57 | if (gid) { 58 | *gid = cr.cr_gid; 59 | } 60 | if (pid) { 61 | *pid = cr.cr_pid; 62 | } 63 | return true; 64 | } 65 | #elif defined(LOCAL_PEEREID) 66 | /* NetBSD */ 67 | struct unpcbid cr; 68 | socklen_t crl = sizeof(cr); 69 | if (!getsockopt(fd, 0, LOCAL_PEEREID, &cr, &crl) && (crl == sizeof(cr))) { 70 | if (uid) { 71 | *uid = cr.unp_euid; 72 | } 73 | if (gid) { 74 | *gid = cr.unp_egid; 75 | } 76 | if (pid) { 77 | *pid = cr.unp_pid; 78 | } 79 | return true; 80 | } 81 | #elif defined(__sun) || defined(sun) 82 | /* Solaris */ 83 | ucred_t *cr = nullptr; 84 | if (getpeerucred(fd, &cr) < 0) { 85 | return false; 86 | } 87 | auto uidv = ucred_geteuid(cr); 88 | auto gidv = ucred_getegid(cr); 89 | auto pidv = ucred_getpid(cr); 90 | ucred_free(cr); 91 | if ( 92 | (uid && (uidv == uid_t(-1))) || 93 | (gid && (gidv == gid_t(-1))) || 94 | (pid && (pidv < 0)) 95 | ) { 96 | return false; 97 | } 98 | if (uid) { 99 | *uid = uidv; 100 | } 101 | if (gid) { 102 | *gid = gidv; 103 | } 104 | if (pid) { 105 | *pid = pidv; 106 | } 107 | return true; 108 | #else 109 | #error Please implement credentials checking for your OS. 110 | #endif 111 | return false; 112 | } 113 | 114 | unsigned long get_pid_vtnr(pid_t pid) { 115 | unsigned long vtnr = 0; 116 | 117 | #ifdef __linux__ 118 | char buf[256]; 119 | char tbuf[256]; 120 | unsigned long cterm; 121 | std::snprintf( 122 | buf, sizeof(buf), "/proc/%lu/stat", static_cast(pid) 123 | ); 124 | FILE *f = std::fopen(buf, "rb"); 125 | if (!f) { 126 | return 0; 127 | } 128 | if (!std::fgets(tbuf, sizeof(tbuf), f)) { 129 | fclose(f); 130 | return 0; 131 | } 132 | fclose(f); 133 | char *sp = std::strchr(tbuf, ')'); 134 | if (!sp) { 135 | return 0; 136 | } 137 | if (std::sscanf(sp + 2, "%*c %*d %*d %*d %lu", &cterm) != 1) { 138 | return 0; 139 | } 140 | if ((major(cterm) == 0) && (minor(cterm) == 0)) { 141 | return 0; 142 | } 143 | std::snprintf( 144 | buf, sizeof(buf), "/sys/dev/char/%d:%d", major(cterm), minor(cterm) 145 | ); 146 | std::memset(tbuf, '\0', sizeof(tbuf)); 147 | if (readlink(buf, tbuf, sizeof(tbuf) - 1) < 0) { 148 | return 0; 149 | } 150 | sp = strrchr(tbuf, '/'); 151 | if (sp && !std::strncmp(sp + 1, "tty", 3)) { 152 | char *endp = nullptr; 153 | vtnr = std::strtoul(sp + 4, &endp, 10); 154 | if (endp && *endp) { 155 | vtnr = 0; 156 | } 157 | } 158 | #else 159 | #error Please add your implementation here 160 | #endif 161 | 162 | return vtnr; 163 | } 164 | -------------------------------------------------------------------------------- /src/utils.hh: -------------------------------------------------------------------------------- 1 | /* shared non-portable utilities 2 | * 3 | * Copyright 2022 q66 4 | * License: BSD-2-Clause 5 | */ 6 | 7 | #ifndef UTILS_HH 8 | #define UTILS_HH 9 | 10 | #include 11 | 12 | bool get_peer_cred(int fd, uid_t *uid, gid_t *gid, pid_t *pid); 13 | unsigned long get_pid_vtnr(pid_t pid); 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /turnstiled.conf.5.scd.in: -------------------------------------------------------------------------------- 1 | turnstiled.conf(5) 2 | 3 | # NAME 4 | 5 | turnstiled.conf - the *turnstiled*(8) configuration file 6 | 7 | # DESCRIPTION 8 | 9 | The file _turnstiled.conf_ contains the daemon's configuration. 10 | It typically resides in _/etc/turnstile_ (or your sysconfdir of choice). 11 | 12 | While the daemon can run without any configuration file thanks to its 13 | built-in defaults, there are many options that the user may want to 14 | adjust to their liking. 15 | 16 | # SYNTAX 17 | 18 | The configuration file has a simple line-based syntax. Each option line 19 | consists of the option name and option value separated by the '=' symbol. 20 | 21 | Comments start with the '#' symbol. All whitespace is ignored, including 22 | lines containing only whitespace, trailing whitespace, leading whitespace 23 | and whitespace inbetween names. 24 | 25 | Only 1024 characters at most are read per line, including whitespace. If 26 | longer, the rest is simply ignored. 27 | 28 | # OPTIONS 29 | 30 | This is the list of possible options, with their type and default value, 31 | as well as additional description. 32 | 33 | Boolean options accept only the values _yes_ and _no_. Other options may 34 | accept more values. 35 | 36 | *debug* (boolean: _no_) 37 | Whether to output debug information. This is verbose logging that is 38 | only useful when investigating issues. 39 | 40 | *backend* (string: _dinit_) 41 | The service backend to use. The default is build-dependent and in this 42 | case is set to _@DEFAULT_BACKEND@_. 43 | 44 | Can also be set to _none_ to disable the service backend. In that case, 45 | nothing will be spawned, but the daemon will still perform login tracking 46 | and auxiliary tasks such as rundir management. 47 | 48 | *debug\_stderr* (boolean: _no_) 49 | Whether to print debug messages also to stderr. 50 | 51 | *linger* (combo: _maybe_) 52 | Whether to keep already started services running even after the last login 53 | of the user is gone. The default behavior is to stop them unless a file 54 | with the same name as the user exists in _@LINGER_PATH@_. 55 | 56 | It is not necessary to log in and out when the linger directory is changed, 57 | as the current state is checked upon last logout. 58 | 59 | Note that lingering is disabled when _manage\_rundir_ is set to no. That is 60 | because various user services may be relying on the rundir's existence, 61 | and it cannot be deleted until the user is gone. This is overridable with 62 | an environment variable (for those who worked around it on their own). 63 | 64 | Valid values are _yes_, _no_ and _maybe_. 65 | 66 | *rundir\_path* (string: _@RUN_PATH@/usr/%u_) 67 | The value of _$XDG\_RUNTIME\_DIR_ that is exported into the user service 68 | environment. Special values _%u_ (user ID), _%g_ (group ID) and _%%_ 69 | (the character '%') are allowed and substituted in the string. Set to 70 | empty string if you want to prevent it from being exported altogether. 71 | 72 | It must not end with a slash, be relative or be just the root filesystem. 73 | 74 | If you are using elogind, you should not mess with this path, and doing 75 | so will result in subtly broken systems. You should in general not mess 76 | with this path. 77 | 78 | *manage\_rundir* (boolean: _@MANAGE_RUNDIR@_) 79 | Whether to manage the _$XDG\_RUNTIME\_DIR_. This may conflict with other 80 | rundir management methods, such as elogind, so when turning it on, make 81 | sure this is not the case. 82 | 83 | It is a requirement for the linger functionality to work. 84 | 85 | The default is dependent on the build. 86 | 87 | *export\_dbus\_address* (boolean: _yes_) 88 | Whether to export _$DBUS\_SESSION\_BUS\_ADDRESS_ into the environment. 89 | When enabled, this will be exported and set to 'unix:path=RUNDIR/bus' 90 | where RUNDIR is the expanded value of _rundir\_path_. This works 91 | regardless of if rundir is managed. 92 | 93 | *login\_timeout* (integer: _60_) 94 | The timeout for the login (in seconds). If the user services that are a 95 | part of the initial startup process take longer than this, the service 96 | manager instance is terminated and all connections to the session are 97 | closed. 98 | 99 | *root\_session* (boolean: _no_) 100 | Whether to run a user service manager for root logins. By default, the 101 | root login is tracked but service manager is not run for it. If you 102 | override that, the root user is treated like any other user and will 103 | have its own user services. This may result in various gotchas, such 104 | root having a session bus, and so on. 105 | -------------------------------------------------------------------------------- /turnstiled.conf.in: -------------------------------------------------------------------------------- 1 | ## This is the configuration file for turnstiled. 2 | ## 3 | ## The daemon will function even without a configuration 4 | ## file, but the values here reflect the built-in defaults. 5 | ## 6 | ## The syntax is a simple line-by-line list of values. 7 | ## Values are case-sensitive. Whitespace around the 8 | ## assignment (=) as well as any leading and trailing 9 | ## whitespace is ignored. Only 1024 characters at most 10 | ## are read per line, including whitespace. 11 | ## 12 | ## Lines starting with # are considered comments. Lines 13 | ## that fail to parse are ignored. Invalid values are 14 | ## ignored (configuration will be unchanged). 15 | 16 | # Whether to output debug information. This is verbose 17 | # logging that is only useful when investigating issues. 18 | # 19 | # Valid values are 'yes' and 'no'. 20 | # 21 | debug = no 22 | 23 | # The service backend to use. The default is build-dependent 24 | # and in this case is set to '@DEFAULT_BACKEND@'. 25 | # 26 | # Can also be set to 'none' to disable the service backend. 27 | # In that case, nothing will be spawned, but the daemon 28 | # will still perform login tracking and auxiliary tasks 29 | # such as rundir management. 30 | # 31 | backend = @DEFAULT_BACKEND@ 32 | 33 | # Whether to print debug messages also to stderr. 34 | # 35 | # Valid values are 'yes' and 'no'. 36 | # 37 | debug_stderr = no 38 | 39 | # Whether to keep already started services running even 40 | # after the last login of the user is gone. The default 41 | # behavior is to stop them unless a file with the same 42 | # name as the user exists in '@LINGER_PATH@'. 43 | # 44 | # It is not necessary to log in and out when the linger 45 | # directory is changed, as the current state is checked 46 | # upon last logout. 47 | # 48 | # Note that lingering is disabled when manage_rundir is 49 | # set to no. That is because various user services may 50 | # be relying on the rundir's existence, and it cannot 51 | # be deleted until the user is gone. 52 | # 53 | # Valid values are 'yes', 'no' and 'maybe'. 54 | # 55 | linger = maybe 56 | 57 | # The value of XDG_RUNTIME_DIR that is exported into the 58 | # user service environment. Special values '%u' (user ID), 59 | # '%g' (group ID) and '%%' (the character %) are allowed 60 | # and substituted in the string. Set to empty string if 61 | # you want to prevent it from being exported altogether. 62 | # 63 | # It must not end with a slash or be relative or just '/'. 64 | # 65 | # If you are using elogind, you should not mess with this 66 | # path, and doing so will result in subtly broken systems. 67 | # You should in general not mess with this path. 68 | # 69 | rundir_path = @RUN_PATH@/user/%u 70 | 71 | # Whether to manage the XDG_RUNTIME_DIR. This may conflict 72 | # with other rundir management methods, such as elogind, 73 | # so when turning it on, make sure this is not the case. 74 | # 75 | # It is a requirement for the linger functionality to work. 76 | # 77 | # The default is dependent on the build (here: @MANAGE_RUNDIR@). 78 | # 79 | # Valid values are 'yes' and 'no'. 80 | # 81 | manage_rundir = @MANAGE_RUNDIR@ 82 | 83 | # Whether to export DBUS_SESSION_BUS_ADDRESS into the 84 | # environment. When enabled, this will be exported and 85 | # set to 'unix:path=RUNDIR/bus' where RUNDIR is the 86 | # expanded value of rundir_path. This works regardless 87 | # of if rundir is managed. 88 | # 89 | # Valid values are 'yes' and 'no'. 90 | # 91 | export_dbus_address = yes 92 | 93 | # The timeout for the login. If the user services that 94 | # are a part of the initial startup process take longer 95 | # than this, the service manager instance is terminated 96 | # and all connections to the session are closed. 97 | # 98 | # The value is an integer and represents seconds. 99 | # If set to 0, the timeout is disabled. 100 | # 101 | login_timeout = 60 102 | 103 | # When using a backend that is not 'none', this controls 104 | # whether to run the user session manager for the root 105 | # user. The login session will still be tracked regardless 106 | # of the setting, 107 | # 108 | # Valid values are 'yes' and 'no'. 109 | # 110 | root_session = no 111 | --------------------------------------------------------------------------------