#!/bin/sh _scriptname="udp_relay" ################################################################################ # UDP Relay script for pfSense # Allows relaying UDP broadcasts between subnets # # WARNING: # Using this script circumvents both the firewall and other security measures # on pfSense. It's a dirty hack for special situations. NOT recommended for # extended use # # Requirements (SORRY!): # This script requires the installation of "bash" and "tcpreplay" # It will attempt to install them if they can not be found # See https://doc.pfsense.org/index.php/Installing_FreeBSD_Packages for more details # # Parameters: # -i # Install mode - attempt to install missing packages # -l # Listen - Launches a separate tcpdump process to print relayed packets # -L # Set a lockfile to prevent starting multiple instances doing the same thing # -m # Relay multicast packets # -M # Relay only multicast packets (not broadcast) # -p # Filter for specific ports. Can be specified more than once # -R # Pseudo-NAT broadcasts to correct subnet # -t # Reduce TTL of relayed packets. # Can prevent disaster in case of a network loop, but would at the same time disguise the problem - Use with caution. # -v # Attempt to correct VLAN tags (EXPERIMENTAL) # # Network spec: # [+|-] # [+|-] Prefix a network with + to only read from this interface (no packets will be written to it) # Prefix a network with - to only write to this interface (no packets will be read from it) # if igb1, igb1_vlan3, etc... # # Sample usage: # ./udp_relay igb1 igb2 # ./udp_relay -- -igb1 +igb2 # ./udp_relay -l -p 1234 igb1 igb2 # ./udp_relay igb1 igb2 igb3 # # Crontab example: # @reboot /root/udp_relay -m -R -L /tmp/.udp_relay.lock -- em2 em5 >/dev/null 2>&1 # */5 * * * * /root/udp_relay -m -R -L /tmp/.udp_relay.lock -- em2 em5 >/dev/null 2>&1 # # # where is it? bash="/usr/local/bin/bash" curl="/usr/local/bin/curl" pkg="/usr/sbin/pkg" ifconfig="/sbin/ifconfig" tcpdump="/usr/sbin/tcpdump" tcpreplay="/usr/local/bin/tcpreplay" tcprewrite="/usr/local/bin/tcprewrite" tmp="/tmp/.udp_relay-pkg.log" list="/tmp/.udp_relay-pkg.list" # bash? if ! "$pkg" info | grep -q "^bash"; then echo "bash is not installed - attempting to fetch it" "$pkg" >/dev/null 2>&1 "$pkg" update -f "$pkg" install -y bash echo fi # switch to bash if [ -z "$BASH_VERSION" ]; then script=$0 exec "$bash" "$script" $* echo "Failed to exec $bash" >&2 exit 1 # just in case the above fails fi # env set -u set -e # cleanup declare -a delete=( "$tmp" "$list" ) function cleanup { for file in "${delete[@]}"; do rm "$file" 2>/dev/null || : done trap : TERM kill -TERM 0 } trap cleanup EXIT # options listen=false vlan=false filter="broadcast" pidfile="" ttl=false pnat=false install=false ports=() OPTIND=1 while getopts ":ilL:mMp:vRt" opt; do case "$opt" in i) install=true ;; l) listen=true ;; L) pidfile=$OPTARG ;; m) filter="(broadcast or multicast)" ;; M) filter="multicast" ;; p) ports+=( $OPTARG ) ;; R) pnat=true ;; t) ttl=true ;; v) vlan=true ;; [?]) echo "Unknown option -${OPTARG}" >&2 exit 1 ;; :) echo "Option -${OPTARG} requires an argument" >&2 exit 1 ;; esac done shift $((OPTIND-1)) if [[ "${1:-}" = "--" ]]; then shift; fi # tcpreplay? if ! "$pkg" info | grep -q "^tcpreplay"; then if ! $install; then echo "tcpreplay is not installed, and we're not running in install mode - will not attempt to install it" >&2 echo "tip: run with -i to enable install mode and attempt to install missing packages" >&2 exit 1 fi echo "tcpreplay is not installed - attempting to fetch it" abi=$("$pkg" -vv | grep "^ABI " | head -n 1 | cut -d\" -f 2) echo "ABI: $abi" url=$(grep "url: " /etc/pkg/FreeBSD.conf | head -n 1 | cut -d\" -f 2 | sed -e 's/^.*\(http.*\)/\1/' -e "s/\${ABI}/${abi}/" -e 's/$/\/All\//') echo "URL: $url" "$curl" --silent "$url" >"$list" wants=( "tcpreplay" ) while [[ -n "${wants[*]}" ]]; do last="${wants[${#wants[@]}-1]}" echo "debug: $(grep "$last" "$list")" file=$(cut -d\" -f 4 "$list" | egrep "^${last}-([0-9\._]+).txz$" | head -n 1) if [[ -z "$file" ]]; then echo "Dependency not found: $last" exit 1 fi echo "PKG: ${url}${file}" if ! "$pkg" add "${url}${file}" >"$tmp" 2>&1; then missing=$(grep '^pkg: Missing dependency' "$tmp" | head -n 1 | cut -d "'" -f 2) if [[ -n "$missing" ]]; then echo "Detected dependency: $missing" wants+=( "$missing" ) continue fi cat "$tmp" >&2 exit 1 fi cat "$tmp" unset wants[${#wants[@]}-1] done echo fi # deps are okay at this point if $install; then echo "All dependencies seem to be installed." fi # sanity function printusage { echo "Usage: $0 [options] ..."; } if (( $# < 2 )); then printusage exit $(( $# == 0 ? 0 : 1 )) fi cards=( "$@" ) for ((i=0; i<${#cards[@]}; i++)); do if ! $ifconfig "${cards[i]}" >/dev/null; then echo "Bad card/network spec: ${cards[i]}" >&2 printusage exit 1 fi done # pidfile? if [[ -n "$pidfile" ]]; then if oldpid=$(cat "$pidfile" 2>/dev/null) && ! kill -0 "$oldpid" 2>/dev/null; then rm -f "$pidfile" 2>/dev/null || : sleep 5 fi if (set -C && umask 0077 && echo $$ >"${pidfile}") &>/dev/null; then delete+=( "$pidfile" ) echo "Lockfile created" else echo "Lockfile exists!" exit 0 fi fi # func function nobuf { stdbuf -i0 -o0 -e0 "$@"; } function sf_bin2ipv4 { printf '%s.%s.%s.%s\n' \ "$(sf_bin2dec "${1:10#00:8}")" \ "$(sf_bin2dec "${1:10#08:8}")" \ "$(sf_bin2dec "${1:10#16:8}")" \ "$(sf_bin2dec "${1:10#24:8}")" } function sf_bin2dec { echo $((2#${1})) } function getnets { # $ifconfig "$1" | egrep "^[[:space:]]inet[[:space:]]" | awk '{print $2,$4}' | $ifconfig "$1" | perl -n -e '/[[:space:]]+inet[[:space:]]+([^[:space:]]+).*[[:space:]]+netmask[[:space:]]+([^[:space:]]+)/ && print $1," ",$2,"\n"' | while read -r ip mask; do if [[ "$mask" != "0x"* ]]; then echo "Invalid subnet mask for interface ${1}: $mask" >&2 return 1 fi mask="${mask#0x}" mask="$(bc <<<"ibase=16;obase=2;${mask^^}")" mask="${mask%%0*}" mask="${#mask}" IFS='.' read -r -a iparray <<<"$ip" ip="" for dec in "${iparray[@]}"; do bin="$(bc <<<"obase=2;ibase=10;$dec")" printf -v ip '%s%8s' "$ip" "$bin" done printf -v ip '%-32s' "${ip::$mask}" ip="${ip// /0}" printf -v ip '%s.%s.%s.%s' \ "$(bc <<<"obase=10;ibase=2;${ip:10#00:8}")" \ "$(bc <<<"obase=10;ibase=2;${ip:10#08:8}")" \ "$(bc <<<"obase=10;ibase=2;${ip:10#16:8}")" \ "$(bc <<<"obase=10;ibase=2;${ip:10#24:8}")" echo "${ip}/${mask}" done } function bpf_src_or { echo -n "(src net $1"; shift; while (( $# )); do echo -n " or src net $1"; shift; done; echo -n ")"; } function bpf_dst_port_or { echo -n "(dst port $1"; shift; while (( $# )); do echo -n " or dst port $1"; shift; done; echo -n ")"; } function run { local cmd="$@" echo "CMD: ${cmd#nobuf }" >&2 "$@" } function commasplit { [[ -n "$@" ]] || return 0 local split i IFS=',' read -ra split <<<"$*" for ((i=0; i<"${#split[@]}"; i++)); do ! (( i )) || echo -n " " echo -n "${split[i]}" done } function dump { local card="$1" nets plist="" ttlfilter="" nets="$(getnets "$card")" shift [[ -z "$nets" ]] || nets="$(bpf_src_or $nets)" [[ -z "${ports[@]:-}" ]] || plist="$(bpf_dst_port_or "${ports[@]}")" run nobuf "$tcpdump" -n -i "$card" "$@" "udp and $filter ${nets:+"and $nets"} ${ttlfilter:+"and $ttlfilter"} ${plist:+"and $plist"}" } function rewrite { local opts=() local card_src="$1" local card_dst="$2" local nets_src=( $(getnets "$1") ) local nets_dst=( $(getnets "$2") ) echo "NETS: ${nets_src[@]} $card_src" >&2 # vlan if $vlan; then local vlan vlan="$($ifconfig "$card_dst" | egrep "(^| )vlan" | awk '{print $2}')" if [[ -n "$vlan" ]]; then opts+=( "--enet-vlan=add" "--enet-vlan-tag=${vlan}" "--enet-vlan-pri=0" ) else opts+=( "--enet-vlan=del" "--enet-vlan-pri=0" ) fi fi # pnat if $pnat && (( ${#nets_src[@]} )) && (( ${#nets_dst[@]} )); then local map="" from to="${nets_dst[0]:-}" for from in "${nets_src[@]}"; do map+="${map:+,}${from}:${to}" done opts+=( "--dstipmap=$map" ) fi # ttl if $ttl; then opts+=( "--ttl=-1" ) fi # rewrite! if (( ${#opts[@]} )); then run nobuf "$tcprewrite" --infile=- --outfile=- "${opts[@]}" else nobuf cat fi } function replay { run "$tcpreplay" --topspeed --intf1="$1" -; } function listen { $listen || return 0 local card="$1" cname="$1" dump "$card" -e | sed "s/^\([^ ][^ ]*\)/\1 $cname/g" & } function relay { local card1="$1" card2="$2" dump "$card1" -w- | rewrite "$card1" "$card2" | replay "$card2" & } # init echo "Relaying..." for ((i=0; i<${#cards[@]}; i++)); do card1="${cards[i]}" listen "$card1" for ((j=0; j<${#cards[@]}; j++)); do card2="${cards[j]}" [[ "$card1" != "$card2" ]] || continue [[ "$card1" != '-'* ]] || continue [[ "$card2" != '+'* ]] || continue relay "${card1#'+'}" "${card2#'-'}" done done # wait for exit wait -n || : echo "Child exited" # vim: tabstop=4:softtabstop=4:shiftwidth=4:noexpandtab