Re: Request for a Lockdown option

[Date Prev] [Date Next] [Thread Prev] [Thread Next] [Date Index] [Thread Index]

 



Hello (and finally),

Steffen Nurpmeso wrote in
 <20240714024434.vvSRh10_@steffen%sdaoden.eu>:
 ...
 |Anyhow, this approach i like much better than TLS.
 |It is a bit ugly that ssh-keygen does not give access to the PEM
 |version of the OpenSSL private key, on the other hand encryption
 |only works for RSA (in the western world, and libsodium, on the
 |other hand, would allow this for Ed25519 it seems, too, and i also
 ...
 |I will attach these, i like them.  Maybe i find time to make the
 |OpenBSD variant work for IPv4 and IPv6, but i think it needs two
 |sockets and select(2), so this is a bit of work.

So i did that.  It became a "more regular thing" while doing that,
ie, with a README (and a big fat warning that this thing should be
firewall protected), with nicer -h for the script, etc etc.
I keep using IPv6 and mapped address except on *BSD, it works
nicely on Linux and OpenIndiana.

It is only a pity that the new algorithms do not "simply" allow
encryption, and that OpenSSH does not reveal a PEM variant of the
private key (for "decrypting", then), so that OpenSSL is needed in
addition, at all.

Anyhow this is much smaller than installing OpenVPN for some knock
daemon (if i understood the discussion right), and i use it.

Thanks, and ciao,

--steffen
|
|Der Kragenbaer,                The moon bear,
|der holt sich munter           he cheerfully and one by one
|einen nach dem anderen runter  wa.ks himself off
|(By Robert Gernhardt)
S - p o r t - k n o c k
=======================

A simple port knock implementation.
It requires modern OpenSSH (ssh-keygen(1)), openssl(1) and sh(1)
implementations.  (It actively searches for bash(1) *if* the sh(1)
seems insufficient.  $OPENSSL= may be passed to dedicate that.)

BIG FAT WARNING
---------------

  This software MUST be protected by suitable firewall rules!
  Any network packet sent to it causes a sh(1)ell script to run!
  For example the author uses this Linux firewall rule which effectively
  allows only one packet per source address per minute (and causes a
  hour-long block otherwise).

    # port_knock: input only server
    if fwcore_has_i port_knock; then
            : ${FWCORE_PORT_KNOCK:?port_knock needs FWCORE_PORT_KNOCK}
            if ipaddr_split ap "${FWCORE_PORT_KNOCK}"; then
                    add_rule -p udp --dport ${port} \
                            -m recent --name port_knock --set \
                            -m recent --name port_knock \
                              --rcheck --seconds 60 --reap --hitcount 2 \
                            -m recent --name alien_super --set -j DROP
                    add_rule -p udp --dport ${port} -j f_m1
            fi
    fi

What does this software do?
---------------------------

On servers:

. If the packets seems sane,
. the OpenSSL private key is used to decrypt the random key,
. the random key is used to decrypt the signature,
. the "ssh-keygen -Y" mechanism is used to find a principal for the
  signature in the "allowed signers" file.
. (If a $MAGIC string is required, the signature is also verified.)

. On any stage a failure causes the act_block() action,
. upon overall success the act_allow() action is applied.

  # cd /tmp/
  # $CC -o zt s-port-knock-bin.c

  # head -n1 ~/.ssh/authorized_keys > .Zpub1
  # { printf 'uid1 '; cat .Zpub1; } > .Zsigs
  # echo MAGIC= > .Z.rc

  # ./s-port-knock.sh create-server-key .Zk
  # PORT_KNOCK_BIN=./zt PORT_KNOCK_SHELL=/bin/bash PORT_KNOCK_RC=./.Z.rc \
    ./s-port-knock.sh start-server -v 10000 s-port-knock.sh \
      .Zk-pri.pem .Zsigs

On clients:

. Creates a signature for a SSH public key via "ssh-keygen -Y",
. (if a $MAGIC string is required, includes that in the signature),
. encrypts that with a random key,
. and encrypts the random key for a dedicated target server via an
  (its) OpenSSL public key,
. sends the resulting packet to that server,
. invokes the $PORT_KNOCK_RC act_sent() hook; if that does not return 0,
  the packet is sent again, etc.

  # cd /tmp/
  # PORT_KNOCK_BIN=./zt PORT_KNOCK_RC=./.Z.rc \
    ./s-port-knock.sh knock localhost 10000 .Zk-pub.pem .Zpub1

Build and Install
-----------------

  gcc -o /usr/sbin/s-port-knock-bin s-port-knock-bin.c
  cp s-port-knock.sh /usr/bin

RELEASES
--------

v0.8.0, 2024-07-17: (first release)
  + Linux (musl, glibc), *BSD:
    As above.  (IPv6 support is assumed.)
  + OpenIndiana 2024:
      $ LD_LIBRARY_PATH=/usr/openssl/3.1/lib/amd64:$LD_LIBRARY_PATH \
        C_INCLUDE_PATH=/usr/openssl/3.1/include:$C_INCLUDE_PATH \
        gcc -o /usr/sbin/s-port-knock-bin s-port-knock-bin.c -lsocket
    is needed in the environment for the build.
    You may need to pass OPENSSL=PATH to the s-port-knock* commands.

# s-ts-mode
#!/bin/sh -
#@ s-port-knock.sh: port knock user interface (for s-port-knock-bin.c server).
#@ Requires (modern) openssl and ssh-keygen with -Y support.
#
: ${PORT_KNOCK_BIN:=/usr/sbin/s-port-knock-bin}
SELF=s-port-knock.sh
VERSION=0.8.0
CONTACT='Steffen Nurpmeso <steffen@xxxxxxxxxx>'
#
syno() {
	echo >&2 $SELF' ('$VERSION'): simple UDP based port knock client/server'
	echo >&2
	echo >&2 'SYNOPSIS: '$SELF' knock host port pubkey ssh-client-pubkey'
	echo >&2 '          - knock for ssh-client-pubkey at server (host:port, pubkey).'
	echo >&2 '            Requires '$PORT_KNOCK_BIN', or bash(1) in $PATH'
	echo >&2
	echo >&2 'SYNOPSIS: '$SELF' create-server-key filename-prefix [rsa[:bits]]'
	echo >&2 '          - generate *-{pri,pub}.pem; -pub.pem is needed by clients'
	echo >&2 'SYNOPSIS: '$SELF' start-server [-v] port cmd prikey allowed-signers'
	echo >&2 '           - cmd is sh(1) script to "verify" with (ie this one); Read README!'
	echo >&2 '           - With -v one should add --no-close and --output to s-s-d options'
	echo >&2 '               --start --background --make-pidfile --pidfile XY --exec'
	echo >&2 'SYNOPSIS: '$SELF' verify IP [prikey allowed-signers enc-key enc-sig]'
	echo >&2 '          - (called by server); if prikey and allowed-signers (see ssh-keygen)'
	echo >&2 '            are given, verify enc-key and enc-sig, else just block IP address'
	echo >&2
	echo >&2 '. Set $PORT_KNOCK_BIN environment to replace '$PORT_KNOCK_BIN','
	echo >&2 '  $PORT_KNOCK_SHELL (/bin/sh), and $MAGIC to include a "magic string" in sigs.'
	echo >&2 '. Set $PORT_KNOCK_RC to specify a resource file'
	echo >&2 '          MAGIC= # (server *verifies* sig additionally only if non-empty!) '
	echo >&2 '          act_sent() { echo >&2 "client sent to $1"; return 0; } # !0: retry!'
	echo >&2 '          act_block() { echo >&2 "server blocking "$1; }'
	echo >&2 '          act_allow() { echo >&2 "server allowing $1, principal: $2"; }'
	echo >&2 '. Bugs/Contact via '$CONTACT
	exit 64 # EX_USAGE
}
#
# 2020 - 2024 Steffen Nurpmeso <steffen@xxxxxxxxxx>
# SPDX-License-Identifier: ISC
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

: ${RSA_DEFBITS:=2048}
: ${RSA_MINBITS:=1024}

: ${SHELL:=/bin/sh}
: ${OPENSSL:=openssl}

: ${TMPDIR:=/tmp}
: ${ENV_TMP:="$TMPDIR:$TMP:$TEMP"}

# >8 -- 8<

# knock: save encrypted key and signature there
# verify: fetch enc-key and enc-sig from $DEBUG
: ${DEBUG:=}

LC_ALL=C
EX_DATAERR=65 EX_CANTCREAT=73 EX_TEMPFAIL=75

# For heaven's sake auto-redirect on SunOS/Solaris, and for pipefail, if necessary
if [ -z "$__PORT_KNOCK_UP" ]; then
	if [ $# -eq 0 ] || [ "$1" = -h ] || [ "$1" = --help ]; then
		syno
	fi

	if (set -C -o pipefail) >/dev/null 2>&1; then
		:
	elif [ -n "$PORT_KNOCK_SHELL" ]; then
		__PORT_KNOCK_UP=$PORT_KNOCK_SHELL
		#printf >&2 'INFO: "set -C -o pipefail" unsupported, redirect via $PORT_KNOCK_SHELL='$__PORT_KNOCK_UP
	else
		__PORT_KNOCK_UP=$(command -v bash 2>&1)
		if [ $? -eq 0 ]; then
			#printf >&2 'INFO: "set -C -o pipefail" unsupported, redirect via '$__PORT_KNOCK_UP
			:
		elif [ -d /usr/xpg4 ]; then
			if [ x"$SHELL" = x/bin/sh ]; then
				__PORT_KNOCK_UP=/usr/xpg4/bin/sh PATH=/usr/xpg4/bin:${PATH}
				#printf >&2 'INFO: SunOS/Solaris, redirect via '$__PORT_KNOCK_UP
			fi
		else
			__PORT_KNOCK_UP= # (should be but whee)
		fi
	fi

	if [ -n "$__PORT_KNOCK_UP" ]; then
		export __PORT_KNOCK_UP PATH
		exec "$__PORT_KNOCK_UP" "$0" "$@"
	fi
fi

if (set -o pipefail); then # >/dev/null 2>&1; then
	set -o pipefail
else
	# XXX circumvent via estat=$(exec 3>&1 1>&2; { DOIT; echo $? >&3; } | BLA)
	echo >&2 'WARNING: shell ('$SHELL') does not support "set -o pipefail", this HIDES ERRORS'
	echo >&2 'Run with a more modern shell like so: SHELL '$0' '$*
fi

if (set -C) >/dev/null 2>&1; then :; else
	echo >&2 'ERROR: shell does not support noclobber option aka set -C'
	echo >&2 'ERROR: please run with a more modern shell like so: SHELL '$0' '$*
	exit $EX_TEMPFAIL
fi

umask 077

# $PORT_KNOCK_RC defaults
: ${MAGIC:=}
act_block() { echo >&2 'blocking '$1; }
act_allow() { echo >&2 'allowing '$1', principal: '$2; }
__wW=
act_sent() {
	# (specific to how my firewall works and assumes we were blocked before OR now)
	echo >&1 'Pinging '$1
	ping=ping
	[ "$1" != "${1##*::}" ] && command -v ping6 >/dev/null 2>&1 && ping=ping6
	if [ -z "$__wW" ]; then
		if ($ping -c1 -w1 localhost >/dev/null 2>&1); then
			__wW=-w1
		elif ($ping -c1 -W500 localhost >/dev/null 2>&1); then
			__wW=-W1000
		fi
	fi

	sleep 1
	$ping -c1 $__wW "$1" && return 0
	echo 'Did not work out, sleeping 60 seconds: '$ping -c1 $__wW "$1"
	sleep 60
	return 1
}

tmp_dir= tmp_file=  __tmp_no=1 __tmpfiles=
tmp_file_new() {
	if [ -z "$tmp_dir" ]; then
		_tmp_dir() {
			i=$IFS
			IFS=:
			set -- $ENV_TMP
			IFS=$i
			# for i; do -- new in POSIX Issue 7 + TC1
			for tmp_dir
			do
				[ -d "$tmp_dir" ] && return 0
			done
			tmp_dir=$TMPDIR
			[ -d "$tmp_dir" ] && return 0
			echo >&2 'Cannot find a temporary directory, please set $TMPDIR'
			exit $EX_TEMPFAIL
		}
		_tmp_dir

		trap "exit $EX_TEMPFAIL" HUP INT QUIT PIPE TERM
		trap "trap \"\" HUP INT QUIT PIPE TERM EXIT; rm -f \$__tmpfiles" EXIT
	fi

	while :; do
		tmp_file="$tmp_dir/port-knock-$$-$__tmp_no.dat"
		(
			set -C
			> "$tmp_file"
		) >/dev/null 2>&1 && break
		__tmp_no=$((__tmp_no + 1))
	done
	__tmpfiles="$__tmpfiles $tmp_file"
}

ossl() (
	set +e

	x=
	while :; do
		"$OPENSSL" "$@"
		[ $? -eq 0 ] && return 0

		echo >&2
		echo >&2 '$OPENSSL='$OPENSSL' seems incompatible; is it an old version?'
		echo >&2 '  Command was: '$*
		if [ -x /usr/openssl/3.1/bin/openssl ]; then
			x=/usr/openssl/3.1/bin/openssl
		elif [ -x /usr/openssl/1.1/bin/openssl ]; then
			x=/usr/openssl/1.1/bin/openssl
		elif [ -x /usr/pkg/bin/openssl ]; then
			x=/usr/pkg/bin/openssl
		elif [ -x /opt/csw/bin/openssl ]; then
			x=/opt/csw/bin/openssl
		fi
		if [ x = x"$x" ] || [ x"$x" = x"$OPENSSL" ]; then
			echo >&2 'Please place version >= 1.1.0 (early) in $PATH, or $OPENSSL=, rerun.'
			exit $EX_DATAERR
		fi
		OPENSSL=$x
		echo >&2 'I will try $OPENSSL='$OPENSSL' instead; restarting operation ...'
		echo >&2
	done
)

incrc() {
	[ -n "$PORT_KNOCK_RC" ] && [ -r "$PORT_KNOCK_RC" ] && . "$PORT_KNOCK_RC"
}

case "$1" in
create-server-key)
	[ $# -gt 3 ] && syno
	fprefix=$2

	algo=rsa opt=
	[ $# -gt 2 ] && algo=$3
	case $algo in
	rsa|rsa:*)
		bits=${algo##*:}
		algo=${algo%%:*}
		if [ "$bits" = "$algo" ] || [ -z "$bits" ]; then
			bits=$RSA_DEFBITS
		else
			i=${bits#${bits%%[!0-9]*}}
			i=${i%${i##*[!0-9]}} # xxx why?
			if [ -n "$i" ]; then
				echo >&2 'RSA bits is not a number'
				exit $EX_DATAERR
			fi
			if [ $bits -lt $RSA_MINBITS ]; then
				echo >&2 'RSA bits insufficient (RFC 8301), need at least '$RSA_MINBITS
				exit $EX_DATAERR
			fi
		fi
		opt="-pkeyopt rsa_keygen_bits:$bits"
		;;
	*) syno;;
	esac

	(
		set -C
		> "$fprefix"-pri.pem
		> "$fprefix"-pub.pem
	) || exit $EX_CANTCREAT

	set -e
	ossl genpkey -out "$fprefix"-pri.pem -outform PEM -algorithm $algo $opt
	ossl pkey -pubout -out "$fprefix"-pub.pem -outform PEM < "$fprefix"-pri.pem
	set +e

	echo 'Server keys stored in'
	echo '   '"$fprefix"-pri.pem
	echo '   '"$fprefix"-pub.pem
	echo 'Server also needs a ssh-keygen(1) allowed_signers_file in format'
	echo '  PRINCIPAL(ie, email address) KEYTYPE PUBKEY [COMMENT]'
	echo '(ie, authorized_keys format but prefixed by an PRINCIPAL)'
	;;
knock)
	[ $# -ne 5 ] && syno

	if [ -n "$DEBUG" ]; then
		kpk= kexe=
	else
		incrc
		kpk= kexe=
		if [ -x "$PORT_KNOCK_BIN" ]; then
			kpk=y
			kexe=$PORT_KNOCK_BIN
		else
			kexe=$(command -v bash 2>/dev/null)
			if [ $? -ne 0 ]; then
				echo >&2 'Knocking requires bash in $PATH; or '$PORT_KNOCK_BIN
				syno
			fi
		fi
	fi

	k=$(dd if=/dev/urandom bs=64 count=1 2>/dev/null | tr -cd 'a-zA-Z0-9_.,=@%^+-')
	if [ $? -ne 0 ]; then
		echo >&2 'Failed to create a random key'
		exit $EX_TEMPFAIL
	fi
	ek=$(echo "$k" | ossl pkeyutl -encrypt -pubin -inkey "$4" | ossl base64 | tr -d '\012 ')
	if [ $? -ne 0 ] || [ -z "$ek" ]; then
		echo >&2 'Failed to pkeyutl encrypt'
		exit $EX_TEMPFAIL
	fi

	es=$(printf '%s' "$MAGIC" | ssh-keygen -Y sign -n pokn -f "$5" |
		sed -Ee '/^-+BEGIN SSH/d;/^-+END SSH/d;s/$/ /' | tr -d '\012 ' |
		ossl enc -aes256 -pass "pass:$k" -pbkdf2 -e -A -a)
	if [ $? -ne 0 ] || [ -z "$es" ]; then
		echo >&2 'Failed to encrypt signature'
		exit $EX_TEMPFAIL
	fi

	if [ -n "$DEBUG" ]; then
		set -e
		{ echo "$ek"; echo "$es"; } > "$DEBUG"
	else
		if [ -z "$kpk" ]; then
			tmp_file_new
			printf "%s\n\n%s\n" "$ek" "$es" > "$tmp_file"
		fi

		while :; do
			printf 'knocking ... '
			if [ -n "$kpk" ]; then
				"$kexe" client "$3" "$2" "$ek" "$es"
			else
				</dev/null bash -c '
					set -e
					cat "'"$tmp_file"'" > /dev/udp/"'"$2"'"/"'"$3"'"
				'
			fi
			[ $? -ne 0 ] && break
			echo 'done'
			act_sent "$2" && break
		done
	fi
	;;
verify)
	if [ -z "$DEBUG" ]; then
		[ $# -eq 2 ] || [ $# -eq 6 ] || syno
		incrc
		if [ $# -eq 6 ]; then
			ek=$5 es=$6
		fi
	elif [ $# -eq 2 ]; then
		:
	else
		[ $# -eq 4 ] || syno
		if [ ! -f "$DEBUG" ]; then
			echo >&2 'The result of "knock" does not exist: '$DEBUG
			syno
		fi
		{
			read ek
			read es
		} < "$DEBUG"
	fi

	if [ $# -eq 2 ]; then
		act_block "$2"
	else
		# (of course if pkeyutl fails *after* reading off the pipe ossl() will not do)
		tmp_file_new
		dk=$(echo "$ek" | ossl base64 -d | ossl pkeyutl -decrypt -inkey "$3")
		if [ $? -ne 0 ]; then
			act_block "$2"
		else
			echo "$es" | ossl enc -aes256 -pass "pass:$dk" -pbkdf2 -a -d | {
				printf '%s\n' '-----BEGIN SSH SIGNATURE-----'
				cat
				printf '\n%s\n' '-----END SSH SIGNATURE-----'
			} > "$tmp_file"

			p=$(ssh-keygen -Y find-principals -s "$tmp_file" -f "$4")
			if [ $? -eq 0 ]; then
				if [ -z "$MAGIC" ]; then
					act_allow "$2" "$p"
				elif printf '%s' "$MAGIC" |
						ssh-keygen -Y verify -n pokn -I "$p" -s "$tmp_file" -f "$4"; then
					act_allow "$2" "$p"
				else
					act_block "$2"
				fi
			else
				act_block "$2"
			fi
		fi
	fi
	;;
start-server)
	shift
	[ $# -eq 4 ] || [ $# -eq 5 ] || syno
	incrc
	v=
	if [ $# -eq 5 ]; then
		v=$1
		shift
	fi
	exec "$PORT_KNOCK_BIN" "$v" server "$@"
	;;
*)
	echo >&2 'Invalid command line: '$*
	syno
	;;
esac

exit 0
# s-sht-mode
/*@ s-port-knock-bin.c: C backend for s-port-knock.sh; please see there.
 *@ TODO - capsicum / pledge/unveil (fork from server a forker process that forks+execv the command)
 *
 * Copyright (c) 2020 - 2024 Steffen Nurpmeso <steffen@xxxxxxxxxx>.
 * SPDX-License-Identifier: ISC
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
#define _GNU_SOURCE

#include <sys/select.h>
#include <sys/socket.h>
#include <sys/types.h>

#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

/* Largest possible pubkey encryption + 1 + 1 + SSH signature (ED25519=~295+, RSA=~1560+) + 1 +1; packet is
 *	1. password encrypted by X509 pubkey (base64) + LF
 *	2. LF (gives as room to place a NUL upon receive)
 *	3. SSH signature cipher-encrypted with password in 1. (base64) + LF */
#define a_BUF_LEN (2048 + 1024)

/* Minimum bytes the encrypted SSH signature should have (do not even try to decrypt it, block sender).
 * ED25519 encrypted with -aes256 is 273 bytes */
#define a_SIG_LEN_MIN 256

/* Used if $PORT_KNOCK_SHELL is not set */
#define a_BIN_SH "/bin/sh"

/* Whether we should open distinct IPv4 and IPv6 sockets */
#undef a_DISTINCT_SOCKS
#if defined __FreeBSD__ || defined __NetBSD__ || defined __OpenBSD__
# define a_DISTINCT_SOCKS
#endif

/*  >8 -- 8< */

enum{
	a_EX_OK,
	a_EX_ERR,
	a_EX_USAGE = 64,
	a_EX_DATAERR = 65,
	a_EX_NOHOST = 68,
	a_EX_UNAVAILABLE = 69,
	a_EX_OSERR = 71,
	a_EX_IOERR = 74
};

static int a_verbose;
static int volatile a_sig_seen;

static int a_server(unsigned short port, char const *cmd_path, char const *privkey, char const *sigpool);
static int a_client(char const *host, char const *port, char const *enckey, char const *encsig);

static long a_fork(void);
static void a_sig_hdl(int sig);

int
main(int argc, char **argv){
	char const *prog;
	int es, srv, portno;

	es = 1;
	prog = (argc == 0) ? "port-knock" : argv[0];

	fclose(stdin);
	fclose(stdout);

	if(argc > 0 && !strcmp(argv[1], "-v")){
		a_verbose = 1;
		--argc, ++argv;
	}

	if(argc != 6)
		goto jesyn;

	if(!strcmp(argv[1], "server"))
		srv = 1;
	else if(!strcmp(argv[1], "client"))
		srv = 0;
	else
		goto jesyn;

	portno = atoi(argv[2]);
	if(portno <= 0 || portno > 65535){
		fprintf(stderr, "Bad port (must be >0 and <65536): %s -> %d\n", argv[3], portno);
		goto jesyn;
	}

	/* TODO chdir, [chroot], pledge/unveil */

	es = srv ? a_server((unsigned short)portno, argv[3], argv[4], argv[5])
			: a_client(argv[3], argv[2], argv[4], argv[5]);
jleave:
	return es;

jesyn:
	fprintf(stderr,
		"Synopsis: %s [-v] server port cmd-path prikey allowed-sigs-db\n"
		"Synopsis: %s client port host enckey encsig\n",
		prog, prog);
	goto jleave;
}

static int
a_server(unsigned short portno, char const *cmd, char const *privkey, char const *sigpool){
	char dbuf[a_BUF_LEN], nbuf[INET6_ADDRSTRLEN +1];
	char const *myshell, *argv[9];
	fd_set rfds;
	struct sigaction siac;
	socklen_t soal;
#ifdef a_DISTINCT_SOCKS
	struct sockaddr_in soa4;
	int sofd4;
#endif
	struct sockaddr_in6 soa6;
	int sofd6, es;

	if(!a_verbose)
		freopen("/dev/null", "w", stderr);

	portno = htons(portno);

	sofd6 = -1;

#ifdef a_DISTINCT_SOCKS
	while((sofd4 = socket(AF_INET, SOCK_DGRAM, 0)) == -1){
		es = errno;
		if(es == EINTR)
			continue;
		if(es == EAFNOSUPPORT){
			fprintf(stderr, "IPv4 socket unsupported, skipping this\n");
			break;
		}
		fprintf(stderr, "IPv4 server socket creation failed: %s\n", strerror(es));
		es = a_EX_OSERR;
		goto jleave;
	}

	if(sofd4 != -1){
		soa4.sin_family = AF_INET;
		soa4.sin_port = portno;
		soa4.sin_addr.s_addr = INADDR_ANY;

		while(bind(sofd4, (struct sockaddr const*)&soa4, sizeof soa4) == -1){
			es = errno;
			if(es != EINTR){
				fprintf(stderr, "IPv4 server socket cannot bind: %s\n", strerror(es));
				es = a_EX_OSERR;
				goto jleave;
			}
		}
	}
#endif

	while((sofd6 = socket(AF_INET6, SOCK_DGRAM, 0)) == -1){
		es = errno;
		if(es == EINTR)
			continue;
#ifndef a_DISTINCT_SOCKS
		if(es == EAFNOSUPPORT){
			fprintf(stderr, "IPv6 socket unsupported, skipping this\n");
			break;
		}
#endif
		fprintf(stderr, "IPv6 server socket creation failed: %s\n", strerror(es));
		es = a_EX_OSERR;
		goto jleave;
	}

	if(sofd6 != -1){
#ifdef a_DISTINCT_SOCKS
# ifdef IPV6_V6ONLY
		int one;

		one = 1;
		if(setsockopt(sofd6, IPPROTO_IPV6, IPV6_V6ONLY, (void*)&one, sizeof(one)) == -1){
			fprintf(stderr, "IPv6 cannot set server socket option V6ONLY: %s\n", strerror(es));
			es = a_EX_OSERR;
			goto jleave;
		}
# endif
#endif

		soa6.sin6_family = AF_INET6;
		soa6.sin6_port = portno;
		memcpy(&soa6.sin6_addr, &in6addr_any, sizeof soa6.sin6_addr);

		while(bind(sofd6, (struct sockaddr const*)&soa6, sizeof soa6) == -1){
			es = errno;
			if(es != EINTR){
				fprintf(stderr, "IPv6 server socket cannot bind: %s\n", strerror(es));
				es = a_EX_OSERR;
				goto jleave;
			}
		}
	}
#ifdef a_DISTINCT_SOCKS
	else if(sofd4 == -1){
		fprintf(stderr, "Cannot create any server sockets, bailing out\n");
		es = a_EX_OSERR;
		goto jleave;
	}
#endif

        memset(&siac, 0, sizeof siac);
        sigemptyset(&siac.sa_mask);

        siac.sa_handler = &a_sig_hdl;
        sigaction(SIGHUP, &siac, NULL);
	/*if(a_verbose)*/
		sigaction(SIGINT, &siac, NULL);
        sigaction(SIGTERM, &siac, NULL);

	siac.sa_handler = SIG_IGN;
	sigaction(SIGCHLD, &siac, NULL);
	sigaction(SIGPIPE, &siac, NULL);

	argv[0] = "sh";
	argv[1] = cmd;
	argv[2] = "verify";
	argv[3] = nbuf;
	/* argv[4] = privkey or NULL, our trigger */
	argv[5] = sigpool;
	argv[6] = dbuf; /* enckey */
	/*argv[7] = encsig;*/
	argv[8] = NULL;

	myshell = getenv("PORT_KNOCK_SHELL");
	if(myshell == NULL)
		myshell = a_BIN_SH;

	while(!a_sig_seen){
		ssize_t rb, i;
		void *sadp;
		struct sockaddr *sap;
		int fd;

		FD_ZERO(&rfds);
#ifdef a_DISTINCT_SOCKS
		if(sofd4 != -1)
			FD_SET(sofd4, &rfds);
		if(sofd6 != -1)
#endif
			FD_SET(sofd6, &rfds);

		es = select((
#ifdef a_DISTINCT_SOCKS
				(sofd4 > sofd6) ? sofd4 :
#endif
				sofd6) + 1, &rfds, NULL, NULL, NULL);
		if(es == -1){
			es = errno;
			if(es == EINTR)
				continue;
			fprintf(stderr, "Selection on server socket I/O failed: %s\n", strerror(es));
			es = a_EX_OSERR;
			goto jleave;
		}else if(es == 0) /* ?? */
			continue;

#ifdef a_DISTINCT_SOCKS
		if(sofd4 != -1 && FD_ISSET(sofd4, &rfds)){
			FD_CLR(sofd4, &rfds);
			sadp = &soa4.sin_addr;
			sap = (struct sockaddr*)&soa4;
			soal = sizeof soa4;
			fd = sofd4;
		}else if(sofd6 != -1) jNext_socket:
#endif
		{
			if(FD_ISSET(sofd6, &rfds)){
				FD_CLR(sofd6, &rfds);
				sadp = &soa6.sin6_addr;
				sap = (struct sockaddr*)&soa6;
				soal = sizeof soa6;
				fd = sofd6;
			}else
				continue;
		}
#ifdef a_DISTINCT_SOCKS
		else /* pacify clang */
			continue;
#endif

		rb = recvfrom(fd, dbuf, sizeof(dbuf) -1, 0, sap, &soal);
		if(rb == 0)
			continue;
		if(rb == -1){
			es = errno;
			if(es == EINTR)
				continue;
			fprintf(stderr, "Failed receiving packet: %s\n", strerror(es));
			/* (xxx CONNRESET should trigger firewall rules, so do not care..) */
			if(es != EBADF && es != EINVAL)
				continue;
			es = a_EX_OSERR;
			goto jleave;
		}

		if(inet_ntop(sap->sa_family, sadp, nbuf, sizeof nbuf) == NULL){
			fprintf(stderr, "IMPL ERROR: cannot inet_ntop() peer address after recvfrom(2)\n");
			continue;
		}

		/* Linux kernel iptables xt_recent 6.1.98/6.6.40 do not deal with mapped addresses
		 * (https://bugzilla.kernel.org/show_bug.cgi?id=219038) */
		if(sap->sa_family == AF_INET6 && strchr(nbuf, '.') != NULL){
			i = (size_t)strlen(nbuf);
			if((size_t)i <= sizeof("::ffff:") -1 + 4+4){
				fprintf(stderr, "IMPL ERROR: IPv4 mapped address is bogus\n");
				continue;
			}
			i -= sizeof("::ffff:") -1;
			memmove(nbuf, &nbuf[sizeof("::ffff:") -1], i +1);
		}

		/* Default: do not inspect packet, only block this IP */
		argv[4] = NULL;

		/* Buffer spaced so excess is error */
		if(rb == (ssize_t)sizeof(dbuf) -1)
			goto jfork;
		dbuf[(size_t)rb] = '\0';

		/* Inspect packet, find enckey and encsig boundaries (for format see a_BUF_LEN #define above) */
		argv[7] = NULL;
		i = 0;
		for(;;){
			char c;

			c = dbuf[(size_t)i];
			if(c != '\n'){
				if(((unsigned)c & 0x80) || iscntrl(c))
					goto jfork;
				if(++i == rb)
					goto jfork;
				continue;
			}

			++i;
			if(argv[7] == NULL){
				c = dbuf[(size_t)i]; /* dbuf is NUL terminated, so never excess */
				if(c != '\n')
					goto jepack;
				dbuf[(size_t)i] = '\0';
				if(++i == rb)
					goto jepack;
				if(rb - i < a_SIG_LEN_MIN)
					goto jfork;
				argv[7] = &dbuf[(size_t)i];
			}else if(i != rb){
jepack:
				if(a_verbose)
					fprintf(stderr, "Invalid packet content\n");
				goto jfork;
			}else
				break;
		}

		/* Hook may inspect packet data and decide further */
		argv[4] = privkey;
jfork:
		if(a_verbose)
			fprintf(stderr, "execv: %s %s %s %s %s %s %s %s (%ld bytes input)\n",
				myshell, argv[1], argv[2], argv[3],
				(argv[4] != NULL ? argv[4] : ""),
				(argv[4] != NULL ? argv[5] : ""),
				(argv[4] != NULL ? argv[6] : ""),
				(argv[4] != NULL ? argv[7] : ""),
				(argv[4] != NULL ? (long)rb : 0));

		i = a_fork();
		if(i < 0){
			es = (int)-i;
			fprintf(stderr, "Server fork failed: %s\n", strerror(es));
			es = a_EX_OSERR;
			goto jleave;
		}else if(i == 0){
			execv(a_BIN_SH, (char*const*)argv);
			for(;;)
				_exit(a_EX_OSERR);
		}

#ifdef a_DISTINCT_SOCKS
		if(fd != sofd6 && sofd6 != -1)
			goto jNext_socket;
#endif
	}

	es = a_EX_OK;
jleave:
	if(sofd6 != -1)
		close(sofd6);
#ifdef a_DISTINCT_SOCKS
	if(sofd4 != -1)
		close(sofd4);
#endif

	return es;
}

static int
a_client(char const *host, char const *port, char const *enckey, char const *encsig){
	char buf[a_BUF_LEN];
	struct addrinfo hints, *aip0, *aip;
	size_t l, yet;
	int sofd, es;

	memset(&hints, 0, sizeof hints);
	hints.ai_flags = AI_NUMERICSERV
#ifdef AI_ADDRCONFIG
			| AI_ADDRCONFIG
#endif
#if /* always, we do not care who gives !defined a_DISTINCT_SOCKS &&*/ defined AI_V4MAPPED
			| AI_V4MAPPED
#endif
			;
	hints.ai_family = AF_UNSPEC;
	hints.ai_socktype = SOCK_DGRAM;

	sofd = -1;
	aip0 = NULL;
	es = a_EX_DATAERR;

	l = strlen(enckey);
	if(l >= sizeof(buf) - 2 - 1 -1){
		fprintf(stderr, "Encrypted key is too long\n");
		goto jleave;
	}
	memcpy(buf, enckey, l);
	buf[l] = '\n';
	buf[l + 1] = '\n';
	l += 2;

	yet = strlen(encsig);
	if(yet >= sizeof(buf) - 1 -1 - l){
		fprintf(stderr, "Encrypted signature is too long\n");
		goto jleave;
	}
	memcpy(&buf[l], encsig, yet);
	buf[l += yet] = '\n';
	buf[++l] = '\0';

	es = getaddrinfo(host, port, &hints, &aip0);
	if(es != 0){
		fprintf(stderr, "DNS lookup failed for: %s:%s %s\n", host, port, gai_strerror(es));
		es = a_EX_NOHOST;
		goto jleave;
	}

	for(aip = aip0; aip != NULL; aip = aip->ai_next){
		for(;;){
			sofd = socket(aip->ai_family, aip->ai_socktype, aip->ai_protocol);
			if(sofd != -1)
				goto jso;
			es = errno;
			if(es != EINTR)
				break;
		}
		fprintf(stderr, "Socket creation failed: %s\n", strerror(es));
	}
	es = a_EX_UNAVAILABLE;
	goto jleave;

jso:
	/* One tick though */
	yet = 0;
	while(l > 0){
		ssize_t wb;

		wb = sendto(sofd, &buf[yet], l, 0, aip->ai_addr, aip->ai_addrlen);
		if(wb == -1){
			es = errno;
			if(es == EINTR)
				continue;
			fprintf(stderr, "Packet transmit failed: %s\n", strerror(es));
			es = a_EX_IOERR;
			goto jleave;
		}
		yet += wb;
		l -= wb;
	}

	es = a_EX_OK;
jleave:
	if(aip0 != NULL)
		freeaddrinfo(aip0);
	if(sofd != -1)
		close(sofd);

	return es;
}

static long
a_fork(void){
	struct timespec ts;
	long i;

	ts.tv_sec = 0;
	ts.tv_nsec = 250000000L;

	for(;;){
		i = fork();
		if(i != -1)
			break;
		i = -(errno);
		if(i == -ENOSYS)
			break;
		nanosleep(&ts, NULL);
	}

	return i;
}

static void
a_sig_hdl(int sig){
	(void)sig;
	a_sig_seen = 1;
}

/* s-itt-mode */
_______________________________________________
openssh-unix-dev mailing list
openssh-unix-dev@xxxxxxxxxxx
https://lists.mindrot.org/mailman/listinfo/openssh-unix-dev

[Date Prev] [Date Next] [Thread Prev] [Thread Next] [Date Index] [Thread Index]

[Index of Archives]     [Linux ARM Kernel]     [Linux ARM]     [Linux Omap]     [Fedora ARM]     [IETF Annouce]     [Security]     [Bugtraq]     [Linux]     [Linux OMAP]     [Linux MIPS]     [ECOS]     [Asterisk Internet PBX]     [Linux API]

  Powered by Linux