pax_global_header00006660000000000000000000000064150727467600014530gustar00rootroot0000000000000052 comment=75f2b1c4c9e88a674edaa6f95b483a6656b960ba turnstile-0.1.11/000077500000000000000000000000001507274676000136415ustar00rootroot00000000000000turnstile-0.1.11/.gitignore000066400000000000000000000000071507274676000156260ustar00rootroot00000000000000build/ turnstile-0.1.11/.mailmap000066400000000000000000000005501507274676000152620ustar00rootroot00000000000000# add yourself here if name/email changes # # format: # # propername commitname q66 Daniel Kolesa q66 Daniel Kolesa q66 Daniel Kolesa q66 q66 turnstile-0.1.11/COPYING.md000066400000000000000000000024161507274676000152760ustar00rootroot00000000000000Copyright 2021-2024 q66 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. turnstile-0.1.11/README.md000066400000000000000000000235471507274676000151330ustar00rootroot00000000000000# turnstile Turnstile is a work in progress effort to create a session/login tracker to serve as a fully featured alternative to the logind subproject from systemd, and to provide a neutral API to both our session tracker and to logind itself. It is: * a session/login tracker * a service-manager-agnostic way to manage per-user service managers for user services it is not: * a seat tracker (you want [seatd](https://git.sr.ht/~kennylevinsen/seatd) for that) it is not yet: * a library to examine session information ## History Its original name was dinit-userservd and it was created as a way to auto-spawn user instances of [Dinit](https://github.com/davmac314/dinit) upon login and shut them down upon logout, to allow for clean management of user services. Soon after it outgrew its original responsibilities and gained adjacent functionality such as handling of `XDG_RUNTIME_DIR`. At that point, it was decided that it would be worthwhile to expand the overall scope, as most of the effort was already there. ## Purpose Its ultimate goal is to provide a fully featured replacement for the `logind` component of systemd, solving the current status quo where `logind` is the de-facto standard, but at the same time very much tied to systemd. While there are workarounds such as elogind, these are far from ideal. For instance, elogind is just a stubbed out version of upstream logind, and only provides the bare minimum, so systems using it are left without support for user services and other useful functionality. This goal has not yet been accomplished, as at the moment Turnstile is only a daemon and does not provide any API. This will change in the future. This API will provide a way to access the session information, but will not deal with seat management. You will be able to use the library together with `libseat` without conflicting. The API will expose the bare minimum needed for the two libraries to interoperate. Turnstile is designed to not care about what service manager it is used with. None of the daemon code cares, instead leaving this to separate backends. ## Backends Turnstile is capable of supporting multiple service managers, and the code makes no assumptions about what service manager one is using to handle user instances. That said, right now the only available backend is for Dinit, which also serves as an example for implementation of other backends. There is also the built-in `none` backend, which does not handle user services at all and lets the daemon do only session tracking and auxiliary tasks. The used backend is configured in `turnstiled.conf`. A backend is a very trivial shell script. Its responsibility is to launch the service manager and ensure that the daemon is notified of its readiness, which is handled with a special file descriptor. ## How it works There are three parts. 1) The daemon, `turnstiled`. 2) The PAM module, `pam_turnstile.so`. 3) The chosen backend. The daemon needs to be running in some way. Usually you will spawn it as a system-wide service. It needs to be running as the superuser. The daemon is what keeps track of the session state, and what launches the user service manager through the backend. The PAM module needs to be in your login path. This will differ per-distro, but typically it will involve a line like this: ``` session optional pam_turnstile.so ``` When the daemon starts, it opens a Unix domain socket. This is where it listens for connections. When a user tries to log in, the PAM module will open one such connection and communicate the information to the daemon using a custom internal protocol. Once the handshake is done and all the state is properly negotiated, the daemon will try to spawn the service manager for the user. It does so through the backend, which is tasked with the `run` action. The backend is a little helper program that can be written in any language, it can e.g. be a shell script. It is started with a clean environment with many of the common environment variables, such as `HOME`, `USER`, `LOGNAME`, `SHELL`, `PATH` and others, freshly initialized. Typically it is expected to source the system `/etc/profile` for `/bin/sh`. Additionally, it runs within a PAM session (without authentication), which persists for the lifetime of the login, so PAM environment, resource limits and so on are also set up. It may also be a good idea to put `pam_elogind` or `pam_systemd` in there in order to have `logind` recognize the `turnstile` user session as a session (which allows it to be tracked by things using it, e.g. `polkitd`). Note that if you use `pam_systemd` or `pam_elogind` in `turnstiled` PAM script to register it as a session, it will be treated as a session without a seat. That means things like `polkit` may treat anything running within `turnstile` as a non-local session, and may not authenticate the processes. There is no way to get around this limitation outside of patching `polkit`, see Chimera's patches for reference. The alternative is not registering it at all, which will not make `polkit` work, as the session tracking logic in it will not be able to assign the processes to any UID and things will not work either. Systemd user services are treated specially by `systemd`, as they are recognized by the service manager, but are explicitly not considered to be a part of any session (as they are shared); that means `polkit` will fall back to looking up whether any seated session for the UID exists. After performing some initial preparation (which is backend-specific), the backend will simply replace itself with the desired service manager. There is a special file descriptor that is passed to the backend. The service manager (or possibly even the backend itself) can write a string of data in there when it's ready enough to accept outside commands. Once that has happened, the daemon will invoke the backend once more, this time with the `ready` action and as a regular (non-login) shell script, without any special environment setup. It passes the previously received string as an argument. The backend then has the responsibility to wait as long as it takes (or until a timeout is reached) for the initial user services to start up. Afterwards, the daemon will send a message back to the PAM module, allowing the login to proceed. This ensures that by the time the user gets their login terminal, the autostarted user services are already up. When the user logs out (or rather, when the last login of the user has logged out), this service manager will shut down by default. However, it can also be configured to linger. ### Auxiliary tasks The daemon can also perform various adjacent tasks. As it can be configured through `turnstiled.conf`, many of these can be enabled or disabled as needed. #### Rundir management The environment variable `XDG_RUNTIME_DIR` is by default set in the user's login environment. Typically it is something like `/run/user/$UID`. Turnstile can also create this directory. Whether it creates it by default comes down to how the build is configured. Environments using stock `logind` will want to keep it off in order to avoid conflicting, while others may want to turn it on. Regardless of the default behavior, it can be altered in the configuration file. #### Session persistence It is possible to configure the sessions to linger, so the user services will remain up even after logout. This can be done either per-user, or globally. Note that session persistence relies on rundir creation being enabled, as in the other case the daemon cannot know whether the other management solution is not deleting the rundir, and many user services rely on its existence. This can be manually overridden with an environment variable, at your own risk. #### D-Bus session bus address By default, the address of the D-Bus session bus will be exported into the login environment and set to something like `unix:path=$XDG_RUNTIME_DIR/bus`, if that socket exists and is valid in that path. This allows the D-Bus session bus to be managed as a user service, to get systemd-style behavior with a single session bus shared between user logins. It can be explicitly disabled if necessary, but mostly there is no need to as the variable will not be exported if the bus does not exist there. Note that this does not mean the bus address is exported into the activation environment, as turnstile does not know about it. The user service that spawns the session bus needs to take care of that, e.g. with `dinitctl setenv` for Dinit. Only this way will other user services know about the session bus. ## Setup Build and install the project. It uses [Meson](https://mesonbuild.com/) and follows the standard Meson workflow. Example: ``` $ mkdir build && cd build $ meson .. --prefix=/usr $ ninja all $ sudo ninja install ``` The dependencies are: 1) A POSIX-compliant OS (Chimera Linux is the reference platform) 2) A C++17 compiler 3) Meson and Ninja (to build) 5) PAM The Dinit backend requires at least Dinit 0.16 or newer, older versions will not work. The project also installs an example Dinit service for starting the daemon. ## Support for other service managers If you write a new backend or other functionality related to other service managers, it would be appreciated if you could submit it upstream (i.e. here). This way we can ensure that other backends stay aligned with the upstream design goals and will not break over time. Additionally, you can get review here, which should ultimately result in more consistent and better quality code. Turnstile is specifically designed to help distro interoperability. Support for other operating systems (such as the BSDs) is also welcome. While the project tries to be portable, it is being tested solely on Linux. Therefore, testing on other operating systems and potential fixes (please send patches) are very helpful. Ultimately I would like the project to serve as a vendor-neutral interface on all Unix-like systems, so that desktop environments and other projects have a quality baseline to target. turnstile-0.1.11/backend/000077500000000000000000000000001507274676000152305ustar00rootroot00000000000000turnstile-0.1.11/backend/dinit000066400000000000000000000106231507274676000162640ustar00rootroot00000000000000#!/bin/sh # # This is the turnstile dinit backend. It accepts the action as its first # argument, which is either "ready", "run", or "stop". The backend can be # written in any language, in this case the shebang is used to run it. # The system profile (but not user profile) for /bin/sh is sourced before # anything is run, in order to include profile.d snippets into the # activation environment. # # It also serves as an example of how to implement such backend. # # Arguments for "ready": # # socket: the path to dinit's control socket; it is the string that is # written by dinit into ready_fd for the "run" part of the process # # Arguments for "run": # # ready_p: path to named pipe (fifo) that should be poked with a string; this # will be passed to the "ready" script of the sequence as its sole # argument (here this is a control socket path) # srvdir: an internal directory that can be used by the service manager # for any purpose (usually to keep track of its state) # confdir: the path where turnstile's configuration data reside, used # to source the configuration file # # Arguments for "stop": # # pid: the PID of the service manager to stop (gracefully); it should # terminate the services it's running and then stop itself # # How the script manages its configuration and so on is up to the script. # # Note that the script *must* exec the service manager directly, i.e. the # service manager must fully replace the shell process for this to work. # # Copyright 2023 q66 # License: BSD-2-Clause # case "$1" in run) ;; ready) if [ -z "$2" -o ! -S "$2" ]; then # must be a control socket echo "dinit: invalid control socket '$2'" >&2 exit 69 fi exec dinitctl --socket-path "$2" start login.target ;; stop) exec kill -s TERM "$2" ;; graphical-notify) if [ -z "$DINIT_CS_FD" ]; then # must have a control socket echo "dinit: control socket not given" >&2 exit 69 fi # this is not invoked by turnstile, but by the monitor service exec dinitctl trigger graphical.target ;; *) exit 32 ;; esac DINIT_READY_PIPE="$2" DINIT_DIR="$3" DINIT_CONF="$4/dinit.conf" if [ ! -p "$DINIT_READY_PIPE" -o ! -d "$DINIT_DIR" ]; then echo "dinit: invalid input argument(s)" >&2 exit 69 fi if [ -z "$HOME" -o ! -d "$HOME" ]; then echo "dinit: invalid home directory" >&2 exit 70 fi shift $# # source system profile mainly for profile.d # do it before switching to set -e etc. [ -r /etc/profile ] && . /etc/profile # be strict set -e # source the conf [ -r "$DINIT_CONF" ] && . "$DINIT_CONF" # set a bunch of defaults in case the conf cannot be read or is mangled [ -z "$boot_dir" ] && boot_dir="${HOME}/.config/dinit.d/boot.d" [ -z "$system_boot_dir" ] && system_boot_dir="/usr/lib/dinit.d/user/boot.d" if [ -z "$services_dir1" ]; then services_dir1="${HOME}/.config/dinit.d" services_dir2="/etc/dinit.d/user" services_dir3="/usr/local/lib/dinit.d/user" services_dir4="/usr/lib/dinit.d/user" fi # translate service dirs to arguments; we pass them to dinit at the end seqn=1 while :; do eval curserv="\$services_dir$seqn" [ -n "$curserv" ] || break set -- "$@" --services-dir "$curserv" seqn=$(($seqn + 1)) done # create boot dir, but make it not a failure if we can't mkdir -p "${boot_dir}" > /dev/null 2>&1 || : # this must succeed cat << EOF > "${DINIT_DIR}/boot" type = internal depends-on = system waits-for.d = ${boot_dir} depends-on = login.target depends-ms = graphical.monitor depends-ms = graphical.target EOF # this must also succeed cat << EOF > "${DINIT_DIR}/system" type = internal waits-for.d = ${system_boot_dir} EOF # monitor service to watch for environment changes cat << EOF > "${DINIT_DIR}/graphical.monitor" type = process depends-on = login.target options = pass-cs-fd command = /usr/bin/dinit-monitor -E -c "$0 graphical-notify" WAYLAND_DISPLAY DISPLAY EOF # this is needed for login to proceed cat << EOF > "${DINIT_DIR}/login.target" type = internal EOF # this is not necessary to have started for login to proceed cat << EOF > "${DINIT_DIR}/graphical.target" type = triggered depends-on = graphical.monitor depends-on = login.target EOF exec dinit --user --ready-fd 3 --services-dir "$DINIT_DIR" "$@" 3>"$DINIT_READY_PIPE" turnstile-0.1.11/backend/dinit.conf000066400000000000000000000023401507274676000172050ustar00rootroot00000000000000# This is the configuration file for turnstile's dinit backend. # # It follows the POSIX shell syntax (being sourced into a script). # The complete launch environment available to dinit can be used. # # It is a low-level configuration file. In most cases, it should # not be modified by the user. # # The directory containing service links that must be # started in order for the login to proceed. Can be # empty, in which case nothing is waited for. # boot_dir="${HOME}/.config/dinit.d/boot.d" # This is just like boot_dir, but not controlled by the # user. Instead, the system installs links there, and # they are started for all users universally. # system_boot_dir="/usr/lib/dinit.d/user/boot.d" # A directory user service files are read from. Every # additional directory needs to have its number incremented. # The numbering matters (defines the order) and there must be # no gaps (it starts with 1, ends at the last undefined). # # If no services directory is defined (i.e. the first one # is not defined), a built-in list will be used (which is # equal to the one defined here). # services_dir1="${HOME}/.config/dinit.d" services_dir2="/etc/dinit.d/user" services_dir3="/usr/local/lib/dinit.d/user" services_dir4="/usr/lib/dinit.d/user" turnstile-0.1.11/backend/meson.build000066400000000000000000000016621507274676000173770ustar00rootroot00000000000000# dinit backend if have_dinit install_data( 'dinit', install_dir: join_paths(get_option('libexecdir'), 'turnstile'), install_mode: 'rwxr-xr-x' ) install_data( 'dinit.conf', install_dir: join_paths(get_option('sysconfdir'), 'turnstile/backend'), install_mode: 'rw-r--r--' ) endif # runit backend if have_runit install_data( 'runit', install_dir: join_paths(get_option('libexecdir'), 'turnstile'), install_mode: 'rwxr-xr-x' ) install_data( 'runit.conf', install_dir: join_paths(get_option('sysconfdir'), 'turnstile/backend'), install_mode: 'rw-r--r--' ) configure_file( input: 'turnstile-update-runit-env.in', output: 'turnstile-update-runit-env', configuration: conf_data, install: true, install_dir: get_option('bindir'), install_mode: 'rwxr-xr-x' ) endif turnstile-0.1.11/backend/runit000066400000000000000000000060401507274676000163140ustar00rootroot00000000000000#!/bin/sh # # This is the turnstile runit backend. It accepts the action as its first # argument, which is either "ready", "run", or "stop". In case of "run", it's # invoked directly through /bin/sh as if it was a login shell, and therefore # it has acccess to shell profile, and the shebang is functionally useless but # should be preserved as a convention. For "ready", it's a regular shell. # # Arguments for "ready": # # ready_sv: path to the readiness service # # Arguments for "run": # # ready_p: readiness pipe (fifo). has the path to the ready service written to it. # srvdir: unused # confdir: the path where turnstile's configuration data resides, used # to source the configuration file # # Arguments for "stop": # # pid: the PID of the service manager to stop (gracefully); it should # terminate the services it's running and then stop itself # # Copyright 2023 classabbyamp # License: BSD-2-Clause case "$1" in run) ;; ready) if [ -z "$2" ] || [ ! -d "$2" ]; then echo "runit: invalid readiness service '$2'" >&2 exit 69 fi exec sv start "$2" >&2 ;; stop) # If runsvdir receives a HUP signal, it sends a TERM signal to each # runsv(8) process it is monitoring and then exits with 111. exec kill -s HUP "$2" ;; *) exit 32 ;; esac RUNIT_READY_PIPE="$2" RUNIT_CONF="$4/runit.conf" if [ ! -p "$RUNIT_READY_PIPE" ]; then echo "runit: invalid input argument(s)" >&2 exit 69 fi if [ -z "$HOME" ] || [ ! -d "$HOME" ]; then echo "runit: invalid home directory" >&2 exit 70 fi shift $# # source system profile mainly for profile.d # do it before switching to set -e etc. [ -r /etc/profile ] && . /etc/profile # be strict set -e # source the conf [ -r "$RUNIT_CONF" ] && . "$RUNIT_CONF" # set some defaults in case the conf cannot be read or is mangled : "${ready_sv:="turnstile-ready"}" : "${services_dir:="${HOME}/.config/service"}" : "${service_env_dir:="${HOME}/.config/service-env"}" mkdir -p "${services_dir}/${ready_sv}" > /dev/null 2>&1 mkdir -p "${service_env_dir}" > /dev/null 2>&1 # this must succeed cat << EOF > "${services_dir}/${ready_sv}/run" #!/bin/sh [ -r ./conf ] && . ./conf [ -n "\$core_services" ] && SVDIR=".." sv start \$core_services if [ -n "\$core_services" ]; then until SVDIR=".." sv check \$core_services; do : done fi [ -p "$RUNIT_READY_PIPE" ] && printf "${services_dir}/${ready_sv}" > "$RUNIT_READY_PIPE" exec pause EOF chmod +x "${services_dir}/${ready_sv}/run" exec env TURNSTILE_ENV_DIR="$service_env_dir" \ runsvdir -P "$services_dir" \ 'log: ...........................................................................................................................................................................................................................................................................................................................................................................................................' turnstile-0.1.11/backend/runit.conf000066400000000000000000000011351507274676000172400ustar00rootroot00000000000000# This is the configuration file for turnstile's runit backend. # # It follows the POSIX shell syntax (being sourced into a script). # The complete launch environment available to dinit can be used. # # It is a low-level configuration file. In most cases, it should # not be modified by the user. # the name of the service that turnstile will check for login readiness ready_sv="turnstile-ready" # the directory user service files are read from. services_dir="${HOME}/.config/service" # the environment variable directory user service files can read from. service_env_dir="${HOME}/.config/service-env" turnstile-0.1.11/backend/turnstile-update-runit-env.in000066400000000000000000000010721507274676000230160ustar00rootroot00000000000000#!/bin/sh # Copyright 2023 classabbyamp # License: BSD-2-Clause usage() { cat <<-EOF turnstile-update-runit-env [VAR] ... Updates values in the shared chpst(8) env dir. If VAR is a variable name, the value is taken from the environment. If VAR is VAR=VAL, sets VAR to VAL. EOF } . @CONF_PATH@/backend/runit.conf if [ $# -eq 0 ] || [ "$1" = "-h" ]; then usage exit 0 fi for var; do case "$var" in *=*) eval echo "${var#*=}" > "$service_env_dir/${var%%=*}" ;; *) eval echo '$'"$var" > "$service_env_dir/$var" ;; esac done turnstile-0.1.11/data/000077500000000000000000000000001507274676000145525ustar00rootroot00000000000000turnstile-0.1.11/data/dinit/000077500000000000000000000000001507274676000156615ustar00rootroot00000000000000turnstile-0.1.11/data/dinit/turnstiled000066400000000000000000000001751507274676000200040ustar00rootroot00000000000000type = process command = /usr/bin/turnstiled logfile = /var/log/turnstiled.log before: login.target depends-on: local.target turnstile-0.1.11/data/pam/000077500000000000000000000000001507274676000153275ustar00rootroot00000000000000turnstile-0.1.11/data/pam/turnstiled000066400000000000000000000003601507274676000174460ustar00rootroot00000000000000auth sufficient pam_rootok.so session optional pam_keyinit.so force revoke session optional pam_umask.so usergroups umask=022 -session optional pam_elogind.so session required pam_turnstile.so turnstiled session required pam_limits.so turnstile-0.1.11/include/000077500000000000000000000000001507274676000152645ustar00rootroot00000000000000turnstile-0.1.11/include/turnstile.h000066400000000000000000000122711507274676000174710ustar00rootroot00000000000000/* @file turnstile.h * * @brief The libturnstile public API * * This is the public API of libturnstile, an abstraction library for * session tracking. * * The API is not safe to access from multiple threads. Use a lock if * you wish to do so. Using multiple turnstiles within a process is * permitted, and they can be used independently without a lock. Using * global APIs without a turnstile object does not require locking. * * @copyright See the attached COPYING.md for more information. */ #ifndef TURNSTILE_H #define TURNSTILE_H #if defined(__GNUC__) && (__GNUC__ >= 4) # define TURNSTILE_API __attribute__((visibility("default"))) #else # define TURNSTILE_API #endif #ifdef __cplusplus extern "C" { #endif /** @brief The turnstile. * * The turnstile is a handle hich contains all the client-local session * tracking state. Some APIs require a connected turnstile, while some * allow dual operation (passing NULL is allowed). * * APIs in connection mode need an event/dispatch loop and receive data * from a connected peer. Global APIs, on the other hand, rely on publicly * available out-of-process data, and thus do not require any further state, * connection, or a loop. */ typedef struct turnstile turnstile; typedef enum turnstile_event { TURNSTILE_EVENT_LOGIN_NEW = 1, TURNSTILE_EVENT_LOGIN_REMOVED, TURNSTILE_EVENT_LOGIN_CHANGED, TURNSTILE_EVENT_SESSION_NEW, TURNSTILE_EVENT_SESSION_REMOVED, TURNSTILE_EVENT_SESSION_CHANGED, } turnstile_event; /** @brief The turnstile event callback. * * A callback may be registered with turnstile_watch_events(). * The turnstile is passed, along with the event type, the id of the * affected object, and custom data provided during callback registration. * * For forward-compatible use, you should always filter for the specific * event type you require. */ typedef void (*turnstile_event_callback)(turnstile *ts, int event, unsigned long id, void *data); /** @brief Initialize a turnstile backend. * * Calling this will result in a backend being chosen for the lifetime of * the program. The available backends depend on what is compiled into the * library, and follow a priority order, with a fallback null backend being * always last. * * Calling this API with an already chosen backend does nothing. */ TURNSTILE_API void turnstile_init(void); /** @brief Create a new turnstile. * * Creating a new turnstile will connect to a backend. If no backend has * been chosen yet (via turnstile_init()), it will be chosen now. Note that * to actually use other APIs, a backend needs to be chosen, and they will * not choose it for you. * * Afterwards, you will want to either integrate it with your event loop * by getting a file descriptor with turnstile_get_fd(), polling it and * dispatching with turnstile_dispatch(), or if you don't have an event * loop, you can create your own dispatch loop (and don't need to poll). * * @return A turnstile, or NULL on error (errno set). */ TURNSTILE_API turnstile *turnstile_new(void); /** @brief Release the given turnstile. * * This will free the client-local state. Connection will be closed. * * @param ts The turnstile. * @return Zero on success, a negative value on error (errno set). */ TURNSTILE_API void turnstile_free(turnstile *ts); /** @brief Get a pollable file descriptor for the given turnstile. * * This can be used for integration into event loops. You should poll the * resulting file descriptor in your event loop and call turnstile_dispatch() * upon availability of data. * * The client does not own the file descriptor, so it does not need to close * it manually. * * @param ts The turnstile. * @return A pollable fd, or a negative value on error (errno set). */ TURNSTILE_API int turnstile_get_fd(turnstile *ts); /** @brief Dispatch the given turnstile. * * Upon reception of data (availability known through turnstile_get_fd() * descriptor), process the data. Registered callbacks and other things * will be triggered during the process. * * The timeout specifies how long to wait for data. Specifying the value of 0 * means that no timeout will be given, -1 means potentially infinite timeout, * and a positive value is in milliseconds. Synchronous systems may want a * potentially infinite timeout (and no blocking) while async systems will * want to dispatch only what they have to avoid main loop stalls. * * @param ts The turnstile. * @param timeout The timeout. * @return A number of messages processed, or a negative value (errno set). */ TURNSTILE_API int turnstile_dispatch(turnstile *ts, int timeout); /** @brief Add a callback to watch for turnstile events. * * Upon an event (received through turnstile_dispatch()), the given callback * will be called. Events may include new logins, sessions, session state * changes, session drops, and so on. The details can be filtered by checking * the callback parameters. You can pass custom data with the extra parameter. * * @param ts The turnstile. * @param data Extra data to always pass to the callback. * @return Zero on success, a negative value on error (errno set). */ TURNSTILE_API int turnstile_watch_events(turnstile *ts, turnstile_event_callback cb, void *data); #ifdef __cplusplus } #endif #endif turnstile-0.1.11/meson.build000066400000000000000000000116011507274676000160020ustar00rootroot00000000000000project( 'turnstile', ['cpp', 'c'], version: '0.1.11', default_options: [ 'cpp_std=c++17', 'c_std=c11', 'warning_level=3', 'buildtype=debugoptimized', ], license: 'BSD-2-Clause' ) cpp = meson.get_compiler('cpp') pam_dep = dependency('pam', required: true) # could be openpam, in which case pam_misc is not present pam_misc_dep = dependency('pam_misc', required: false) rt_dep = cpp.find_library('rt', required: false) scdoc_dep = dependency( 'scdoc', version: '>=1.10', required: get_option('man'), native: true ) have_dinit = get_option('dinit').enabled() have_runit = get_option('runit').enabled() conf_data = configuration_data() conf_data.set_quoted('RUN_PATH', get_option('rundir')) conf_data.set_quoted('CONF_PATH', join_paths( get_option('prefix'), get_option('sysconfdir'), 'turnstile' )) conf_data.set10('MANAGE_RUNDIR', get_option('manage_rundir')) conf_data.set('HAVE_PAM_MISC', pam_misc_dep.found()) statepath = join_paths( get_option('prefix'), get_option('localstatedir'), get_option('statedir') ) lingerpath = join_paths(statepath, 'linger') conf_data.set_quoted('STATE_PATH', statepath) conf_data.set_quoted('LINGER_PATH', lingerpath) conf_data.set_quoted('LIBEXEC_PATH', join_paths( get_option('prefix'), get_option('libexecdir'), 'turnstile' )) configure_file(output: 'config.hh', configuration: conf_data) extra_inc = [include_directories('src')] add_project_arguments('-D_BSD_SOURCE', language: ['c', 'cpp']) if get_option('library').enabled() lib_sources = [ 'src/lib_api.c', 'src/lib_backend_none.c', 'src/lib_backend_turnstile.c', ] lib = library( 'turnstile', lib_sources, version: meson.project_version(), include_directories: extra_inc + [include_directories('include')], install: true, gnu_symbol_visibility: 'hidden', ) install_headers('include/turnstile.h') endif daemon_sources = [ 'src/turnstiled.cc', 'src/fs_utils.cc', 'src/cfg_utils.cc', 'src/exec_utils.cc', 'src/utils.cc', ] daemon = executable( 'turnstiled', daemon_sources, include_directories: extra_inc, install: true, dependencies: [rt_dep, pam_dep, pam_misc_dep], gnu_symbol_visibility: 'hidden' ) pam_moddir = get_option('pam_moddir') pamdir = get_option('pamdir') if pam_moddir == '' pam_moddir = join_paths( pam_dep.get_variable('libdir', default_value: get_option('libdir')), 'security' ) message('Detected PAM module directory:', pam_moddir) endif if pamdir == '' pamdir = join_paths(get_option('sysconfdir'), 'pam.d') endif pam_mod = shared_module( 'pam_turnstile', ['src/pam_turnstile.cc', 'src/utils.cc'], include_directories: extra_inc, install: true, install_dir: pam_moddir, name_prefix: '', dependencies: [pam_dep], gnu_symbol_visibility: 'hidden' ) if have_dinit install_data( 'data/dinit/turnstiled', install_dir: join_paths(get_option('sysconfdir'), 'dinit.d'), install_mode: 'rw-r--r--' ) endif install_data( 'data/pam/turnstiled', install_dir: pamdir, install_mode: 'rw-r--r--' ) # decide the default backend default_backend = get_option('default_backend') if default_backend == '' if have_dinit default_backend = 'dinit' elif have_runit default_backend = 'runit' else default_backend = 'none' endif endif uconf_data = configuration_data() uconf_data.set('RUN_PATH', get_option('rundir')) uconf_data.set('LINGER_PATH', lingerpath) uconf_data.set('DEFAULT_BACKEND', default_backend) if get_option('manage_rundir') uconf_data.set('MANAGE_RUNDIR', 'yes') else uconf_data.set('MANAGE_RUNDIR', 'no') endif configure_file( input: 'turnstiled.conf.in', output: 'turnstiled.conf', configuration: uconf_data, install: true, install_dir: join_paths(get_option('sysconfdir'), 'turnstile'), install_mode: 'rw-r--r--' ) cscd = configure_file( input: 'turnstiled.conf.5.scd.in', output: 'turnstiled.conf.5.scd', configuration: uconf_data ) fs = import('fs') if get_option('man') scdoc_prog = find_program( scdoc_dep.get_pkgconfig_variable('scdoc'), native: true ) sh = find_program('sh', native: true) mandir = get_option('mandir') man_files = [ 'src/turnstiled.8.scd', 'src/pam_turnstile.8.scd', cscd, ] foreach fobj: man_files filename = fs.name(fobj) output = fs.replace_suffix(filename, '') section = output.split('.')[-1] custom_target( output, input: fobj, capture: true, output: output, command: [ sh, '-c', '@0@ < @INPUT@'.format(scdoc_prog.path()) ], install: true, install_dir: '@0@/man@1@'.format(mandir, section) ) endforeach endif subdir('backend') turnstile-0.1.11/meson_options.txt000066400000000000000000000022271507274676000173010ustar00rootroot00000000000000option('dinit', type: 'feature', value: 'enabled', description: 'Whether to install Dinit-related backend and data' ) option('runit', type: 'feature', value: 'disabled', description: 'Whether to install runit-related backend and data' ) option('default_backend', type: 'string', value: '', description: 'Override the default backend' ) option('rundir', type: 'string', value: '/run', description: 'Where the base directory will be located' ) option('statedir', type: 'string', value: 'lib/turnstiled', description: 'The state directory relative to localstatedir' ) option('pamdir', type: 'string', value: '', description: 'Override the path where PAM files go' ) option('pam_moddir', type: 'string', value: '', description: 'Where to install the PAM module (leave empty to autodetect)' ) option('manage_rundir', type: 'boolean', value: false, description: 'Whether to manage rundir by default' ) option('man', type: 'boolean', value: true, description: 'Whether to generate manpages' ) option('library', type: 'feature', value: 'disabled', description: 'Whether to build the library' ) turnstile-0.1.11/src/000077500000000000000000000000001507274676000144305ustar00rootroot00000000000000turnstile-0.1.11/src/cfg_utils.cc000066400000000000000000000130211507274676000167130ustar00rootroot00000000000000#include #include #include #include #include #include "turnstiled.hh" static void read_bool(char const *name, char const *value, bool &val) { if (!std::strcmp(value, "yes")) { val = true; } else if (!std::strcmp(value, "no")) { val = false; } else { syslog( LOG_WARNING, "Invalid configuration value '%s' for '%s' (expected yes/no)", value, name ); } } void cfg_read(char const *cfgpath) { char buf[1024]; auto *f = std::fopen(cfgpath, "r"); if (!f) { syslog( LOG_NOTICE, "No configuration file '%s', using defaults", cfgpath ); return; } while (std::fgets(buf, sizeof(buf), f)) { auto slen = strlen(buf); /* ditch the rest of the line if needed */ if ((buf[slen - 1] != '\n')) { while (!std::feof(f)) { auto c = std::fgetc(f); if (c == '\n') { std::fgetc(f); break; } } } char *bufp = buf; /* drop trailing whitespace */ while (std::isspace(bufp[slen - 1])) { bufp[--slen] = '\0'; } /* drop leading whitespace */ while (std::isspace(*bufp)) { ++bufp; } /* comment or empty line */ if (!*bufp || (*bufp == '#')) { continue; } /* find the assignment */ char *ass = strchr(bufp, '='); /* invalid */ if (!ass || (ass == bufp)) { syslog(LOG_WARNING, "Malformed configuration line: %s", bufp); continue; } *ass = '\0'; /* find the name */ char *preass = (ass - 1); while (std::isspace(*preass)) { *preass-- = '\0'; } /* empty name */ if (preass == bufp) { syslog(LOG_WARNING, "Invalid configuration line name: %s", bufp); continue; } /* find the value */ while (std::isspace(*++ass)) { continue; } /* supported config lines */ if (!std::strcmp(bufp, "debug")) { read_bool("debug", ass, cdata->debug); } else if (!std::strcmp(bufp, "debug_stderr")) { read_bool("debug_stderr", ass, cdata->debug_stderr); } else if (!std::strcmp(bufp, "manage_rundir")) { read_bool("manage_rundir", ass, cdata->manage_rdir); } else if (!std::strcmp(bufp, "export_dbus_address")) { read_bool("export_dbus_address", ass, cdata->export_dbus); } else if (!std::strcmp(bufp, "root_session")) { read_bool("root_session", ass, cdata->root_session); } else if (!std::strcmp(bufp, "linger")) { if (!std::strcmp(ass, "maybe")) { cdata->linger = false; cdata->linger_never = false; } else { read_bool("linger", ass, cdata->linger); cdata->linger_never = !cdata->linger; } } else if (!std::strcmp(bufp, "backend")) { if (!std::strcmp(ass, "none")) { cdata->backend.clear(); cdata->disable = true; } else if (!std::strlen(ass)) { syslog( LOG_WARNING, "Invalid config value for '%s' (must be non-empty)", bufp ); } else { cdata->backend = ass; } } else if (!std::strcmp(bufp, "rundir_path")) { std::string rp = ass; if (!rp.empty() && ((rp.back() == '/') || (rp.front() != '/'))) { syslog( LOG_WARNING, "Invalid config value for '%s' (%s)", bufp, rp.data() ); } else { cdata->rdir_path = std::move(rp); } } else if (!std::strcmp(bufp, "login_timeout")) { char *endp = nullptr; auto tout = std::strtoul(ass, &endp, 10); if (*endp || (endp == ass)) { syslog( LOG_WARNING, "Invalid config value '%s' for '%s' (expected integer)", ass, bufp ); } else { cdata->login_timeout = time_t(tout); } } } } void cfg_expand_rundir( std::string &dest, char const *tmpl, unsigned int uid, unsigned int gid ) { char buf[32]; while (*tmpl) { auto mark = std::strchr(tmpl, '%'); if (!mark) { /* no formatting mark in the rest of the string, copy all */ dest += tmpl; break; } /* copy up to mark */ auto rlen = std::size_t(mark - tmpl); if (rlen) { dest.append(tmpl, rlen); } /* trailing % or %%, just copy it as is */ if (!mark[1] || ((mark[1] == '%') && !mark[2])) { dest.push_back('%'); break; } ++mark; unsigned int wid; switch (*mark) { case 'u': wid = uid; goto writenum; case 'g': wid = gid; writenum: std::snprintf(buf, sizeof(buf), "%u", wid); dest += buf; break; case '%': dest.push_back(*mark); break; default: dest.push_back('%'); dest.push_back(*mark); break; } tmpl = mark + 1; } } turnstile-0.1.11/src/exec_utils.cc000066400000000000000000000327241507274676000171130ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include "turnstiled.hh" #include #include #ifdef HAVE_PAM_MISC # include # define PAM_CONV_FUNC misc_conv #else # include # define PAM_CONV_FUNC openpam_ttyconv #endif static bool exec_backend( char const *backend, char const *arg, char const *data, unsigned int uid, unsigned int gid, pid_t &outpid ) { auto pid = fork(); if (pid < 0) { /* unrecoverable */ return false; } if (pid != 0) { /* parent process */ outpid = pid; return true; } if (!backend) { /* if service manager is not managed, simply succeed immediately */ exit(0); return true; } /* child process */ if (getuid() == 0) { if (setgid(gid) != 0) { perror("srv: failed to set gid"); exit(1); } if (setuid(uid) != 0) { perror("srv: failed to set uid"); exit(1); } } char buf[sizeof(LIBEXEC_PATH) + 128]; std::snprintf(buf, sizeof(buf), LIBEXEC_PATH "/%s", backend); execl(buf, buf, arg, data, nullptr); exit(1); return true; } bool srv_boot(login &lgn, char const *backend) { print_dbg("srv: startup (ready)"); if (!exec_backend( backend, "ready", lgn.srvstr.data(), lgn.uid, lgn.gid, lgn.start_pid )) { print_err("srv: fork failed (%s)", strerror(errno)); return false; } return true; } static bool dpam_setup_groups( pam_handle_t *pamh, char const *user, unsigned int gid ) { if (initgroups(user, gid) != 0) { perror("srv: failed to set supplementary groups"); return false; } auto pst = pam_setcred(pamh, PAM_ESTABLISH_CRED); if (pst != PAM_SUCCESS) { fprintf(stderr, "srv: pam_setcred: %s", pam_strerror(pamh, pst)); pam_end(pamh, pst); return false; } return true; } static pam_handle_t *dpam_begin(char const *user, unsigned int gid) { pam_conv cnv = { PAM_CONV_FUNC, nullptr }; pam_handle_t *pamh = nullptr; auto pst = pam_start(DPAM_SERVICE, user, &cnv, &pamh); if (pst != PAM_SUCCESS) { fprintf(stderr, "srv: pam_start: %s", pam_strerror(pamh, pst)); return nullptr; } if (!dpam_setup_groups(pamh, user, gid)) { return nullptr; } return pamh; } static void sanitize_limits() { struct rlimit l{0, 0}; print_dbg("srv: sanitize rlimits"); setrlimit(RLIMIT_NICE, &l); setrlimit(RLIMIT_RTPRIO, &l); l.rlim_cur = RLIM_INFINITY; l.rlim_max = RLIM_INFINITY; setrlimit(RLIMIT_FSIZE, &l); setrlimit(RLIMIT_AS, &l); getrlimit(RLIMIT_NOFILE, &l); if (l.rlim_cur != FD_SETSIZE) { l.rlim_cur = FD_SETSIZE; setrlimit(RLIMIT_NOFILE, &l); } } static bool dpam_open(pam_handle_t *pamh) { if (!pamh) { return false; } /* before opening session, do not rely on just PAM and sanitize a bit */ sanitize_limits(); print_dbg("srv: open pam session"); auto pst = pam_open_session(pamh, 0); if (pst != PAM_SUCCESS) { fprintf(stderr, "srv: pam_open_session: %s", pam_strerror(pamh, pst)); pam_setcred(pamh, PAM_DELETE_CRED | PAM_SILENT); pam_end(pamh, pst); return false; } return true; } static void dpam_finalize(pam_handle_t *pamh) { if (!pamh) { /* when not doing PAM, at least restore umask to user default, * otherwise the PAM configuration will do it (pam_umask.so) */ umask(022); return; } /* end with success */ pam_end(pamh, PAM_SUCCESS | PAM_DATA_SILENT); } static int sigpipe[2] = {-1, -1}; static void sig_handler(int sign) { write(sigpipe[1], &sign, sizeof(sign)); } static void fork_and_wait( pam_handle_t *pamh, char const *backend, unsigned int uid, unsigned int gid ) { int pst, status; int term_count = 0; struct pollfd pfd; struct sigaction sa{}; sigset_t mask; pid_t p; /* set up event loop bits, before fork for simpler cleanup */ if (pipe(sigpipe) < 0) { perror("srv: pipe failed"); goto fail; } pfd.fd = sigpipe[0]; pfd.events = POLLIN; pfd.revents = 0; /* fork */ p = fork(); if (p == 0) { /* child, return to exec */ close(sigpipe[0]); close(sigpipe[1]); return; } else if (p < 0) { perror("srv: fork failed"); goto fail; } /* ignore signals */ sigfillset(&mask); sigdelset(&mask, SIGTERM); sigdelset(&mask, SIGCHLD); sigprocmask(SIG_SETMASK, &mask, nullptr); /* set up handlers for non-ignored signals */ sa.sa_handler = sig_handler; sa.sa_flags = SA_RESTART; sigemptyset(&sa.sa_mask); sigaction(SIGCHLD, &sa, nullptr); sigaction(SIGTERM, &sa, nullptr); /* our own little event loop */ for (;;) { auto pret = poll(&pfd, 1, -1); if (pret < 0) { /* interrupted by signal */ if (errno == EINTR) { continue; } perror("srv: poll failed"); goto fail; } else if (pret == 0) { continue; } int sign; if (read(pfd.fd, &sign, sizeof(sign)) != sizeof(sign)) { perror("srv: signal read failed"); goto fail; } if (sign == SIGTERM) { char buf[32]; pid_t outp; int st; if ((term_count++ > 1) || !backend) { /* hard kill */ kill(p, SIGKILL); continue; } std::snprintf(buf, sizeof(buf), "%zu", size_t(p)); /* otherwise run the stop part */ if (!exec_backend(backend, "stop", buf, uid, gid, outp)) { /* failed? */ perror("srv: stop exec failed, fall back to TERM"); kill(p, SIGTERM); } /* wait for it to end */ do { pid_t w = waitpid(outp, &st, 0); if (w < 0) { if (errno == EINTR) { continue; } perror("srv: stop exec wait failed"); break; } } while (!WIFEXITED(st) && !WIFSIGNALED(st)); continue; } /* SIGCHLD */ int wpid; while ((wpid = waitpid(-1, &status, WNOHANG)) > 0) { if (wpid != p) { continue; } goto done; } } done: /* close session */ if (!pamh) { goto estatus; } pst = pam_close_session(pamh, 0); if (pst != PAM_SUCCESS) { fprintf(stderr, "srv: pam_close_session: %s", pam_strerror(pamh, pst)); pam_end(pamh, pst); goto fail; } /* finalize */ pam_setcred(pamh, PAM_DELETE_CRED); pam_end(pamh, PAM_SUCCESS); estatus: /* propagate exit status */ exit(WIFEXITED(status) ? WEXITSTATUS(status) : (WTERMSIG(status) + 128)); fail: exit(1); } /* dummy "service manager" child process with none backend */ static void srv_dummy(unsigned int uid) { /* block all signals except the ones we need to terminate */ sigset_t mask; sigfillset(&mask); /* kill/stop are ignored, but term is not */ sigdelset(&mask, SIGTERM); sigprocmask(SIG_SETMASK, &mask, nullptr); /* mark as ready */ char path[4096]; std::snprintf( path, sizeof(path), "%s/%s/%u/ready", RUN_PATH, SOCK_DIR, uid ); FILE *ready = std::fopen(path, "w"); if (!ready) { perror("srv: could not open readiness fifo"); exit(1); } std::fprintf(ready, "boop\n"); std::fclose(ready); /* this will sleep until a termination signal wakes it */ pause(); /* in which case just exit */ exit(0); } void srv_child(login &lgn, char const *backend, bool make_rundir) { pam_handle_t *pamh = nullptr; bool is_root = (getuid() == 0); /* create a new session */ if (setsid() < 0) { perror("srv: setsid failed"); } /* begin pam session setup */ if (is_root) { print_dbg("srv: establish pam"); pamh = dpam_begin(lgn.username.data(), lgn.gid); if (!dpam_open(pamh)) { return; } } /* make rundir if needed, we want to make it as late as possible, ideally * after the PAM session setup is already finalized (so that nothing gets * the idea to nuke it), but before we fork and drop privileges */ if (make_rundir) { print_dbg("srv: setup rundir for %u", lgn.uid); if (!rundir_make(lgn.rundir.data(), lgn.uid, lgn.gid)) { return; } } print_dbg("srv: forking for service manager exec"); /* handle the parent/child logic here * if we're forking, only child makes it past this func */ fork_and_wait(pamh, backend, lgn.uid, lgn.gid); /* drop privs */ if (is_root) { /* change identity */ if (setgid(lgn.gid) != 0) { perror("srv: failed to set gid"); return; } if (setuid(lgn.uid) != 0) { perror("srv: failed to set uid"); return; } } /* dummy service manager if requested */ if (!backend) { srv_dummy(lgn.uid); return; } /* change directory to home, fall back to / or error */ if ((chdir(lgn.homedir.data()) < 0) && (chdir("/") < 0)) { perror("srv: failed to change directory"); return; } /* set up service manager tempdir after we drop privileges */ char tdirn[38]; std::snprintf( tdirn, sizeof(tdirn), "srv.%lu", static_cast(getpid()) ); int tdirfd = dir_make_at(lgn.dirfd, tdirn, 0700); if (tdirfd < 0) { perror("srv: failed to create state dir"); return; } close(tdirfd); /* stringify the uid/gid */ char uidbuf[32], gidbuf[32]; std::snprintf(uidbuf, sizeof(uidbuf), "%u", lgn.uid); std::snprintf(gidbuf, sizeof(gidbuf), "%u", lgn.gid); /* build up env and args list */ std::vector execs{}; std::size_t argc = 0, nexec = 0; auto add_str = [&execs, &nexec](auto &&...s) { (execs.insert(execs.end(), s, s + std::strlen(s)), ...); execs.push_back('\0'); ++nexec; }; /* path to run script, argv starts here */ add_str(LIBEXEC_PATH, "/", backend); /* arg1: action */ add_str("run"); /* arg1: ready pipe */ add_str(RUN_PATH, "/", SOCK_DIR, "/", uidbuf, "/ready"); /* arg2: srvdir */ add_str(RUN_PATH, "/", SOCK_DIR, "/", uidbuf, "/", tdirn); /* arg3: confdir */ add_str(CONF_PATH, "/backend"); argc = nexec; /* pam env vars take preference */ bool have_env_shell = false, have_env_user = false, have_env_logname = false, have_env_home = false, have_env_uid = false, have_env_gid = false, have_env_path = false, have_env_rundir = false; /* get them and loop */ if (pamh) { /* this is a copy, but we exec so it's fine to leak */ char **penv = pam_getenvlist(pamh); while (penv && *penv) { /* ugly but it's not like putenv actually does anything else */ if (!strncmp(*penv, "SHELL=", 6)) { have_env_shell = true; } else if (!strncmp(*penv, "USER=", 5)) { have_env_user = true; } else if (!strncmp(*penv, "LOGNAME=", 8)) { have_env_logname = true; } else if (!strncmp(*penv, "HOME=", 5)) { have_env_home = true; } else if (!strncmp(*penv, "UID=", 4)) { have_env_uid = true; } else if (!strncmp(*penv, "GID=", 4)) { have_env_gid = true; } else if (!strncmp(*penv, "PATH=", 5)) { have_env_path = true; } else if (!strncmp(*penv, "XDG_RUNTIME_DIR=", 16)) { have_env_rundir = true; } add_str(*penv++); } } /* add our environment defaults if not already set */ if (!have_env_shell) { add_str("SHELL=", lgn.shell.data()); } if (!have_env_user) { add_str("USER=", lgn.username.data()); } if (!have_env_logname) { add_str("LOGNAME=", lgn.username.data()); } if (!have_env_home) { add_str("HOME=", lgn.homedir.data()); } if (!have_env_uid) { add_str("UID=", uidbuf); } if (!have_env_gid) { add_str("GID=", gidbuf); } if (!have_env_path) { add_str("PATH=" _PATH_DEFPATH); } if (!lgn.rundir.empty() && !have_env_rundir) { add_str("XDG_RUNTIME_DIR=", lgn.rundir.data()); } /* make up env and arg arrays */ std::vector argp{}; { char const *execsp = execs.data(); argp.reserve(nexec + 2); for (std::size_t i = 0; i < argc; ++i) { argp.push_back(execsp); execsp += std::strlen(execsp) + 1; } argp.push_back(nullptr); for (std::size_t i = argc; i < nexec; ++i) { argp.push_back(execsp); execsp += std::strlen(execsp) + 1; } argp.push_back(nullptr); } /* finish pam before execing */ dpam_finalize(pamh); /* fire */ auto *argv = const_cast(&argp[0]); execve(argv[0], argv, argv + argc + 1); } turnstile-0.1.11/src/fs_utils.cc000066400000000000000000000140421507274676000165700ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include "turnstiled.hh" int dir_make_at(int dfd, char const *dname, mode_t mode) { int sdfd = openat(dfd, dname, O_RDONLY | O_NOFOLLOW); struct stat st; int reterr = 0; int omask = umask(0); if (fstat(sdfd, &st) || !S_ISDIR(st.st_mode)) { close(sdfd); if (mkdirat(dfd, dname, mode)) { goto ret_err; } sdfd = openat(dfd, dname, O_RDONLY | O_NOFOLLOW); if ((sdfd < 0) || (fstat(sdfd, &st) < 0)) { goto ret_err; } if (!S_ISDIR(st.st_mode)) { reterr = ENOTDIR; goto ret_err; } } else { /* dir_clear_contents closes the descriptor, we need to keep it */ int nfd; if ((fchmod(sdfd, mode) < 0) || ((nfd = dup(sdfd)) < 0)) { goto ret_err; } if (!dir_clear_contents(nfd)) { reterr = ENOTEMPTY; goto ret_err; } } umask(omask); return sdfd; ret_err: umask(omask); if (sdfd >= 0) { close(sdfd); } if (reterr) { errno = reterr; } return -1; } bool rundir_make(char *rundir, unsigned int uid, unsigned int gid) { struct stat dstat; int bfd = open("/", O_RDONLY | O_NOFOLLOW); if (bfd < 0) { print_err("rundir: failed to open root (%s)", strerror(errno)); return false; } char *dirbase = rundir + 1; char *sl = std::strchr(dirbase, '/'); print_dbg("rundir: make directory %s", rundir); /* recursively create all parent paths */ mode_t omask = umask(022); while (sl) { *sl = '\0'; print_dbg("rundir: try make parent %s", rundir); int cfd = openat(bfd, dirbase, O_RDONLY | O_NOFOLLOW); if (cfd < 0) { if (mkdirat(bfd, dirbase, 0755) == 0) { cfd = openat(bfd, dirbase, O_RDONLY | O_NOFOLLOW); } } if (cfd < 0 || fstat(cfd, &dstat) < 0) { print_err( "rundir: failed to make parent %s (%s)", rundir, strerror(errno) ); close(bfd); close(cfd); umask(omask); return false; } if (!S_ISDIR(dstat.st_mode)) { print_err("rundir: non-directory encountered at %s", rundir); close(bfd); close(cfd); umask(omask); return false; } close(bfd); bfd = cfd; *sl = '/'; dirbase = sl + 1; sl = std::strchr(dirbase, '/'); } umask(omask); /* now create rundir or at least sanitize its perms */ if ( (fstatat(bfd, dirbase, &dstat, AT_SYMLINK_NOFOLLOW) < 0) || !S_ISDIR(dstat.st_mode) ) { if (mkdirat(bfd, dirbase, 0700) < 0) { print_err( "rundir: failed to make rundir %s (%s)", rundir, strerror(errno) ); close(bfd); return false; } } else if (fchmodat(bfd, dirbase, 0700, AT_SYMLINK_NOFOLLOW) < 0) { print_err("rundir: fchmodat failed for rundir (%s)", strerror(errno)); close(bfd); return false; } if (fchownat(bfd, dirbase, uid, gid, AT_SYMLINK_NOFOLLOW) < 0) { print_err("rundir: fchownat failed for rundir (%s)", strerror(errno)); close(bfd); return false; } close(bfd); return true; } void rundir_clear(char const *rundir) { struct stat dstat; print_dbg("rundir: clear directory %s", rundir); int dfd = open(rundir, O_RDONLY | O_NOFOLLOW); /* non-existent */ if (dfd < 0) { return; } /* an error? */ if (fstat(dfd, &dstat)) { print_dbg("rundir: could not stat %s (%s)", rundir, strerror(errno)); close(dfd); return; } /* not a directory */ if (!S_ISDIR(dstat.st_mode)) { print_dbg("rundir: %s is not a directory", rundir); close(dfd); return; } if (dir_clear_contents(dfd)) { /* was empty */ rmdir(rundir); } else { print_dbg("rundir: failed to clear contents of %s", rundir); } } bool dir_clear_contents(int dfd) { if (dfd < 0) { /* silently return if an invalid file descriptor */ return false; } DIR *d = fdopendir(dfd); if (!d) { print_err("dir_clear: fdopendir failed (%s)", strerror(errno)); close(dfd); return false; } unsigned char buf[offsetof(struct dirent, d_name) + NAME_MAX + 1]; unsigned char *bufp = buf; struct dirent *dentb = nullptr, *dent = nullptr; std::memcpy(&dentb, &bufp, sizeof(dent)); for (;;) { if (readdir_r(d, dentb, &dent) < 0) { print_err("dir_clear: readdir_r failed (%s)", strerror(errno)); closedir(d); return false; } if (!dent) { break; } if ( !std::strcmp(dent->d_name, ".") || !std::strcmp(dent->d_name, "..") ) { continue; } print_dbg("dir_clear: clear %s at %d", dent->d_name, dfd); int efd = openat(dfd, dent->d_name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK); int ufl = 0; if (efd < 0) { /* this may fail e.g. for invalid sockets, we don't care */ goto do_unlink; } struct stat st; if (fstat(efd, &st) < 0) { print_err("dir_clear: fstat failed (%s)", strerror(errno)); closedir(d); return false; } if (S_ISDIR(st.st_mode)) { if (!dir_clear_contents(efd)) { closedir(d); return false; } ufl = AT_REMOVEDIR; } else { close(efd); } do_unlink: if (unlinkat(dfd, dent->d_name, ufl) < 0) { print_err("dir_clear: unlinkat failed (%s)", strerror(errno)); closedir(d); return false; } } closedir(d); return true; } turnstile-0.1.11/src/lib_api.c000066400000000000000000000023071507274676000161750ustar00rootroot00000000000000#include #include #include #include #include #include #include "lib_api.h" extern struct backend_api backend_api_turnstile; extern struct backend_api backend_api_none; /* the "current" backend is chosen once per client */ static struct backend_api *backend_api_current; /* THE API STUBS */ TURNSTILE_API void turnstile_init(void) { if (backend_api_current) { return; } if (backend_api_turnstile.active()) { backend_api_current = &backend_api_turnstile; return; } backend_api_current = &backend_api_none; } TURNSTILE_API turnstile *turnstile_new(void) { turnstile_init(); return backend_api_current->create(); } TURNSTILE_API void turnstile_free(turnstile *ts) { backend_api_current->destroy(ts); } TURNSTILE_API int turnstile_get_fd(turnstile *ts) { return backend_api_current->get_fd(ts); } TURNSTILE_API int turnstile_dispatch(turnstile *ts, int timeout) { return backend_api_current->dispatch(ts, timeout); } TURNSTILE_API int turnstile_watch_events( turnstile *ts, turnstile_event_callback cb, void *data ) { return backend_api_current->watch_events(ts, cb, data); } turnstile-0.1.11/src/lib_api.h000066400000000000000000000005701507274676000162020ustar00rootroot00000000000000#ifndef LIB_API_HH #define LIB_API_HH #include #include struct backend_api { bool (*active)(void); turnstile *(*create)(void); void (*destroy)(turnstile *ts); int (*get_fd)(turnstile *ts); int (*dispatch)(turnstile *ts, int timeout); int (*watch_events)(turnstile *ts, turnstile_event_callback cb, void *data); }; #endif turnstile-0.1.11/src/lib_backend_none.c000066400000000000000000000017431507274676000200350ustar00rootroot00000000000000#include #include "lib_api.h" typedef struct turnstile_none { int p; } turnstile_none; static bool backend_none_active(void) { return true; } static turnstile *backend_none_create(void) { turnstile_none *ret = malloc(sizeof(turnstile_none)); return (turnstile *)ret; } static void backend_none_destroy(turnstile *ts) { free(ts); } static int backend_none_get_fd(turnstile *ts) { (void)ts; return -1; } static int backend_none_dispatch(turnstile *ts, int timeout) { (void)ts; (void)timeout; return 0; } static int backend_none_watch_events( turnstile *ts, turnstile_event_callback cb, void *data ) { (void)ts; (void)cb; (void)data; return 0; } struct backend_api backend_api_none = { .active = backend_none_active, .create = backend_none_create, .destroy = backend_none_destroy, .get_fd = backend_none_get_fd, .dispatch = backend_none_dispatch, .watch_events = backend_none_watch_events, }; turnstile-0.1.11/src/lib_backend_none.h000066400000000000000000000001721507274676000200350ustar00rootroot00000000000000#ifndef LIB_BACKEND_NONE_H #define LIB_BACKEND_NONE_H #include "lib_api.h" extern backend_api backend_api_none; #endif turnstile-0.1.11/src/lib_backend_turnstile.c000066400000000000000000000040461507274676000211260ustar00rootroot00000000000000#include #include #include #include #include #include // actually a C header too #include "protocol.hh" #include "lib_api.h" typedef struct turnstile_ts { int p_fd; } turnstile_ts; static int ts_connect(void) { struct sockaddr_un saddr; int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); if (sock < 0) { return -1; } memset(&saddr, 0, sizeof(saddr)); saddr.sun_family = AF_UNIX; memcpy(saddr.sun_path, DAEMON_SOCK, sizeof(DAEMON_SOCK)); if (connect(sock, (struct sockaddr const *)&saddr, sizeof(saddr)) < 0) { return -1; } return sock; } static bool nts_connect(turnstile_ts *ts) { return ((ts->p_fd = ts_connect()) >= 0); } static bool backend_ts_active(void) { int sock = ts_connect(); if (sock < 0) { return false; } close(sock); return true; } static void backend_ts_destroy(turnstile *ts) { turnstile_ts *nts = (turnstile_ts *)ts; if (nts->p_fd >= 0) { close(nts->p_fd); } free(ts); } static turnstile *backend_ts_create(void) { turnstile_ts *ret = malloc(sizeof(turnstile_ts)); if (!ret) { return NULL; } ret->p_fd = -1; if (!nts_connect(ret)) { int serrno = errno; backend_ts_destroy((turnstile *)ret); errno = serrno; return NULL; } return (turnstile *)ret; } static int backend_ts_get_fd(turnstile *ts) { return ((turnstile_ts *)ts)->p_fd; } static int backend_ts_dispatch(turnstile *ts, int timeout) { (void)ts; (void)timeout; return 0; } static int backend_ts_watch_events( turnstile *ts, turnstile_event_callback cb, void *data ) { (void)ts; (void)cb; (void)data; return 0; } struct backend_api backend_api_turnstile = { .active = backend_ts_active, .create = backend_ts_create, .destroy = backend_ts_destroy, .get_fd = backend_ts_get_fd, .dispatch = backend_ts_dispatch, .watch_events = backend_ts_watch_events, }; turnstile-0.1.11/src/pam_turnstile.8.scd000066400000000000000000000024301507274676000201560ustar00rootroot00000000000000pam_turnstile(8) # NAME pam\_turnstile - register user sessions in *turnstiled*(8) # SYNOPSIS pam\_turnstile.so # DESCRIPTION *pam\_turnstile* registers user sessions with the main daemon, which allows them to be tracked. It communicates with the daemon over its control socket. Upon login, it opens a connection to it, and this connection lasts for as long as the login lasts. By keeping track of the connections, the daemon can be aware of the full lifetime of the session. The login will only proceed in one of the following cases: - The daemon has replied with a success. - The daemon has replied with a failure. - The connection was closed. Upon success, the daemon will have already started all user services. If that is the case, it may also initialize some environment variables: . _$DBUS\_SESSION\_BUS\_ADDRESS_ is exported assuming 'RUNDIR/bus' exists and is a valid socket, where 'RUNDIR' is the runtime directory the daemon is potentially managing. The value of the environment variable becomes _unix:path=/path/to/bus_. . _$XDG\_RUNTIME\_DIR_ is exported if the daemon's _manage\_rundir_ is enabled in the configuration. Upon success, the module returns _PAM\_SUCCESS_. In any other case, the module returns _PAM\_SESSION\_ERR_. # OPTIONS The module takes no options. turnstile-0.1.11/src/pam_turnstile.cc000066400000000000000000000376201507274676000176350ustar00rootroot00000000000000/* pam_turnstile: the client part of turnstiled * * it connects to its socket and requests logins/logouts, * communicating over a rudimentary protocol * * the PAM session opens a persistent connection, which also * takes care of tracking when a session needs ending on the * daemon side (once all connections are gone) * * Copyright 2021 q66 * License: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "protocol.hh" #include "utils.hh" #define PAMAPI __attribute__((visibility ("default"))) static void free_sock(pam_handle_t *, void *data, int) { int sock = *static_cast(data); if (sock != -1) { close(sock); } free(data); } static bool open_session( pam_handle_t *pamh, unsigned int uid, char const *service, char const *stype, char const *sclass, char const *sdesktop, char const *sseat, char const *tty, char const *display, char const *ruser, char const *rhost, unsigned long vtnr, bool remote, unsigned int &elen, char *&ebuf, bool debug ) { if (debug) { pam_syslog(pamh, LOG_DEBUG, "open session"); } int *sock = static_cast(std::malloc(sizeof(int))); if (!sock) { return false; } /* blocking socket and a simple protocol */ *sock = socket(AF_UNIX, SOCK_STREAM, 0); if (*sock == -1) { return false; } /* associate the socket with the session */ if (pam_set_data( pamh, "pam_turnstile_session", sock, free_sock ) != PAM_SUCCESS) { return false; } sockaddr_un saddr; std::memset(&saddr, 0, sizeof(saddr)); saddr.sun_family = AF_UNIX; std::memcpy(saddr.sun_path, DAEMON_SOCK, sizeof(DAEMON_SOCK)); auto send_full = [sock](void const *buf, std::size_t len) -> bool { auto *cbuf = static_cast(buf); while (len) { auto n = write(*sock, cbuf, len); if (n < 0) { if (errno == EINTR) { continue; } return false; } cbuf += n; len -= n; } return true; }; auto send_msg = [&send_full](unsigned char msg) -> bool { return send_full(&msg, sizeof(msg)); }; auto send_str = [&send_full](char const *str) -> bool { std::size_t slen = str ? strlen(str) : 0; if (!send_full(&slen, sizeof(slen))) { return false; } return send_full(str, slen); }; if (connect( *sock, reinterpret_cast(&saddr), sizeof(saddr) ) < 0) { goto err; } if (!send_msg(MSG_START)) { goto err; } /* send all the arguments */ if (!send_full(&uid, sizeof(uid))) { goto err; } if (!send_full(&vtnr, sizeof(vtnr))) { goto err; } if (!send_full(&remote, sizeof(remote))) { goto err; } if (!send_str(service)) { goto err; } if (!send_str(stype)) { goto err; } if (!send_str(sclass)) { goto err; } if (!send_str(sdesktop)) { goto err; } if (!send_str(sseat)) { goto err; } if (!send_str(tty)) { goto err; } if (!send_str(display)) { goto err; } if (!send_str(ruser)) { goto err; } if (!send_str(rhost)) { goto err; } /* main message loop */ { unsigned char msg; unsigned char state = 0; /* read an entire known-size buffer in one go */ auto recv_full = [sock](void *buf, size_t len) -> bool { auto *cbuf = static_cast(buf); while (len) { auto n = recv(*sock, cbuf, len, 0); if (n < 0) { if (errno == EINTR) { continue; } return false; } else if (n == 0) { /* eof; connection closed by peer */ return false; } cbuf += n; len -= n; } return true; }; for (;;) { if (!recv_full(&msg, sizeof(msg))) { goto err; } switch (state) { case 0: case MSG_OK_WAIT: /* if started, get the rundir back; else block * * if we previously waited and now got another message, * it means either an error or that the system is now * fully ready */ if (msg == MSG_OK_DONE) { state = msg; if (!send_msg(MSG_REQ_ENV)) { goto err; } continue; } else if ((state == 0) && (msg == MSG_OK_WAIT)) { state = msg; continue; } /* bad message */ goto err; case MSG_OK_DONE: { if (msg != MSG_ENV) { goto err; } /* after MSG_OK_DONE, we should receive the environment * length first; if zero, it means we are completely done */ if (!recv_full(&elen, sizeof(elen))) { goto err; } /* alloc the buffer */ if (elen) { ebuf = static_cast(std::malloc(elen)); if (!ebuf) { goto err; } /* followed by the environment block */ if (!recv_full(ebuf, elen)) { goto err; } } return true; } default: goto err; } } } return true; err: std::free(ebuf); close(*sock); *sock = -1; return false; } /* this may get used later for something */ static int open_session_turnstiled(pam_handle_t *pamh, bool debug) { if (debug) { pam_syslog(pamh, LOG_DEBUG, "pam_turnstile init session"); } return PAM_SUCCESS; } static unsigned long get_x_vtnr(char const *display) { /* get the server number, drop if non-local */ if (display[0] != ':') { return 0; } char *endp = nullptr; unsigned long xnum = std::strtoul(display + 1, &endp, 10); if (endp && *endp) { return 0; } int sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock < 0) { return 0; } sockaddr_un saddr; std::memset(&saddr, 0, sizeof(saddr)); saddr.sun_family = AF_UNIX; /* try abstract socket first, linux only but harmless */ std::snprintf( saddr.sun_path, sizeof(saddr.sun_path), "@/tmp/.X11-unix/X%lu", xnum ); auto *sa = reinterpret_cast(&saddr); if (connect(sock, sa, sizeof(saddr)) < 0) { /* try non-abstract socket */ std::memmove( saddr.sun_path, saddr.sun_path + 1, sizeof(saddr.sun_path ) - 1 ); /* if that fails too, drop */ if (connect(sock, sa, sizeof(saddr)) < 0) { close(sock); return 0; } } /* the xserver PID */ pid_t xpid = -1; get_peer_cred(sock, nullptr, nullptr, &xpid); close(sock); if (xpid < 0) { return 0; } return get_pid_vtnr(xpid); } static void parse_args( pam_handle_t *pamh, int argc, char const **argv, bool &debug, bool &sess, char const **cl, char const **dtop, char const **type ) { for (int i = 0; i < argc; ++i) { /* is in-session invocation */ if (!std::strcmp(argv[i], DPAM_SERVICE)) { sess = true; continue; } /* debug */ if (!std::strcmp(argv[i], "debug")) { debug = true; continue; } /* provided class */ if (!std::strncmp(argv[i], "class=", 6)) { if (cl) { *cl = argv[i] + 6; } continue; } /* provided desktop */ if (!std::strncmp(argv[i], "desktop=", 8)) { if (dtop) { *dtop = argv[i] + 8; } continue; } /* provided type */ if (!std::strncmp(argv[i], "type=", 5)) { if (type) { *type = argv[i] + 5; } continue; } /* unknown */ pam_syslog(pamh, LOG_WARNING, "unknown parameter '%s'", argv[i]); } } static char const *getenv_pam(pam_handle_t *pamh, char const *key) { auto *v = pam_getenv(pamh, key); if (v && *v) { return v; } v = getenv(key); if (v && *v) { return v; } return nullptr; } extern "C" PAMAPI int pam_sm_open_session( pam_handle_t *pamh, int, int argc, char const **argv ) { /* optional args */ bool debug = false, in_sess = false; char const *pclass = nullptr; char const *pdesktop = nullptr; char const *ptype = nullptr; /* parse the args */ parse_args(pamh, argc, argv, debug, in_sess, &pclass, &pdesktop, &ptype); /* debug */ if (debug) { pam_syslog(pamh, LOG_DEBUG, "pam_turnstile init"); } /* dual purpose */ if (in_sess) { return open_session_turnstiled(pamh, debug); } /* obtain the user */ char const *puser = nullptr; if (pam_get_user(pamh, &puser, nullptr) != PAM_SUCCESS) { pam_syslog(pamh, LOG_ERR, "could not get PAM user"); return PAM_SESSION_ERR; } passwd *pwd = getpwnam(puser); if (!pwd) { pam_syslog(pamh, LOG_ERR, "getpwnam failed (%s)", strerror(errno)); return PAM_SESSION_ERR; } /* get some pam session data */ auto get_pamitem = [pamh](int itype, char const *name, char const **item) { void const *itemv = nullptr; auto r = pam_get_item(pamh, itype, &itemv); if ((r == PAM_SUCCESS) || (r == PAM_BAD_ITEM)) { if (itemv) { *item = static_cast(itemv); } return true; } pam_syslog( pamh, LOG_ERR, "could not get PAM item: %s (%s)", name, pam_strerror(pamh, r) ); return false; }; char const *service = nullptr; if (!get_pamitem(PAM_SERVICE, "PAM_SERVICE", &service)) { return PAM_SESSION_ERR; } char const *display = nullptr; if (!get_pamitem(PAM_XDISPLAY, "PAM_XDISPLAY", &display)) { return PAM_SESSION_ERR; } char const *tty = nullptr; if (!get_pamitem(PAM_TTY, "PAM_TTY", &tty)) { return PAM_SESSION_ERR; } char const *remote_user = nullptr; if (!get_pamitem(PAM_RUSER, "PAM_RUSER", &remote_user)) { return PAM_SESSION_ERR; } char const *remote_host = nullptr; if (!get_pamitem(PAM_RHOST, "PAM_RHOST", &remote_host)) { return PAM_SESSION_ERR; } /* try obtain from environment */ char const *xclass = getenv_pam(pamh, "XDG_SESSION_CLASS"); if (!xclass) { xclass = pclass; } char const *xdesktop = getenv_pam(pamh, "XDG_SESSION_DESKTOP"); if (!xdesktop) { xdesktop = pdesktop; } char const *xtype = getenv_pam(pamh, "XDG_SESSION_TYPE"); if (!xtype) { xtype = ptype; } char const *xseat = getenv_pam(pamh, "XDG_SEAT"); char const *xvtnr = getenv_pam(pamh, "XDG_VTNR"); /* this more or less mimics logind for compatibility */ if (tty) { if (std::strchr(tty, ':')) { /* X11 display */ if (!display || !*display) { display = tty; } tty = nullptr; } else if (!std::strcmp(tty, "cron")) { xtype = "unspecified"; xclass = "background"; tty = nullptr; } else if (!std::strcmp(tty, "ssh")) { xtype = "tty"; xclass = "user"; tty = nullptr; } else if (!std::strncmp(tty, "/dev/", 5)) { tty += 5; } } unsigned long vtnr = 0; if (xvtnr) { char *endp = nullptr; vtnr = std::strtoul(xvtnr, &endp, 10); if (endp && *endp) { vtnr = 0; } } /* get vtnr from X display if possible */ if (display && *display && !vtnr) { if (!xseat || !*xseat) { /* assign default seat for X sessions if not set */ xseat = "seat0"; } vtnr = get_x_vtnr(display); } /* get vtnr from tty number if possible */ if (tty && !std::strncmp(tty, "tty", 3) && !vtnr) { char *endp = nullptr; vtnr = strtoul(tty + 3, &endp, 10); if (endp && *endp) { /* tty != "ttyN" */ vtnr = 0; } if (vtnr && (!xseat || !*xseat)) { /* assign default seat for console sessions if not set */ xseat = "seat0"; } } /* other-seat sessions cannot have vtnr */ if (xseat && std::strcmp(xseat, "seat0") && vtnr) { vtnr = 0; } if (!xtype || !*xtype) { xtype = (display && *display) ? "x11" : ( (tty && *tty) ? "tty" : "unspecified" ); } if (!xclass || !*xclass) { xclass = !std::strcmp(xtype, "unspecified") ? "background" : "user"; } bool remote = false; if (remote_host && *remote_host) { char buf[32]; auto hlen = std::strlen(remote_host); if (hlen >= sizeof(buf)) { std::memcpy(buf, remote_host + hlen - sizeof(buf) + 1, sizeof(buf)); hlen = sizeof(buf) - 1; } else { std::memcpy(buf, remote_host, hlen + 1); } /* strip trailing dot */ if (buf[hlen - 1] == '.') { buf[hlen - 1] = '\0'; } char *rdot = std::strrchr(buf, '.'); if (rdot && !strcasecmp(rdot + 1, "localdomain")) { *rdot = '\0'; } if (!strcasecmp(buf, "localhost")) { remote = true; } else { rdot = std::strrchr(buf, '.'); if (rdot && !strcasecmp(rdot + 1, "localhost")) { remote = true; } } } char *ebuf = nullptr; unsigned int elen = 0; if (!open_session( pamh, pwd->pw_uid, service, xtype, xclass, xdesktop, xseat, tty, display, remote_user, remote_host, vtnr, remote, /* output and misc parameters */ elen, ebuf, debug )) { return PAM_SESSION_ERR; } for (char *ecur = ebuf; elen;) { if (pam_putenv(pamh, ecur) != PAM_SUCCESS) { std::free(ebuf); return PAM_SESSION_ERR; } /* includes null terminator */ auto clen = std::strlen(ecur) + 1; if (elen >= clen) { ecur += clen; elen -= clen; } else { std::free(ebuf); return PAM_SESSION_ERR; } } std::free(ebuf); return PAM_SUCCESS; } extern "C" PAMAPI int pam_sm_close_session( pam_handle_t *pamh, int, int, char const ** ) { void const *data; /* there is nothing we can do here */ if (pam_get_data(pamh, "pam_turnstile_session", &data) != PAM_SUCCESS) { return PAM_SUCCESS; } int sock = *static_cast(data); if (sock < 0) { return PAM_SUCCESS; } /* close the session */ close(sock); return PAM_SUCCESS; } turnstile-0.1.11/src/protocol.hh000066400000000000000000000033211507274676000166110ustar00rootroot00000000000000/* defines the simple protocol between the daemon and the PAM module * * Copyright 2021 q66 * License: BSD-2-Clause */ #ifndef TURNSTILED_PROTOCOL_HH #define TURNSTILED_PROTOCOL_HH #include #include "config.hh" #ifndef RUN_PATH #error "No RUN_PATH is defined" #endif #define DPAM_SERVICE "turnstiled" #define SOCK_DIR DPAM_SERVICE #define DAEMON_SOCK RUN_PATH "/" SOCK_DIR "/control.sock" /* protocol messages * * this is a simple stream protocol; there are messages which fit within * a single byte, optionally followed by message-specific data bytes * * turnstiled is the server; the pam module is the client * * the client connects to DAEMON_SOCK * * from there, the following sequence happens: * * CLIENT: sends MSG_START, followed by uid (unsigned int), and enters a * message loop (state machine) * SERVER: if service manager for the user is already running, responds * with MSG_OK_DONE; else initiates startup and responds MSG_OK_WAIT * CLIENT: if MSG_OK_WAIT was received, waits for another message * SERVER: once service manager starts, MSG_OK_DONE is sent * CLIENT: sends MSG_REQ_ENV * SERVER: responds with MSG_ENV, followed by length of the environment * block (unsigned int) followed by the environment data, which * is a sequence of null-terminated strings * CLIENT: finishes startup, exports each variable in the received env * block and finalizes session */ /* byte-sized message identifiers */ enum { MSG_OK_WAIT = 0x1, /* login, wait */ MSG_OK_DONE, /* ready, proceed */ MSG_REQ_ENV, /* session environment request */ MSG_ENV, MSG_START, /* sent by server on errors */ MSG_ERR, }; #endif turnstile-0.1.11/src/turnstiled.8.scd000066400000000000000000000024401507274676000174660ustar00rootroot00000000000000turnstiled(8) # NAME turnstiled - the main session management daemon # SYNOPSIS *turnstiled* [config_path] # DESCRIPTION *turnstiled* is a daemon that tracks user sessions and optionally spawns and manages service managers for them. For configuration, see *turnstiled.conf*(5). Upon user login, it spawns an instance of the chosen service manager for the user, while upon last logout, it shuts down this instance (unless configured to longer). User logins and logouts are communicated via *pam\_turnstile*(8). The daemon itself takes no options other than possibly a configuration file path as its sole argument. If not provided, the default path is used, typically _/etc/turnstile/turnstiled.conf_. # XDG\_RUNTIME\_DIR MANAGEMENT The daemon can also serve as the manager of the _$XDG\_RUNTIME\_DIR_ environment variable and directory. # ENVIRONMENT *TURNSTILED\_LINGER\_ENABLE\_FORCE* If set during daemon startup (to any value), enable lingering even if rundir management is disabled. This is primarily for people who want to use the linger functionality and have worked around the problem in their own rundir management system. Enabling this without having worked around the problem may lead to unfortunate consequences. Of course, lingering still has to be enabled in the configuration. turnstile-0.1.11/src/turnstiled.cc000066400000000000000000001306641507274676000171460ustar00rootroot00000000000000/* turnstiled: handle incoming login requests and start (or * stop) service manager instances as necessary * * the daemon should never exit under "normal" circumstances * * Copyright 2021 q66 * License: BSD-2-Clause */ #ifndef _GNU_SOURCE #define _GNU_SOURCE /* accept4 */ #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "turnstiled.hh" #include "utils.hh" #ifndef CONF_PATH #error "No CONF_PATH is defined" #endif /* we accept connections from non-root * * this relies on non-portable credentials checking, * so it must be implemented for every system separately * * it would be nice to get this implemented on other systems */ #define CSOCK_MODE 0666 #define DEFAULT_CFG_PATH CONF_PATH "/turnstiled.conf" /* when stopping service manager, we first do a SIGTERM and set up this * timeout, if it fails to quit within that period, we issue a SIGKILL * and try this timeout again, after that it is considered unrecoverable */ static constexpr std::time_t kill_timeout = 60; /* global */ cfg_data *cdata = nullptr; /* the file descriptor for the base directory */ static int dirfd_base = -1; /* the file descriptor for the users directory */ static int dirfd_users = -1; /* the file descriptor for the sessions directory */ static int dirfd_sessions = -1; static bool write_udata(login const &lgn); static bool write_sdata(session const &sess); static void drop_udata(login const &lgn); static void drop_sdata(session const &sess); login::login() { timer_sev.sigev_notify = SIGEV_SIGNAL; timer_sev.sigev_signo = SIGALRM; timer_sev.sigev_value.sival_ptr = this; srvstr.reserve(256); } void login::remove_sdir() { char buf[32]; std::snprintf(buf, sizeof(buf), "%u", this->uid); unlinkat(dirfd_base, buf, AT_REMOVEDIR); /* just in case, we know this is a named pipe */ unlinkat(this->dirfd, "ready", 0); dir_clear_contents(this->dirfd); this->dirfd = -1; } bool login::arm_timer(std::time_t timeout) { if (timer_create(CLOCK_MONOTONIC, &timer_sev, &timer) < 0) { print_err("timer: timer_create failed (%s)", strerror(errno)); return false; } itimerspec tval{}; tval.it_value.tv_sec = timeout; if (timer_settime(timer, 0, &tval, nullptr) < 0) { print_err("timer: timer_settime failed (%s)", strerror(errno)); timer_delete(timer); return false; } timer_armed = true; return true; } void login::disarm_timer() { if (!timer_armed) { return; } timer_delete(timer); timer_armed = false; } static std::vector logins; /* file descriptors for poll */ static std::vector fds; /* connections pending a session */ static std::vector pending_sess; /* number of pipes we are polling on */ static std::size_t npipes = 0; /* control IPC socket */ static int ctl_sock; /* signal self-pipe */ static int sigpipe[2] = {-1, -1}; /* session counter, each session gets a new number (i.e. numbers never * get reused even if the session of that number dies); session numbers * are unique even across logins */ static unsigned long idbase = 0; /* start the service manager instance for a login */ static bool srv_start(login &lgn) { /* prepare some strings */ char uidbuf[32]; std::snprintf(uidbuf, sizeof(uidbuf), "%u", lgn.uid); /* mark as waiting */ lgn.srv_wait = true; /* set up login dir */ print_dbg("srv: create login dir for %u", lgn.uid); /* make the directory itself */ lgn.dirfd = dir_make_at(dirfd_base, uidbuf, 0700); if (lgn.dirfd < 0) { print_err( "srv: failed to make login dir for %u (%s)", lgn.uid, strerror(errno) ); return false; } /* ensure it's owned by the user */ if (fchownat( dirfd_base, uidbuf, lgn.uid, lgn.gid, AT_SYMLINK_NOFOLLOW ) || fcntl(lgn.dirfd, F_SETFD, FD_CLOEXEC)) { print_err( "srv: login dir setup failed for %u (%s)", lgn.uid, strerror(errno) ); lgn.remove_sdir(); return false; } print_dbg("srv: create readiness pipe"); unlinkat(lgn.dirfd, "ready", 0); if (mkfifoat(lgn.dirfd, "ready", 0700) < 0) { print_err("srv: failed to make ready pipe (%s)", strerror(errno)); return false; } /* ensure it's owned by user too, and open in nonblocking mode */ if (fchownat( lgn.dirfd, "ready", lgn.uid, lgn.gid, AT_SYMLINK_NOFOLLOW ) || ((lgn.userpipe = openat( lgn.dirfd, "ready", O_NONBLOCK | O_RDONLY )) < 0)) { print_err( "srv: failed to set up ready pipe (%s)", strerror(errno) ); unlinkat(lgn.dirfd, "ready", 0); lgn.remove_sdir(); return false; } /* set up the timer, issue SIGLARM when it fires */ print_dbg("srv: timer set"); if (cdata->login_timeout > 0) { if (!lgn.arm_timer(cdata->login_timeout)) { return false; } } else { print_dbg("srv: no timeout"); } /* launch service manager */ print_dbg("srv: launch"); auto pid = fork(); if (pid == 0) { /* reset signals from parent */ struct sigaction sa{}; sa.sa_handler = SIG_DFL; sa.sa_flags = SA_RESTART; sigemptyset(&sa.sa_mask); sigaction(SIGCHLD, &sa, nullptr); sigaction(SIGALRM, &sa, nullptr); sigaction(SIGTERM, &sa, nullptr); sigaction(SIGINT, &sa, nullptr); /* close some descriptors, these can be reused */ close(lgn.userpipe); close(dirfd_base); close(sigpipe[0]); close(sigpipe[1]); /* and run the login */ bool has_backend = !cdata->disable && ( (lgn.uid != 0) || cdata->root_session ); srv_child( lgn, has_backend ? cdata->backend.data() : nullptr, cdata->manage_rdir ); exit(1); } else if (pid < 0) { print_err("srv: fork failed (%s)", strerror(errno)); return false; } /* close the write end on our side */ lgn.srv_pending = false; lgn.srv_pid = pid; if (lgn.userpipe < 0) { /* disabled */ return srv_boot(lgn, nullptr); } /* otherwise queue the pipe */ lgn.pipe_queued = true; return true; } static session *get_session(int fd) { for (auto &lgn: logins) { for (auto &sess: lgn.sessions) { if (fd == sess.fd) { return &sess; } } } print_dbg("msg: no session for %d", fd); return nullptr; } static login *login_populate(unsigned int uid) { login *lgn = nullptr; for (auto &lgnr: logins) { if (lgnr.uid == uid) { if (!lgnr.repopulate) { print_dbg("msg: using existing login %u", uid); return &lgnr; } lgn = &lgnr; break; } } auto *pwd = getpwuid(uid); if (!pwd) { print_err("msg: failed to get pwd for %u (%s)", uid, strerror(errno)); return nullptr; } if (pwd->pw_dir[0] != '/') { print_err( "msg: homedir of %s (%u) is not absolute (%s)", pwd->pw_name, uid, pwd->pw_dir ); return nullptr; } if (lgn) { print_dbg("msg: repopulate login %u", pwd->pw_uid); } else { print_dbg("msg: init login %u", pwd->pw_uid); lgn = &logins.emplace_back(); } /* fill in initial login details */ lgn->uid = pwd->pw_uid; lgn->gid = pwd->pw_gid; lgn->username = pwd->pw_name; lgn->homedir = pwd->pw_dir; lgn->shell = pwd->pw_shell; lgn->rundir.clear(); /* somewhat heuristical */ lgn->rundir.reserve(cdata->rdir_path.size() + 8); cfg_expand_rundir(lgn->rundir, cdata->rdir_path.data(), lgn->uid, lgn->gid); lgn->manage_rdir = cdata->manage_rdir && !lgn->rundir.empty(); lgn->repopulate = false; return lgn; } static session *handle_session_new(int fd, unsigned int uid) { /* check for credential mismatch */ uid_t puid; pid_t lpid; if (!get_peer_cred(fd, &puid, nullptr, &lpid)) { print_dbg("msg: could not get peer credentials"); return nullptr; } if (puid != 0) { print_dbg("msg: can't set up session (permission denied)"); return nullptr; } /* acknowledge the login */ print_dbg("msg: welcome %u", uid); auto *lgn = login_populate(uid); if (!lgn) { return nullptr; } /* check the sessions */ for (auto &sess: lgn->sessions) { if (sess.fd == fd) { print_dbg("msg: already have session for %u/%d", lgn->uid, fd); return nullptr; } } print_dbg("msg: new session for %u/%d", lgn->uid, fd); /* create a new session */ auto &sess = lgn->sessions.emplace_back(); sess.fd = fd; sess.id = ++idbase; sess.lgn = lgn; sess.lpid = lpid; /* initial message */ sess.needed = 1; /* reply */ return &sess; } static bool write_udata(login const &lgn) { char uname[32], tmpname[32]; std::snprintf(tmpname, sizeof(tmpname), "%u.tmp", lgn.uid); std::snprintf(uname, sizeof(uname), "%u", lgn.uid); int omask = umask(0); int lgnfd = openat( dirfd_users, tmpname, O_CREAT | O_TRUNC | O_WRONLY, 0644 ); if (lgnfd < 0) { print_err("msg: user tmpfile failed (%s)", strerror(errno)); umask(omask); return false; } umask(omask); auto *lgnf = fdopen(lgnfd, "w"); if (!lgnf) { print_err("msg: user fdopen failed (%s)", strerror(errno)); close(lgnfd); return false; } std::fprintf( lgnf, "NAME=%s\n" "RUNTIME=%s\n", lgn.username.data(), lgn.rundir.data() ); std::fprintf(lgnf, "SESSIONS="); bool first = true; for (auto &s: lgn.sessions) { if (!first) { std::fprintf(lgnf, " "); } std::fprintf(lgnf, "%lu", s.id); first = false; } std::fprintf(lgnf, "\nSEATS="); first = true; for (auto &s: lgn.sessions) { if (!first) { std::fprintf(lgnf, " "); } if (s.s_seat.empty()) { continue; } std::fprintf(lgnf, "%s", s.s_seat.data()); first = false; } std::fprintf(lgnf, "\n"); /* done writing */ std::fclose(lgnf); /* now rename to real file */ if (renameat(dirfd_users, tmpname, dirfd_users, uname) < 0) { print_err("msg: user renameat failed (%s)", strerror(errno)); unlinkat(dirfd_users, tmpname, 0); return false; } return true; } static bool write_sdata(session const &sess) { char sessname[32], tmpname[32]; std::snprintf(tmpname, sizeof(tmpname), "%lu.tmp", sess.id); std::snprintf(sessname, sizeof(sessname), "%lu", sess.id); auto &lgn = *sess.lgn; int omask = umask(0); int sessfd = openat( dirfd_sessions, tmpname, O_CREAT | O_TRUNC | O_WRONLY, 0644 ); if (sessfd < 0) { print_err("msg: session tmpfile failed (%s)", strerror(errno)); umask(omask); return false; } umask(omask); auto *sessf = fdopen(sessfd, "w"); if (!sessf) { print_err("msg: session fdopen failed (%s)", strerror(errno)); close(sessfd); return false; } /* now write all the session data */ std::fprintf( sessf, "UID=%u\n" "USER=%s\n", lgn.uid, lgn.username.data() ); if (sess.vtnr) { std::fprintf(sessf, "IS_DISPLAY=1\n"); } std::fprintf(sessf, "REMOTE=%d\n", int(sess.remote)); std::fprintf(sessf, "TYPE=%s\n", sess.s_type.data()); std::fprintf(sessf, "ORIGINAL_TYPE=%s\n", sess.s_type.data()); std::fprintf(sessf, "CLASS=%s\n", sess.s_class.data()); if (!sess.s_seat.empty()) { std::fprintf(sessf, "SEAT=%s\n", sess.s_seat.data()); } if (!sess.s_tty.empty()) { std::fprintf(sessf, "TTY=%s\n", sess.s_tty.data()); } if (!sess.s_service.empty()) { std::fprintf(sessf, "SERVICE=%s\n", sess.s_service.data()); } if (sess.vtnr) { std::fprintf(sessf, "VTNR=%lu\n", sess.vtnr); } std::fprintf(sessf, "LEADER=%ld\n", long(sess.lpid)); /* done writing */ std::fclose(sessf); /* now rename to real file */ if (renameat(dirfd_sessions, tmpname, dirfd_sessions, sessname) < 0) { print_err("msg: session renameat failed (%s)", strerror(errno)); unlinkat(dirfd_sessions, tmpname, 0); return false; } return write_udata(lgn); } static void drop_udata(login const &lgn) { char lgname[64]; std::snprintf(lgname, sizeof(lgname), "%u", lgn.uid); unlinkat(dirfd_users, lgname, 0); } static void drop_sdata(session const &sess) { char sessname[64]; std::snprintf(sessname, sizeof(sessname), "%lu", sess.id); unlinkat(dirfd_sessions, sessname, 0); } static bool sock_block(int fd, short events) { if (errno == EINTR) { return true; } else if ((errno != EAGAIN) && (errno != EWOULDBLOCK)) { return false; } /* re-poll */ struct pollfd pfd; pfd.fd = fd; pfd.events = events; pfd.revents = 0; for (;;) { auto pret = poll(&pfd, 1, -1); if (pret < 0) { if (errno == EINTR) { continue; } return false; } else if (pret == 0) { continue; } break; } return true; } static bool send_full(int fd, void const *buf, size_t len) { auto *cbuf = static_cast(buf); while (len) { auto ret = send(fd, cbuf, len, 0); if (ret < 0) { if (sock_block(fd, POLLOUT)) { continue; } print_err("msg: send failed (%s)", strerror(errno)); return false; } cbuf += ret; len -= ret; } return true; } static bool send_msg(int fd, unsigned char msg) { if (!send_full(fd, &msg, sizeof(msg))) { return false; } return (msg != MSG_ERR); } static bool recv_val(int fd, void *buf, size_t sz) { auto ret = recv(fd, buf, sz, 0); if (ret < 0) { if (errno == EINTR) { return recv_val(fd, buf, sz); } print_err("msg: recv failed (%s)", strerror(errno)); } if (size_t(ret) != sz) { print_err("msg: partial recv despite peek"); return false; } return true; } static bool recv_str( session &sess, std::string &outs, unsigned int minlen, unsigned int maxlen ) { char buf[1024]; if (!sess.str_left) { print_dbg("msg: str start"); outs.clear(); size_t slen; if (!recv_val(sess.fd, &slen, sizeof(slen))) { return false; } if ((slen < minlen) || (slen > maxlen)) { print_err("msg: invalid string length"); return false; } sess.str_left = slen; /* we are awaiting string, which may come in arbitrary chunks */ sess.needed = 0; return true; } auto left = sess.str_left; if (left > sizeof(buf)) { left = sizeof(buf); } auto ret = recv(sess.fd, buf, left, 0); if (ret < 0) { if (errno == EINTR) { return recv_str(sess, outs, minlen, maxlen); } else if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { return true; } return false; } outs.append(buf, ret); sess.str_left -= ret; return true; } static bool handle_read(int fd) { int sess_needed; /* try get existing session */ auto *sess = get_session(fd); int *pidx = nullptr; /* no session: initialize one, expect initial data */ if (!sess) { sess_needed = sizeof(unsigned char); for (auto &pfd: pending_sess) { if (pfd == fd) { pidx = &pfd; sess_needed = sizeof(unsigned int); break; } } } else { sess_needed = sess->needed; } /* check if we have enough data, otherwise re-poll */ if (sess_needed) { int avail; auto ret = ioctl(fd, FIONREAD, &avail); if (ret < 0) { print_err("msg: ioctl failed (%s)", strerror(errno)); return false; } if (avail < sess_needed) { return true; } } /* must be an initial message */ if (!sess && !pidx) { unsigned char msg; if (!recv_val(fd, &msg, sizeof(msg))) { return false; } if (msg != MSG_START) { /* unexpected message */ print_err("msg: expected MSG_START, got %u", msg); return false; } pending_sess.push_back(fd); return true; } /* pending a uid */ if (!sess) { unsigned int uid; /* drop from pending */ pending_sess.erase(pending_sess.begin() + (pidx - &pending_sess[0])); /* now receive uid */ if (!recv_val(fd, &uid, sizeof(uid))) { return false; } sess = handle_session_new(fd, uid); if (!sess) { return send_msg(fd, MSG_ERR); } /* expect vtnr */ sess->needed = sizeof(unsigned long); return true; } /* handle the right section of handshake */ if (sess->handshake) { if (sess->pend_vtnr) { print_dbg("msg: get session vtnr"); if (!recv_val(fd, &sess->vtnr, sizeof(sess->vtnr))) { return false; } /* remote */ sess->needed = sizeof(bool); sess->pend_vtnr = 0; return true; } if (sess->pend_remote) { print_dbg("msg: get remote"); if (!recv_val(fd, &sess->remote, sizeof(sess->remote))) { return false; } /* service str */ sess->needed = sizeof(size_t); sess->pend_remote = 0; return true; } #define GET_STR(type, min, max, code) \ if (sess->pend_##type) { \ print_dbg("msg: get " #type); \ if (!recv_str(*sess, sess->s_##type, min, max)) { \ return false; \ } \ if (!sess->str_left) { \ sess->pend_##type = false; \ /* we are waiting for length of next string */ \ sess->needed = sizeof(size_t); \ print_dbg("msg: got \"%s\"", sess->s_##type.data()); \ code \ } \ return true; \ } GET_STR(service, 1, 64,) GET_STR(type, 1, 16,) GET_STR(class, 1, 16,) GET_STR(desktop, 0, 64,) GET_STR(seat, 0, 32,) GET_STR(tty, 0, 16,) GET_STR(display, 0, 16,) GET_STR(ruser, 0, 256,) GET_STR(rhost, 0, 256, goto handshake_finish;) #undef GET_STR /* should be unreachable */ print_dbg("msg: unreachable handshake"); return false; } handshake_finish: if (sess->handshake) { /* from this point the protocol is byte-sized messages only */ sess->needed = sizeof(unsigned char); sess->handshake = 0; /* finish startup */ if (!sess->lgn->srv_wait) { /* already started, reply with ok */ print_dbg("msg: done"); /* establish internal session file */ if (!write_sdata(*sess)) { return false; } if (!send_msg(fd, MSG_OK_DONE)) { return false; } } else { if (sess->lgn->srv_pid == -1) { if (sess->lgn->term_pid != -1) { /* still waiting for old service manager to die */ print_dbg("msg: still waiting for old srv term"); sess->lgn->srv_pending = true; } else { print_dbg("msg: start service manager"); if (!srv_start(*sess->lgn)) { return false; } /* establish internal session file */ if (!write_sdata(*sess)) { return false; } } } print_dbg("msg: wait"); return send_msg(fd, MSG_OK_WAIT); } return true; } /* get msg */ unsigned char msg; if (!recv_val(fd, &msg, sizeof(msg))) { return false; } if (msg != MSG_REQ_ENV) { print_err("msg: invalid message %u (%d)", msg, fd); return false; } print_dbg("msg: session environment request"); /* data message */ if (!send_msg(fd, MSG_ENV)) { return false; } unsigned int rlen = sess->lgn->rundir.size(); if (!rlen) { /* no rundir means no env, send a zero */ print_dbg("msg: no rundir, not sending env"); return send_full(fd, &rlen, sizeof(rlen)); } /* we have a rundir, compute an environment block */ unsigned int elen = 0; bool got_bus = false; /* declare some constants we need */ char const dpfx[] = "DBUS_SESSION_BUS_ADDRESS=unix:path="; char const rpfx[] = "XDG_RUNTIME_DIR="; char const dsfx[] = "/bus"; /* we can optionally export session bus address */ if (cdata->export_dbus) { /* check if the session bus socket exists */ struct stat sbuf; /* first get the rundir descriptor */ int rdirfd = open(sess->lgn->rundir.data(), O_RDONLY | O_NOFOLLOW); if (rdirfd >= 0) { if ( !fstatat(rdirfd, "bus", &sbuf, AT_SYMLINK_NOFOLLOW) && S_ISSOCK(sbuf.st_mode) ) { /* the bus socket exists */ got_bus = true; /* includes null terminator */ elen += sizeof(dpfx) + sizeof(dsfx) - 1; elen += rlen; } close(rdirfd); } } /* we can also export rundir if we're managing it */ if (cdata->manage_rdir) { /* includes null terminator */ elen += sizeof("XDG_RUNTIME_DIR="); elen += rlen; } /* send the total length */ print_dbg("msg: send len: %u", elen); if (!send_full(fd, &elen, sizeof(elen))) { return false; } auto &rdir = sess->lgn->rundir; /* now send rundir if we have it */ if (cdata->manage_rdir) { if (!send_full(fd, rpfx, sizeof(rpfx) - 1)) { return false; } /* includes null terminator */ if (!send_full(fd, rdir.data(), rdir.size() + 1)) { return false; } } /* now send bus if we have it */ if (got_bus) { if (!send_full(fd, dpfx, sizeof(dpfx) - 1)) { return false; } if (!send_full(fd, rdir.data(), rdir.size())) { return false; } /* includes null terminator */ if (!send_full(fd, dsfx, sizeof(dsfx))) { return false; } } print_dbg("msg: sent env, done"); /* we've sent all */ return true; } struct sig_data { int sign; void *datap; }; static void sig_handler(int sign) { sig_data d; d.sign = sign; d.datap = nullptr; write(sigpipe[1], &d, sizeof(d)); } static void timer_handler(int sign, siginfo_t *si, void *) { sig_data d; d.sign = sign; d.datap = si->si_value.sival_ptr; write(sigpipe[1], &d, sizeof(d)); } static bool check_linger(login const &lgn) { if (cdata->linger_never) { return false; } if (cdata->linger) { return true; } int dfd = open(LINGER_PATH, O_RDONLY); if (dfd < 0) { return false; } struct stat lbuf; bool ret = (!fstatat( dfd, lgn.username.data(), &lbuf, AT_SYMLINK_NOFOLLOW ) && S_ISREG(lbuf.st_mode)); close(dfd); return ret; } static bool init_linger() { if (cdata->linger_never) { return false; } auto dfd = open(LINGER_PATH, O_RDONLY); if (dfd < 0) { return false; } auto dfdup = dup(dfd); if (dfdup < 0) { close(dfd); return false; } auto *dir = fdopendir(dfdup); if (!dir) { close(dfd); return false; } bool queued = false; for (;;) { struct stat lbuf; errno = 0; auto *p = readdir(dir); if (!p) { if (errno) { print_err( "turnstiled: failed to pre-linger all logins (%s)", strerror(errno) ); } break; } if ((p->d_name[0] == '.') && ((p->d_name[1] == '.') || !p->d_name[1])) { continue; } switch (p->d_type) { case DT_UNKNOWN: /* fall back to stat */ if ( fstatat(dfd, p->d_name, &lbuf, AT_SYMLINK_NOFOLLOW) || !S_ISREG(lbuf.st_mode) ) { continue; } break; case DT_REG: /* ok */ break; default: /* wrong type */ continue; } auto *pwd = getpwnam(p->d_name); if (!pwd) { continue; } auto *lgn = login_populate(pwd->pw_uid); if (lgn) { if (srv_start(*lgn)) { queued = true; } } else { print_err( "turnstiled: failed to populate login for %u", static_cast(pwd->pw_uid) ); } } close(dfd); closedir(dir); return queued; } /* terminate given conn, but only if within login */ static bool conn_term_login(login &lgn, int conn) { for (auto cit = lgn.sessions.begin(); cit != lgn.sessions.end(); ++cit) { if (cit->fd != conn) { continue; } print_dbg("conn: close %d for login %u", conn, lgn.uid); drop_sdata(*cit); lgn.sessions.erase(cit); write_udata(lgn); /* empty now; shut down login */ if (lgn.sessions.empty() && !check_linger(lgn)) { print_dbg("srv: stop"); if (lgn.srv_pid != -1) { print_dbg("srv: term"); kill(lgn.srv_pid, SIGTERM); lgn.term_pid = lgn.srv_pid; /* just in case */ lgn.arm_timer(kill_timeout); } else { /* if no service manager, drop the dir early; otherwise * wait because we need to remove the boot service first */ lgn.remove_sdir(); drop_udata(lgn); } lgn.srv_pid = -1; lgn.start_pid = -1; lgn.srv_wait = true; } close(conn); return true; } return false; } static void conn_term(int conn) { for (auto &lgn: logins) { if (conn_term_login(lgn, conn)) { return; } } /* wasn't a session, may be pending */ for (auto it = pending_sess.begin(); it != pending_sess.end(); ++it) { if (*it == conn) { pending_sess.erase(it); break; } } /* in any case, close */ close(conn); } static bool sock_new(char const *path, int &sock, mode_t mode) { sock = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); if (sock < 0) { print_err("socket failed (%s)", strerror(errno)); return false; } /* set buffers */ int bufsz = 4096; if (setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &bufsz, sizeof(bufsz)) < 0) { print_err("setssockopt failed (%s)", strerror(errno)); } if (setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &bufsz, sizeof(bufsz)) < 0) { print_err("setssockopt failed (%s)", strerror(errno)); } print_dbg("socket: created %d for %s", sock, path); sockaddr_un un; std::memset(&un, 0, sizeof(un)); un.sun_family = AF_UNIX; auto plen = std::strlen(path); if (plen >= sizeof(un.sun_path)) { print_err("socket: path name %s too long", path); close(sock); return false; } std::memcpy(un.sun_path, path, plen + 1); /* no need to check this */ unlink(path); if (bind(sock, reinterpret_cast(&un), sizeof(un)) < 0) { print_err("bind failed (%s)", strerror(errno)); close(sock); return false; } print_dbg("socket: bound %d for %s", sock, path); if (chmod(path, mode) < 0) { print_err("chmod failed (%s)", strerror(errno)); goto fail; } print_dbg("socket: permissions set"); if (listen(sock, SOMAXCONN) < 0) { print_err("listen failed (%s)", strerror(errno)); goto fail; } print_dbg("socket: listen"); print_dbg("socket: done"); return true; fail: unlink(path); close(sock); return false; } static bool drop_login(login &lgn) { /* terminate all connections belonging to this login */ print_dbg("turnstiled: drop login %u", lgn.uid); for (std::size_t j = 2; j < fds.size(); ++j) { if (conn_term_login(lgn, fds[j].fd)) { fds[j].fd = -1; fds[j].revents = 0; } } /* mark the login to repopulate from passwd */ lgn.repopulate = true; /* this should never happen unless we have a bug */ if (!lgn.sessions.empty()) { print_err("turnstiled: sessions not empty, it should be"); /* unrecoverable */ return false; } return true; } static bool sig_handle_term() { print_dbg("turnstiled: term"); bool succ = true; /* close the control socket */ close(ctl_sock); /* drop logins */ for (auto &lgn: logins) { if (!drop_login(lgn)) { succ = false; } } /* shrink the descriptor list to just signal pipe */ fds.resize(1); return succ; } static bool sig_handle_alrm(void *data) { print_dbg("turnstiled: sigalrm"); auto &lgn = *static_cast(data); /* disarm the timer if armed */ if (lgn.timer_armed) { print_dbg("turnstiled: drop timer"); lgn.disarm_timer(); } else { print_dbg("turnstiled: spurious alarm, ignoring"); return true; } if (lgn.term_pid != -1) { if (lgn.kill_tried) { print_err( "turnstiled: service manager process %ld refused to die", static_cast(lgn.term_pid) ); return false; } /* waiting for service manager to die and it did not die, try again * this will propagate as SIGKILL in the double-forked process */ kill(lgn.term_pid, SIGTERM); lgn.kill_tried = true; /* re-arm the timer, if that fails again, we give up */ lgn.arm_timer(kill_timeout); return true; } /* terminate all connections belonging to this login */ return drop_login(lgn); } /* this is called upon receiving a SIGCHLD * * can happen for 3 things: * * the service manager instance which is still supposed to be running, in * which case we attempt to restart it (except if it never signaled readiness, * in which case we give up, as we'd likely loop forever) * * the readiness job, which waits for the bootup to finish, and is run once * the service manager has opened its control socket; in those cases we notify * all pending connections and disarm the timeout (and mark the login ready) * * or the service manager instance which has stopped (due to logout typically), * in which case we take care of removing the generated service directory and * possibly clear the rundir (if managed) */ static bool srv_reaper(pid_t pid) { print_dbg("srv: reap %u", (unsigned int)pid); for (auto &lgn: logins) { if (pid == lgn.srv_pid) { lgn.srv_pid = -1; lgn.start_pid = -1; /* we don't care anymore */ lgn.disarm_timer(); if (lgn.srv_wait) { /* failed without ever having signaled readiness * let the login proceed but indicate an error */ print_err("srv: died without notifying readiness"); /* clear rundir if needed */ if (lgn.manage_rdir) { rundir_clear(lgn.rundir.data()); lgn.manage_rdir = false; } return drop_login(lgn); } return srv_start(lgn); } else if (pid == lgn.start_pid) { /* reaping service startup jobs */ print_dbg("srv: ready notification"); for (auto &sess: lgn.sessions) { send_msg(sess.fd, MSG_OK_DONE); } /* disarm an associated timer */ print_dbg("srv: disarm timer"); lgn.disarm_timer(); lgn.start_pid = -1; lgn.srv_wait = false; } else if (pid == lgn.term_pid) { /* if there was a timer on the login, safe to drop it now */ lgn.disarm_timer(); lgn.remove_sdir(); /* clear rundir if needed */ if (lgn.manage_rdir) { rundir_clear(lgn.rundir.data()); lgn.manage_rdir = false; } /* mark to repopulate if there are no sessions */ if (lgn.sessions.empty()) { drop_udata(lgn); lgn.repopulate = true; } lgn.term_pid = -1; lgn.kill_tried = false; if (lgn.srv_pending) { return srv_start(lgn); } } } return true; } static bool sig_handle_chld() { pid_t wpid; int status; print_dbg("turnstiled: sigchld"); /* reap */ while ((wpid = waitpid(-1, &status, WNOHANG)) > 0) { /* deal with each pid here */ if (!srv_reaper(wpid)) { print_err( "turnstiled: failed to restart service manager (%u)\n", static_cast(wpid) ); /* this is an unrecoverable condition */ return false; } } return true; } static bool fd_handle_pipe(std::size_t i) { if (fds[i].revents == 0) { return true; } /* find if this is a pipe */ login *lgn = nullptr; for (auto &lgnr: logins) { if (fds[i].fd == lgnr.userpipe) { lgn = &lgnr; break; } } if (!lgn) { /* this should never happen */ return false; } bool done = false; if (fds[i].revents & POLLIN) { /* read the string from the pipe */ for (;;) { char c; if (read(fds[i].fd, &c, 1) != 1) { break; } if ((c == '\0') || (lgn->srvstr.size() >= PATH_MAX)) { /* done receiving */ done = true; break; } lgn->srvstr.push_back(c); } } if (done || (fds[i].revents & POLLHUP)) { print_dbg("pipe: close"); /* kill the pipe, we don't need it anymore */ close(lgn->userpipe); lgn->userpipe = -1; /* just in case */ lgn->pipe_queued = false; fds[i].fd = -1; fds[i].revents = 0; --npipes; /* unlink the pipe */ unlinkat(lgn->dirfd, "ready", 0); print_dbg("pipe: gone"); /* wait for the boot service to come up */ if (!srv_boot(*lgn, cdata->backend.data())) { /* this is an unrecoverable condition */ return false; } /* reset the buffer for next time */ lgn->srvstr.clear(); } return true; } static bool fd_handle_conn(std::size_t i) { if (fds[i].revents == 0) { return true; } if (fds[i].revents & POLLHUP) { print_dbg("conn: hup %d", fds[i].fd); conn_term(fds[i].fd); fds[i].fd = -1; fds[i].revents = 0; return true; } if (fds[i].revents & POLLIN) { /* input on connection */ try { print_dbg("conn: read %d", fds[i].fd); if (!handle_read(fds[i].fd)) { goto read_fail; } } catch (std::bad_alloc const &) { goto read_fail; } } return true; read_fail: print_err("read: handler failed (terminate connection)"); conn_term(fds[i].fd); fds[i].fd = -1; fds[i].revents = 0; return true; } static void sock_handle_conn() { if (!fds[1].revents) { return; } for (;;) { auto afd = accept4( fds[1].fd, nullptr, nullptr, SOCK_NONBLOCK | SOCK_CLOEXEC ); if (afd < 0) { if (errno != EAGAIN) { /* should not happen? disregard the connection */ print_err("accept4 failed (%s)", strerror(errno)); } break; } auto &rfd = fds.emplace_back(); rfd.fd = afd; rfd.events = POLLIN | POLLHUP; rfd.revents = 0; print_dbg("conn: accepted %d for %d", afd, fds[1].fd); } } int main(int argc, char **argv) { /* establish simple signal handler for sigchld */ { struct sigaction sa{}; sa.sa_handler = sig_handler; sa.sa_flags = SA_RESTART; sigemptyset(&sa.sa_mask); sigaction(SIGCHLD, &sa, nullptr); sigaction(SIGTERM, &sa, nullptr); sigaction(SIGINT, &sa, nullptr); } /* establish more complicated signal handler for timers */ { struct sigaction sa; sa.sa_flags = SA_SIGINFO | SA_RESTART; sa.sa_sigaction = timer_handler; sigemptyset(&sa.sa_mask); sigaction(SIGALRM, &sa, nullptr); } /* prealloc a bunch of space */ logins.reserve(16); fds.reserve(64); pending_sess.reserve(16); openlog("turnstiled", LOG_CONS | LOG_NDELAY, LOG_DAEMON); syslog(LOG_INFO, "Initializing turnstiled..."); /* initialize configuration structure */ cfg_data cdata_val; cdata = &cdata_val; if (argc >= 2) { cfg_read(argv[1]); } else { cfg_read(DEFAULT_CFG_PATH); } if (!cdata->manage_rdir && !std::getenv( "TURNSTILED_LINGER_ENABLE_FORCE" )) { /* we don't want to linger when we are not in charge of the rundir, * because services may be relying on it; we can never really delete * the rundir when lingering, and something like elogind might * * for those who are aware of the consequences and have things handled * on their own, they can start the daemon with the env variable */ cdata->linger_never = true; } print_dbg("turnstiled: init signal fd"); { struct stat pstat; int dfd = open(RUN_PATH, O_RDONLY | O_NOFOLLOW); /* ensure the base path exists and is a directory */ if (fstat(dfd, &pstat) || !S_ISDIR(pstat.st_mode)) { print_err("turnstiled base path does not exist"); return 1; } dirfd_base = dir_make_at(dfd, SOCK_DIR, 0755); if (dirfd_base < 0) { print_err("failed to create base directory (%s)", strerror(errno)); return 1; } dirfd_users = dir_make_at(dirfd_base, "users", 0755); if (dirfd_users < 0) { print_err("failed to create users directory (%s)", strerror(errno)); return 1; } dirfd_sessions = dir_make_at(dirfd_base, "sessions", 0755); if (dirfd_sessions < 0) { print_err( "failed to create sessions directory (%s)", strerror(errno) ); return 1; } close(dfd); } /* ensure it is not accessible by service manager child processes */ if ( fcntl(dirfd_base, F_SETFD, FD_CLOEXEC) || fcntl(dirfd_users, F_SETFD, FD_CLOEXEC) || fcntl(dirfd_sessions, F_SETFD, FD_CLOEXEC) ) { print_err("fcntl failed (%s)", strerror(errno)); return 1; } /* use a strict mask */ umask(077); /* signal pipe */ { if (pipe(sigpipe) < 0) { print_err("pipe failed (%s)", strerror(errno)); return 1; } if ( (fcntl(sigpipe[0], F_SETFD, FD_CLOEXEC) < 0) || (fcntl(sigpipe[1], F_SETFD, FD_CLOEXEC) < 0) ) { print_err("fcntl failed (%s)", strerror(errno)); return 1; } auto &pfd = fds.emplace_back(); pfd.fd = sigpipe[0]; pfd.events = POLLIN; pfd.revents = 0; } print_dbg("turnstiled: init control socket"); /* main control socket */ { if (!sock_new(DAEMON_SOCK, ctl_sock, CSOCK_MODE)) { return 1; } auto &pfd = fds.emplace_back(); pfd.fd = ctl_sock; pfd.events = POLLIN; pfd.revents = 0; } print_dbg("turnstiled: main loop"); std::size_t i = 0, curpipes; bool term = false; int pret = -1; print_dbg("turnstiled: init linger"); if (init_linger()) { /* we have pipes to queue, skip the first poll */ goto do_compact; } /* main loop */ for (;;) { print_dbg("turnstiled: poll"); pret = poll(fds.data(), fds.size(), -1); if (pret < 0) { /* interrupted by signal */ if (errno == EINTR) { goto do_compact; } print_err("poll failed (%s)", strerror(errno)); return 1; } else if (pret == 0) { goto do_compact; } /* check signal fd */ print_dbg("turnstiled: check signal"); if (fds[0].revents == POLLIN) { sig_data sd; if (read(fds[0].fd, &sd, sizeof(sd)) != sizeof(sd)) { print_err("signal read failed (%s)", strerror(errno)); goto do_compact; } if (sd.sign == SIGALRM) { if (!sig_handle_alrm(sd.datap)) { return 1; } goto signal_done; } if ((sd.sign == SIGTERM) || (sd.sign == SIGINT)) { if (!sig_handle_term()) { return 1; } term = true; goto signal_done; } /* this is a SIGCHLD */ if (!sig_handle_chld()) { return 1; } } signal_done: print_dbg("turnstiled: check term"); if (term) { /* check if there are any more live processes */ bool die_now = true; for (auto &lgn: logins) { if ((lgn.srv_pid >= 0) || (lgn.term_pid >= 0)) { /* still waiting for something to die */ die_now = false; break; } } if (die_now) { /* no more managed processes */ return 0; } /* the only thing to handle when terminating is signal pipe */ continue; } /* check incoming connections on control socket */ print_dbg("turnstiled: check incoming"); sock_handle_conn(); /* check on pipes; npipes may be changed by fd_handle_pipe */ curpipes = npipes; print_dbg("turnstiled: check pipes"); for (i = 2; i < (curpipes + 2); ++i) { try { if (!fd_handle_pipe(i)) { return 1; } } catch (std::bad_alloc const &) { return 1; } } print_dbg("turnstiled: check conns"); /* check on connections */ for (; i < fds.size(); ++i) { if (!fd_handle_conn(i)) { return 1; } } do_compact: print_dbg("turnstiled: compact"); /* compact the descriptor list */ for (auto it = fds.begin(); it != fds.end();) { if (it->fd == -1) { it = fds.erase(it); } else { ++it; } } /* queue pipes after control socket */ for (auto &lgn: logins) { if (!lgn.pipe_queued) { continue; } pollfd pfd; pfd.fd = lgn.userpipe; pfd.events = POLLIN | POLLHUP; pfd.revents = 0; /* insert in the pipe area so they are polled before conns */ fds.insert(fds.begin() + 2, pfd); /* ensure it's not re-queued again */ lgn.pipe_queued = false; ++npipes; } } for (auto &fd: fds) { if (fd.fd >= 0) { close(fd.fd); } } return 0; } turnstile-0.1.11/src/turnstiled.hh000066400000000000000000000124611507274676000171520ustar00rootroot00000000000000/* shared turnstiled header * * Copyright 2022 q66 * License: BSD-2-Clause */ #ifndef TURNSTILED_HH #define TURNSTILED_HH #include #include #include #include #include #include #include #include #include "protocol.hh" struct login; /* represents a single session within a login */ struct session { session(): str_left{0}, handshake{1}, pend_vtnr{1}, pend_remote{1}, pend_service{1}, pend_type{1}, pend_class{1}, pend_desktop{1}, pend_seat{1}, pend_tty{1}, pend_display{1}, pend_ruser{1}, pend_rhost{1} {} /* data strings */ std::string s_service{}; std::string s_type{}; std::string s_class{}; std::string s_desktop{}; std::string s_seat{}; std::string s_tty{}; std::string s_display{}; std::string s_ruser{}; std::string s_rhost{}; /* the login the session belongs to */ login *lgn; /* session id */ unsigned long id; /* the session vt number */ unsigned long vtnr; /* pid of the login process */ pid_t lpid; /* requested amount of data before we can proceed */ int needed; /* whether we're remote */ bool remote; /* the connection descriptor */ int fd; /* stage */ unsigned int str_left: 16; unsigned int handshake: 1; unsigned int pend_vtnr: 1; unsigned int pend_remote: 1; unsigned int pend_service: 1; unsigned int pend_type: 1; unsigned int pend_class: 1; unsigned int pend_desktop: 1; unsigned int pend_seat: 1; unsigned int pend_tty: 1; unsigned int pend_display: 1; unsigned int pend_ruser: 1; unsigned int pend_rhost: 1; }; /* represents a collection of sessions for a specific user id */ struct login { /* a list of connection file descriptors for this login */ std::vector sessions{}; /* the username */ std::string username{}; /* the string the backend 'run' hands over to 'ready' */ std::string srvstr{}; /* the user's shell */ std::string shell{}; /* the user's home directory */ std::string homedir{}; /* the XDG_RUNTIME_DIR */ std::string rundir{}; /* the PID of the service manager process we are currently managing */ pid_t srv_pid = -1; /* the PID of the backend "ready" process that reports final readiness */ pid_t start_pid = -1; /* the PID of the service manager process that is currently dying */ pid_t term_pid = -1; /* login timer; there can be only one per login */ timer_t timer{}; sigevent timer_sev{}; /* user and group IDs read off the first connection */ unsigned int uid = 0; unsigned int gid = 0; /* the read end of the pipe that the service manager uses to signal * command readiness */ int userpipe = -1; /* login directory descriptor */ int dirfd = -1; /* whether the login should be repopulated on next session */ bool repopulate = true; /* true unless srv_pid has completely finished starting */ bool srv_wait = true; /* false unless waiting for term_pid to quit before starting again */ bool srv_pending = false; /* whether to manage XDG_RUNTIME_DIR (typically false) */ bool manage_rdir = false; /* whether the timer is actually currently set up */ bool timer_armed = false; /* whether a SIGKILL was attempted */ bool kill_tried = false; /* whether a pipe is queued */ bool pipe_queued = false; login(); void remove_sdir(); bool arm_timer(std::time_t); void disarm_timer(); }; /* filesystem utilities */ int dir_make_at(int dfd, char const *dname, mode_t mode); bool rundir_make(char *rundir, unsigned int uid, unsigned int gid); void rundir_clear(char const *rundir); bool dir_clear_contents(int dfd); /* config file related utilities */ void cfg_read(char const *cfgpath); void cfg_expand_rundir( std::string &dest, char const *tmpl, unsigned int uid, unsigned int gid ); /* service manager utilities */ void srv_child(login &sess, char const *backend, bool make_rundir); bool srv_boot(login &sess, char const *backend); struct cfg_data { time_t login_timeout = 60; bool debug = false; bool disable = false; bool debug_stderr = false; bool manage_rdir = MANAGE_RUNDIR; bool export_dbus = true; bool linger = false; bool linger_never = false; bool root_session = false; std::string backend = "dinit"; std::string rdir_path = RUN_PATH "/user/%u"; }; extern cfg_data *cdata; /* these are macros for a simple reason; making them functions will trigger * format-security warnings (even though it's technically always safe for * us, there is no way to bypass that portably) and making it a C-style * vararg function is not possible (because vsyslog is not standard) * * in a macro we just pass things through, so it's completely safe */ #define print_dbg(...) \ if (cdata->debug) { \ if (cdata->debug_stderr) { \ fprintf(stderr, __VA_ARGS__); \ fputc('\n', stderr); \ } \ syslog(LOG_DEBUG, __VA_ARGS__); \ } #define print_err(...) \ if (cdata->debug_stderr) { \ fprintf(stderr, __VA_ARGS__); \ fputc('\n', stderr); \ } \ syslog(LOG_ERR, __VA_ARGS__); #endif turnstile-0.1.11/src/utils.cc000066400000000000000000000070621507274676000161040ustar00rootroot00000000000000/* shared non-portable utilities * * Copyright 2022 q66 * License: BSD-2-Clause */ #include #include #include #include #include #include #include #if defined(__sun) || defined(sun) # if __has_include() # include # else # include # endif #endif #include "utils.hh" bool get_peer_cred(int fd, uid_t *uid, gid_t *gid, pid_t *pid) { #if defined(SO_PEERCRED) /* Linux or OpenBSD */ #ifdef __OpenBSD struct sockpeercred cr; #else struct ucred cr; #endif socklen_t crl = sizeof(cr); if (!getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cr, &crl) && (crl == sizeof(cr))) { if (uid) { *uid = cr.uid; } if (gid) { *gid = cr.gid; } if (pid) { *pid = cr.pid; } return true; } #elif defined(LOCAL_PEERCRED) /* FreeBSD or Darwin */ struct xucred cr; socklen_t crl = sizeof(cr); if ( !getsockopt(fd, SOL_LOCAL, LOCAL_PEERCRED, &cr, &crl) && (crl == sizeof(cr)) && (cr.cr_version == XUCRED_VERSION) ) { if (uid) { *uid = cr.cr_uid; } if (gid) { *gid = cr.cr_gid; } if (pid) { *pid = cr.cr_pid; } return true; } #elif defined(LOCAL_PEEREID) /* NetBSD */ struct unpcbid cr; socklen_t crl = sizeof(cr); if (!getsockopt(fd, 0, LOCAL_PEEREID, &cr, &crl) && (crl == sizeof(cr))) { if (uid) { *uid = cr.unp_euid; } if (gid) { *gid = cr.unp_egid; } if (pid) { *pid = cr.unp_pid; } return true; } #elif defined(__sun) || defined(sun) /* Solaris */ ucred_t *cr = nullptr; if (getpeerucred(fd, &cr) < 0) { return false; } auto uidv = ucred_geteuid(cr); auto gidv = ucred_getegid(cr); auto pidv = ucred_getpid(cr); ucred_free(cr); if ( (uid && (uidv == uid_t(-1))) || (gid && (gidv == gid_t(-1))) || (pid && (pidv < 0)) ) { return false; } if (uid) { *uid = uidv; } if (gid) { *gid = gidv; } if (pid) { *pid = pidv; } return true; #else #error Please implement credentials checking for your OS. #endif return false; } unsigned long get_pid_vtnr(pid_t pid) { unsigned long vtnr = 0; #ifdef __linux__ char buf[256]; char tbuf[256]; unsigned long cterm; std::snprintf( buf, sizeof(buf), "/proc/%lu/stat", static_cast(pid) ); FILE *f = std::fopen(buf, "rb"); if (!f) { return 0; } if (!std::fgets(tbuf, sizeof(tbuf), f)) { fclose(f); return 0; } fclose(f); char *sp = std::strchr(tbuf, ')'); if (!sp) { return 0; } if (std::sscanf(sp + 2, "%*c %*d %*d %*d %lu", &cterm) != 1) { return 0; } if ((major(cterm) == 0) && (minor(cterm) == 0)) { return 0; } std::snprintf( buf, sizeof(buf), "/sys/dev/char/%d:%d", major(cterm), minor(cterm) ); std::memset(tbuf, '\0', sizeof(tbuf)); if (readlink(buf, tbuf, sizeof(tbuf) - 1) < 0) { return 0; } sp = strrchr(tbuf, '/'); if (sp && !std::strncmp(sp + 1, "tty", 3)) { char *endp = nullptr; vtnr = std::strtoul(sp + 4, &endp, 10); if (endp && *endp) { vtnr = 0; } } #else #error Please add your implementation here #endif return vtnr; } turnstile-0.1.11/src/utils.hh000066400000000000000000000004321507274676000161100ustar00rootroot00000000000000/* shared non-portable utilities * * Copyright 2022 q66 * License: BSD-2-Clause */ #ifndef UTILS_HH #define UTILS_HH #include bool get_peer_cred(int fd, uid_t *uid, gid_t *gid, pid_t *pid); unsigned long get_pid_vtnr(pid_t pid); #endif turnstile-0.1.11/turnstiled.conf.5.scd.in000066400000000000000000000100241507274676000202220ustar00rootroot00000000000000turnstiled.conf(5) # NAME turnstiled.conf - the *turnstiled*(8) configuration file # DESCRIPTION The file _turnstiled.conf_ contains the daemon's configuration. It typically resides in _/etc/turnstile_ (or your sysconfdir of choice). While the daemon can run without any configuration file thanks to its built-in defaults, there are many options that the user may want to adjust to their liking. # SYNTAX The configuration file has a simple line-based syntax. Each option line consists of the option name and option value separated by the '=' symbol. Comments start with the '#' symbol. All whitespace is ignored, including lines containing only whitespace, trailing whitespace, leading whitespace and whitespace inbetween names. Only 1024 characters at most are read per line, including whitespace. If longer, the rest is simply ignored. # OPTIONS This is the list of possible options, with their type and default value, as well as additional description. Boolean options accept only the values _yes_ and _no_. Other options may accept more values. *debug* (boolean: _no_) Whether to output debug information. This is verbose logging that is only useful when investigating issues. *backend* (string: _dinit_) The service backend to use. The default is build-dependent and in this case is set to _@DEFAULT_BACKEND@_. Can also be set to _none_ to disable the service backend. In that case, nothing will be spawned, but the daemon will still perform login tracking and auxiliary tasks such as rundir management. *debug\_stderr* (boolean: _no_) Whether to print debug messages also to stderr. *linger* (combo: _maybe_) Whether to keep already started services running even after the last login of the user is gone. The default behavior is to stop them unless a file with the same name as the user exists in _@LINGER_PATH@_. It is not necessary to log in and out when the linger directory is changed, as the current state is checked upon last logout. Note that lingering is disabled when _manage\_rundir_ is set to no. That is because various user services may be relying on the rundir's existence, and it cannot be deleted until the user is gone. This is overridable with an environment variable (for those who worked around it on their own). Valid values are _yes_, _no_ and _maybe_. *rundir\_path* (string: _@RUN_PATH@/usr/%u_) The value of _$XDG\_RUNTIME\_DIR_ that is exported into the user service environment. Special values _%u_ (user ID), _%g_ (group ID) and _%%_ (the character '%') are allowed and substituted in the string. Set to empty string if you want to prevent it from being exported altogether. It must not end with a slash, be relative or be just the root filesystem. If you are using elogind, you should not mess with this path, and doing so will result in subtly broken systems. You should in general not mess with this path. *manage\_rundir* (boolean: _@MANAGE_RUNDIR@_) Whether to manage the _$XDG\_RUNTIME\_DIR_. This may conflict with other rundir management methods, such as elogind, so when turning it on, make sure this is not the case. It is a requirement for the linger functionality to work. The default is dependent on the build. *export\_dbus\_address* (boolean: _yes_) Whether to export _$DBUS\_SESSION\_BUS\_ADDRESS_ into the environment. When enabled, this will be exported and set to 'unix:path=RUNDIR/bus' where RUNDIR is the expanded value of _rundir\_path_. This works regardless of if rundir is managed. *login\_timeout* (integer: _60_) The timeout for the login (in seconds). If the user services that are a part of the initial startup process take longer than this, the service manager instance is terminated and all connections to the session are closed. *root\_session* (boolean: _no_) Whether to run a user service manager for root logins. By default, the root login is tracked but service manager is not run for it. If you override that, the root user is treated like any other user and will have its own user services. This may result in various gotchas, such root having a session bus, and so on. turnstile-0.1.11/turnstiled.conf.in000066400000000000000000000070621507274676000173170ustar00rootroot00000000000000## This is the configuration file for turnstiled. ## ## The daemon will function even without a configuration ## file, but the values here reflect the built-in defaults. ## ## The syntax is a simple line-by-line list of values. ## Values are case-sensitive. Whitespace around the ## assignment (=) as well as any leading and trailing ## whitespace is ignored. Only 1024 characters at most ## are read per line, including whitespace. ## ## Lines starting with # are considered comments. Lines ## that fail to parse are ignored. Invalid values are ## ignored (configuration will be unchanged). # Whether to output debug information. This is verbose # logging that is only useful when investigating issues. # # Valid values are 'yes' and 'no'. # debug = no # The service backend to use. The default is build-dependent # and in this case is set to '@DEFAULT_BACKEND@'. # # Can also be set to 'none' to disable the service backend. # In that case, nothing will be spawned, but the daemon # will still perform login tracking and auxiliary tasks # such as rundir management. # backend = @DEFAULT_BACKEND@ # Whether to print debug messages also to stderr. # # Valid values are 'yes' and 'no'. # debug_stderr = no # Whether to keep already started services running even # after the last login of the user is gone. The default # behavior is to stop them unless a file with the same # name as the user exists in '@LINGER_PATH@'. # # It is not necessary to log in and out when the linger # directory is changed, as the current state is checked # upon last logout. # # Note that lingering is disabled when manage_rundir is # set to no. That is because various user services may # be relying on the rundir's existence, and it cannot # be deleted until the user is gone. # # Valid values are 'yes', 'no' and 'maybe'. # linger = maybe # The value of XDG_RUNTIME_DIR that is exported into the # user service environment. Special values '%u' (user ID), # '%g' (group ID) and '%%' (the character %) are allowed # and substituted in the string. Set to empty string if # you want to prevent it from being exported altogether. # # It must not end with a slash or be relative or just '/'. # # If you are using elogind, you should not mess with this # path, and doing so will result in subtly broken systems. # You should in general not mess with this path. # rundir_path = @RUN_PATH@/user/%u # Whether to manage the XDG_RUNTIME_DIR. This may conflict # with other rundir management methods, such as elogind, # so when turning it on, make sure this is not the case. # # It is a requirement for the linger functionality to work. # # The default is dependent on the build (here: @MANAGE_RUNDIR@). # # Valid values are 'yes' and 'no'. # manage_rundir = @MANAGE_RUNDIR@ # Whether to export DBUS_SESSION_BUS_ADDRESS into the # environment. When enabled, this will be exported and # set to 'unix:path=RUNDIR/bus' where RUNDIR is the # expanded value of rundir_path. This works regardless # of if rundir is managed. # # Valid values are 'yes' and 'no'. # export_dbus_address = yes # The timeout for the login. If the user services that # are a part of the initial startup process take longer # than this, the service manager instance is terminated # and all connections to the session are closed. # # The value is an integer and represents seconds. # If set to 0, the timeout is disabled. # login_timeout = 60 # When using a backend that is not 'none', this controls # whether to run the user session manager for the root # user. The login session will still be tracked regardless # of the setting, # # Valid values are 'yes' and 'no'. # root_session = no