# -*- tab-width: 4 -*- ;; Emacs
# vi: set filetype=sh tabstop=8 shiftwidth=8 noexpandtab :: Vi/ViM
############################################################ IDENT(1)
#
# $Title: dwatch(8) JSON module for network activity $
# $Copyright: 2014-2022 Devin Teske. All rights reserved. $
# $FrauBSD: dwatch-json/json-net-config-raw 2022-08-19 10:18:35 -0700 freebsdfrau $
# $Version: 0.9 $
#
############################################################ DESCRIPTION
#
# Produce JSON custom log format for network activity
#
############################################################ PROBE

: "${PROBE_SECONDS:=10}"
: "${PROBE:=profile-${PROBE_SECONDS}s}"

############################################################ INCLUDES

. /usr/share/bsdconfig/strings.subr || exit

############################################################ FUNCTIONS

#
# JSON helpers
#
jfmt(){ printf "\\\\\"%s\\\\\":$1" "$2" "$3"; }
jraw(){ jfmt %s "$1" "$2"; }
jstr(){ jraw "$1" "\\\"$2\\\""; }
_jstr(){ printf ,; jstr "$@"; }
_jraw(){ printf ,; jraw "$@"; }

############################################################ CONFIG

#
# Defaults
#
: "${JSON_NET_CONFIG:=/usr/local/etc/dwatch-${PROFILE%-config*}.conf}"
: "${HOSTNAME:=$( hostname )}"
: "${REPORT_TYPE:=${PROFILE%-raw}}"
: "${TAG_NAME:=name}"
: "${UNMATCHED_LABEL:=}"

#
# Tabulate variables prior to loading config
#
vars_ignore="$( set | awk '
	match($0, /^[a-zA-Z_][a-zA-Z0-9_]*=/) {
		print substr($0, 1, RLENGTH - 1)
	}
' ) vars_ignore"

#
# Load config file
#
# NB: Allowing the config to tell us which vars we should unset via IGNORE
# NB: Making sure to nullify IGNORE before we load the config
#
IGNORE=
. "$JSON_NET_CONFIG" || exit

#
# Set default values
#
: "${UNMATCHED_LABEL:=unlabeled}"

#
# Determine which variables were set by config
# NB: Ignoring variables set prior to loading config [vars_ignore]
# NB: Ignoring "*_label" variables
#
vars_ignore="IGNORE $IGNORE $vars_ignore"
export vars_ignore # for awk(1) ENVIRON
confvars=$( set | awk '
	BEGIN {
		confvars = ""
		delete env
		nvars = split(ENVIRON["vars_ignore"], vars, /[[:space:]]+/)
		for (n = 1; n <= nvars; n++) ignore[vars[n]]
	}
	match($0, /^[a-zA-Z_][a-zA-Z0-9_]*=/) {
		name = substr($0, 1, RLENGTH - 1)
		if (name in ignore) next
		if (name ~ /_label$/) next
		confvars = confvars " " name
	}
	END { print substr(confvars, 2) }
' )

############################################################ OVERRIDES

#
# Override dwatch(1) settings
#
MAX_ARGS=0		# -B num
MAX_DEPTH=0		# -K num

#
# Unsupported dwatch(1) features
#
unset EXECREGEX		# -z regex
unset GROUP		# -g group
unset PID		# -p pid
unset PROBE_COALESCE	# -F
unset PSTREE		# -R
unset USER		# -u user

############################################################ EVENT ACTION

_EVENT_TEST="${EVENT_TEST:+($EVENT_TEST)}"
_EVENT_TEST="${CUSTOM_TEST:+$CUSTOM_TEST${EVENT_TEST:+ && }}$_EVENT_TEST"
if [ "$JID" ]; then
	pr_id="curthread->td_proc->p_ucred->cr_prison->pr_id"
	_EVENT_TEST="$pr_id == $JID${_EVENT_TEST:+ && ($_EVENT_TEST)}"
	unset JID
fi
EVENT_TEST="cpu == 0"
CUSTOM_TEST=

_CONF_OTHER=
IFS=" "
nconf=0
for var in $confvars; do
	nconf=$(( $nconf + 1 ))
	eval val=\"\$$var\"
	_CONF_OTHER="$_CONF_OTHER || ($val)"
done
[ "$_CONF_OTHER" ] && _CONF_OTHER="!(${_CONF_OTHER# || })"

############################################################ ACTIONS

exec 9<<EOF
this uint8_t	inet;
this string	event;
this string	family;
this string	local;
this string	remote;
this u_char	local6;
this u_char	remote6;
this uint16_t	lport;
this uint16_t	rport;
this uint32_t	length;

uint64_t	tcp_rcvd;
uint64_t	tcp_sent;
uint64_t	udp_rcvd;
uint64_t	udp_sent;

$( IFS=" "
	for var in $confvars; do
		printf "uint64_t\t%s_tcp_rcvd;\n" $var
		printf "uint64_t\t%s_tcp_sent;\n" $var
		printf "uint64_t\t%s_udp_rcvd;\n" $var
		printf "uint64_t\t%s_udp_sent;\n" $var
	done
)

typedef struct sainfo {
	sa_family_t sa_family;
	uint16_t port;
	string addr;
} sainfo_t;
 
#pragma D binding "1.13" sa_data_size
inline int sa_data_size = 30; /* 14 if only handling IPv4 */
#pragma D binding "1.13" sa_dummy_data
inline char *sa_dummy_data =
	"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";

#pragma D binding "1.13" sa_data_addr
inline string sa_data_addr[sa_family_t af, char data[sa_data_size]] =
	af == AF_INET ? inet_ntoa((in_addr_t *)&data[2]) :
	af == AF_INET6 ? inet_ntoa6((struct in6_addr *)&data[6]) :
	"<unknown>";

#pragma D binding "1.13" sa_data_port
inline uint16_t sa_data_port[sa_family_t af, char data[sa_data_size]] =
	af == AF_INET ? ((data[0] & 0xFF) << 8) + (data[1] & 0xFF) :
	af == AF_INET6 ? ((data[0] & 0xFF) << 8) + (data[1] & 0xFF) :
	0;

#pragma D binding "1.13" translator
translator sainfo_t < struct sockaddr *SA > {
	sa_family =	SA == NULL ? 0 : SA->sa_family;
	addr =		SA == NULL ?
	    sa_data_addr[0, sa_dummy_data] :
	    sa_data_addr[SA->sa_family, SA->sa_data];
	port =		SA == NULL ?
	    sa_data_port[0, sa_dummy_data] :
	    sa_data_port[SA->sa_family, SA->sa_data];
};

BEGIN /* probe ID $ID */
{${TRACE:+
	printf("<$ID>");}
	tcp_rcvd = tcp_sent = 0;
	udp_rcvd = udp_sent = 0;$( IFS=" "
	for var in $confvars; do
		printf "\n\t%s_tcp_rcvd = %s_tcp_sent = 0;" $var $var
		printf "\n\t%s_udp_rcvd = %s_udp_sent = 0;" $var $var
	done
)
}

/****************************** TCP ******************************/

tcp:::send,
tcp:::receive /* probe ID $(( $ID + 1 )) */
{${TRACE:+
	printf("<$(( $ID + 1 ))>");}
	this->length = (uint32_t)args[2]->ip_plength -
                (uint8_t)args[4]->tcp_offset;
	this->inet = 1;
}

tcp:::debug-user /* probe ID $(( $ID + 2 )) */
{${TRACE:+
	printf("<$(( $ID + 2 ))>");
}
	/*
	 * tcpsinfo_t *
	 */
	this->local  = args[0]->tcps_laddr;
	this->lport  = args[0]->tcps_lport;
	this->remote = args[0]->tcps_raddr;
	this->rport  = args[0]->tcps_rport;

	/*
	 * IPv6 support
	 */
	this->local6 = strstr(this->local, ":") != NULL ? 1 : 0;
	this->remote6 = strstr(this->remote, ":") != NULL ? 1 : 0;
	this->local = strjoin(strjoin(this->local6 ? "[" : "",
		this->local), this->local6 ? "]" : "");
	this->remote = strjoin(strjoin(this->remote6 ? "[" : "",
		this->remote), this->remote6 ? "]" : "");

	this->family = "tcp";
	this->event = prureq_string[arg1];
	this->inet = 1;
}

$( IFS=" "
	id=$(( $ID + 3 ))
	for var in $confvars; do
		eval val=\"\$$var\"
		printf "tcp:::debug-user\n"
		printf "\t/this->event == \"RCVD\""
		[ "$_EVENT_TEST" ] && printf " && %s" "$_EVENT_TEST"
		printf " && (%s)/\n" "$val"
		printf "\t/* probe ID %u */\n" $id
		printf "{${TRACE:+\n\tprintf(\"<$id>\");}\n"
		printf "\t%s_tcp_rcvd += this->length;\n" $var
		printf "}\n\n"
		id=$(( $id + 1 ))
	done
)

tcp:::debug-user
	/this->event == "RCVD"${_EVENT_TEST:+ &&
		$_EVENT_TEST}${_CONF_OTHER:+ &&
		$_CONF_OTHER}/
	/* probe ID $(( $ID + 3 + $nconf )) */
{${TRACE:+
	printf("<$(( $ID + 3 + $nconf ))>");}
	tcp_rcvd += this->length;
	this->length = 0;
}

$( IFS=" "
	id=$(( $ID + 4 + $nconf ))
	for var in $confvars; do
		eval val=\"\$$var\"
		printf "tcp:::send\n"
		printf "\t/"
		[ "$_EVENT_TEST" ] && printf "%s && " "$_EVENT_TEST"
		printf "(%s)/\n" "$val"
		printf "\t/* probe ID %u */\n" $id
		printf "{${TRACE:+\n\tprintf(\"<$id>\");}\n"
		printf "\t%s_tcp_sent += this->length;\n" $var
		printf "}\n\n"
		id=$(( $id + 1 ))
	done
)

tcp:::send /1${_EVENT_TEST:+ && $_EVENT_TEST}${_CONF_OTHER:+ &&
		$_CONF_OTHER}/
	/* probe ID $(( $ID + 4 + $nconf * 2 )) */
{${TRACE:+
	printf("<$(( $ID + 4 + $nconf * 2 ))>");}
	tcp_sent += this->length;
	this->length = 0;
}

/****************************** UDP ******************************/

/*
 * UDP send(2) [syscall] stacks:
 * sys_write -> dofilewrite -> soo_write -> sosend -> sosend_dgram -> udp:send
 * sys_sendto -> sendit -> kern_sendit -> sosend -> sosend_dgram -> udp:send
 * sys_sendmsg -> sendit -> kern_sendit -> sosend -> sosend_dgram -> udp:send
 */

udp:::send /* probe ID $(( $ID + 5 + $nconf * 2 )) */
{${TRACE:+
	printf("<$(( $ID + 5 + $nconf * 2 ))");
}
	/*
	 * ipinfo_t *
	 */
	this->local  = args[2]->ip_saddr;
	this->remote = args[2]->ip_daddr;

	/*
	 * udpinfo_t *
	 */
	this->length = (uint32_t)args[4]->udp_length - sizeof(struct udphdr);
	this->lport  = args[4]->udp_sport;
	this->rport  = args[4]->udp_dport;

	/*
	 * IPv6 support
	 */
	this->local6 = strstr(this->local, ":") != NULL ? 1 : 0;
	this->remote6 = strstr(this->remote, ":") != NULL ? 1 : 0;
	this->local = strjoin(strjoin(this->local6 ? "[" : "",
		this->local), this->local6 ? "]" : "");
	this->remote = strjoin(strjoin(this->remote6 ? "[" : "",
		this->remote), this->remote6 ? "]" : "");

	this->family = "udp";
	this->event = "SEND";
	this->inet = 1;
}

$( IFS=" "
	id=$(( $ID + 6 + $nconf * 2 ))
	for var in $confvars; do
		eval val=\"\$$var\"
		printf "udp:::send\n"
		printf "\t/"
		[ "$_EVENT_TEST" ] && printf "%s && " "$_EVENT_TEST"
		printf "(%s)/\n" "$val"
		printf "\t/* probe ID %u */\n" $id
		printf "{${TRACE:+\n\tprintf(\"<$id>\");}\n"
		printf "\t%s_udp_sent += this->length;\n" $var
		printf "}\n\n"
		id=$(( $id + 1 ))
	done
)

udp:::send /1${_EVENT_TEST:+ && $_EVENT_TEST}${_CONF_OTHER:+ &&
		$_CONF_OTHER}/
	/* probe ID $(( $ID + 6 + $nconf * 3 )) */
{${TRACE:+
	printf("<$(( $ID + 6 + $nconf * 3 ))>");}
	udp_sent += this->length;
	this->length = 0;
}

/*
 * UDP recv(2) [syscall] stacks:
 * sys_read -> dofileread -> soreceive -> soreceive_dgram
 * sys_recvfrom -> kern_recvit -> soreceive -> soreceive_dgram
 * sys_recvmsg -> kern_recvit -> soreceive -> soreceive_dgram
 */

syscall::read:entry,
syscall::recvfrom:entry,
syscall::recvmsg:entry
	/* probe ID $(( $ID + 7 + $nconf * 3 )) */
{${TRACE:+
	printf("<$(( $ID + 7 + $nconf * 3 ))>");
}
	this->inet = 0;
}

fbt:kernel:soreceive_dgram:entry /* probe ID $(( $ID + 8 + $nconf * 3 )) */
{${TRACE:+
	printf("<$(( $ID + 8 + $nconf * 3 ))>");
}
	this->udps = (struct socket *)args[0];
	this->af   = this->udps->so_proto->pr_domain->dom_family;
	this->psa  = (struct sockaddr **)args[1];
	this->sa   = (struct sockaddr *)NULL;

	this->so_pcb = this->udps->so_pcb;
	this->udpsinfo = xlate <udpsinfo_t> ((struct inpcb *)this->so_pcb);

	/* UDP receive local */
	this->local = this->udpsinfo.udps_laddr;
	this->local6 = this->af == AF_INET6;
	this->local = strjoin(strjoin(this->local6 ? "[" : "",
		this->local), this->local6 ? "]" : "");
	this->lport = this->udpsinfo.udps_lport;

	this->family = "udp";
	this->event = "RCVD";
	this->inet = (
		this->af == AF_INET ? 1 :
		this->af == AF_INET6 ? 1 :
		0
	);
}

fbt:kernel:soreceive_dgram:return /this->inet/
	/* probe ID $(( $ID + 9 + $nconf * 3 )) */
{${TRACE:+
	printf("<$(( $ID + 9 + $nconf * 3 ))>");
}
	this->sa = this->psa == NULL ? NULL : *this->psa;
	this->sainfo = xlate <sainfo_t> ((struct sockaddr *)this->sa);

	/* UDP receive remote */
	this->remote = this->sainfo.addr;
	this->remote6 = this->sainfo.sa_family == AF_INET6;
	this->remote = strjoin(strjoin(this->remote6 ? "[" : "",
		this->remote), this->remote6 ? "]" : "");
	this->rport = this->sainfo.port;
}

syscall::read:return,
syscall::recvfrom:return,
syscall::recvmsg:return
	/this->inet/ /* probe ID $(( $ID + 10 + $nconf * 3 )) */
{${TRACE:+
	printf("<$(( $ID + 10 + $nconf * 3 ))>");
}
	this->length = (uint32_t)((int)arg0 < 0 ? 0 : arg0);
	this->inet = (this->length > 0);
}

$( IFS=" "
	id=$(( $ID + 11 + $nconf * 3 ))
	for var in $confvars; do
		eval val=\"\$$var\"
		printf "syscall::read:return,\n"
		printf "syscall::recvfrom:return,\n"
		printf "syscall::recvmsg:return\n"
		printf "\t/this->inet && "
		[ "$_EVENT_TEST" ] && printf "%s && " "$_EVENT_TEST"
		printf "(%s)/\n" "$val"
		printf "\t/* probe ID %u */\n" $id
		printf "{${TRACE:+\n\tprintf(\"<$id>\");}\n"
		printf "\t%s_udp_rcvd += this->length;\n" $var
		printf "}\n\n"
		id=$(( $id + 1 ))
	done
)

syscall::read:return,
syscall::recvfrom:return,
syscall::recvmsg:return
	/this->inet${_EVENT_TEST:+ && $_EVENT_TEST}${_CONF_OTHER:+ &&
		$_CONF_OTHER}/
	/* probe ID $(( $ID + 11 + $nconf * 4 )) */
{${TRACE:+
	printf("<$(( $ID + 11 + $nconf * 4 ))>");}
	udp_rcvd += this->length;
	this->length = 0;
}
EOF
ACTIONS=$( cat <&9 )
ID=$(( $ID + 12 + $nconf * 4 ))

############################################################ EVENT TAG

# The EVENT_TAG is run inside the print action after the timestamp has been
# printed. By default, `UID.GID CMD[PID]: ' of the process is printed.

EVENT_TAG="printf(\"${PROFILE%-raw}: \")"

############################################################ EVENT DETAILS

SECONDS="walltimestamp / 1000000000"

exec 9<<EOF
	/*
	 * Print JSON
	 * NB: D does NOT allow for floating point calculations
	 */
	printf("{$(
		 jstr report_type "$REPORT_TYPE"
		_jstr hostname "$HOSTNAME"
		_jstr "$TAG_NAME" "$UNMATCHED_LABEL"
		_jraw epoch %u
		_jraw tcp_rcvd %lu
		_jraw tcp_rcvd_rate %lu
		_jraw tcp_sent %lu
		_jraw tcp_sent_rate %lu
		_jraw tcp_rw %lu
		_jraw tcp_rw_rate %lu
		_jraw udp_rcvd %lu
		_jraw udp_rcvd_rate %lu
		_jraw udp_sent %lu
		_jraw udp_sent_rate %lu
		_jraw udp_rw %lu
		_jraw udp_rw_rate %lu
	)}\n", $SECONDS,
		tcp_rcvd, tcp_rcvd / $PROBE_SECONDS,
		tcp_sent, tcp_sent / $PROBE_SECONDS,
		tcp_rcvd + tcp_sent, (tcp_rcvd + tcp_sent) / $PROBE_SECONDS,
		udp_rcvd, udp_rcvd / $PROBE_SECONDS,
		udp_sent, udp_sent / $PROBE_SECONDS,
		udp_rcvd + udp_sent, (udp_rcvd + udp_sent) / $PROBE_SECONDS);
	$( IFS=" "
	   for var in $confvars; do
	   	eval label=\"\$${var}_label\"
		printf "\n\tprintf(\"{"
			 jstr report_type "$REPORT_TYPE"
			_jstr hostname "$HOSTNAME"
			_jstr "$TAG_NAME" "${label:-$var}"
			_jraw epoch %u
			_jraw tcp_rcvd %lu
			_jraw tcp_rcvd_rate %lu
			_jraw tcp_sent %lu
			_jraw tcp_sent_rate %lu
			_jraw tcp_rw %lu
			_jraw tcp_rw_rate %lu
			_jraw udp_rcvd %lu
			_jraw udp_rcvd_rate %lu
			_jraw udp_sent %lu
			_jraw udp_sent_rate %lu
			_jraw udp_rw %lu
			_jraw udp_rw_rate %lu
		printf "}\\\n\""
		printf ", $SECONDS"
		printf ", %s_tcp_rcvd" $var
		printf ", %s_tcp_rcvd / %u" $var $PROBE_SECONDS
		printf ", %s_tcp_sent" $var
		printf ", %s_tcp_sent / %u" $var $PROBE_SECONDS
		printf ", %s_tcp_rcvd + %s_tcp_sent" $var $var
		printf ", (%s_tcp_rcvd + %s_tcp_sent) / %u" \
			$var $var $PROBE_SECONDS
		printf ", %s_udp_rcvd" $var
		printf ", %s_udp_rcvd / %u" $var $PROBE_SECONDS
		printf ", %s_udp_sent" $var
		printf ", %s_udp_sent / %u" $var $PROBE_SECONDS
		printf ", %s_udp_rcvd + %s_udp_sent" $var $var
		printf ", (%s_udp_rcvd + %s_udp_sent) / %u" \
			$var $var $PROBE_SECONDS
		printf ");"
	   done
	)

	/*
	 * Reset counters
	 */
	tcp_rcvd = tcp_sent = 0;
	udp_rcvd = udp_sent = 0;
$( IFS=" "
	for var in $confvars; do
		printf "\t%s_tcp_rcvd = %s_tcp_sent = 0;\n" $var $var
		printf "\t%s_udp_rcvd = %s_udp_sent = 0;\n" $var $var
	done
)

	this->event = "";
	this->inet = 0;
EOF
EVENT_DETAILS=$( cat <&9 )

################################################################################
# END
################################################################################
