Admin Stuff

This was the biggest Problem for implementing per-user SSH traffic accounting. Sadly, SSH does not log traffic information. There are patches, which add this functionality to SSH, but they are unmaintained and I did not like them.

Therefore I was looking for a Solution outside of SSH. One idea was, to use the owner match of ip-tables, however this does not work, since even with privilege separation enabled, the SSH prozess who owns the socket is stille owned by root. But I found a solution: use PAM.

When PAM support is enabled in current SSH versions, the ssh daemon will open a PAM session. Based on a simple PAM module I found on the internet (pam_preprofile), I implemented my own pam_exec module. This module can start an arbitrary script at session setup and also on session removal. Sadly, SSH does not seem to explicitly close the PAM session. However, it does open a PAM session even for non-interactive or sftp access. Using pam_exec, I start a script on session setup. This script is passed the authenticated user name. It will delve deeply into the Linux proc filestystem to find out what I want to know: The src-ip / src-port pair so I can use iptables to track this connection. As an added bonus, the Script also finds the session in the ip_conntrack table and reports the bytes transfered in this session so far. This allows, to even account the pre-authentication traffic to the user.

Here is the script snipped to do this stuff:

#!/bin/sh

parse_addr() {
    local rv
    case "$1" in
        00000000:0000) rv="" ;;
        00000000000000000000000000000000:0000) rv="" ;;
        0000000000000000FFFF0000*) rv="${1#0000000000000000FFFF0000}" ;;
    esac
    if [ -n "$rv" ]; then
        ip="$(echo -n "${rv%:*}" | \
                  perl -ne 'print join(".",map(ord,reverse(split(//,pack("H*",$_)))))')"
        port="$(echo -n "${rv#*:}" | perl -ne 'print hex($_)')"
        echo "$ip:$port"
    fi
}

for inode in $(find /proc/$(cut -d' ' -f 4 /proc/$PPID/stat)/fd \
                   -type l -lname "socket:*" -printf "%l\n"); do
    inode="${inode#socket:[}"
    inode="${inode%]}"
    x="$(awk "\$10==$inode {print \$2,\$3}" /proc/net/tcp{,6})"
    local="$(parse_addr "${x% *}")"
    remote="$(parse_addr "${x#* }")"
    [ -z "$remote" ] || break;
done

x="$(awk "/src=${local%:*} dst=${remote%:*} sport=${local#*:} dport=${remote#*:}/{print \$9,\$10}" \
             /proc/net/ip_conntrack)"
bytes="${x#*bytes=}"
packets="${x% *}"
packets="${packets#*=}"

echo "$PPID $PAM_USER $local $remote $packets $bytes" >>/tmp/log

The script explicitly handles IPv6 mapped IPv4 addresses, since SSH will open an IPv6 socket if possible. This will lead to the session information to be recorded using IPv6 addresses (mapped from IPv4) in /proc/net/tcp6 instead of with ordinary IPv4 addresses in /proc/net/tcp