#!/bin/sh FAKE_IP_STATE="${FAKE_NETWORK_STATE}/ip-state" mkdir -p "$FAKE_IP_STATE" promote_secondaries=true not_implemented() { echo "ip stub command: \"$1\" not implemented" exit 127 } ###################################################################### ip_link() { case "$1" in set) shift # iface="$1" case "$2" in up) ip_link_set_up "$1" ;; down) ip_link_down_up "$1" ;; *) not_implemented "\"$2\" in \"$orig_args\"" ;; esac ;; show) shift ip_link_show "$@" ;; add*) shift ip_link_add "$@" ;; del*) shift ip_link_delete "$@" ;; *) not_implemented "$*" ;; esac } ip_link_add() { _link="" _name="" _type="" while [ -n "$1" ]; do case "$1" in link) _link="$2" shift 2 ;; name) _name="$2" shift 2 ;; type) if [ "$2" != "vlan" ]; then not_implemented "link type $1" fi _type="$2" shift 2 ;; id) shift 2 ;; *) not_implemented "$1" ;; esac done case "$_type" in vlan) if [ -z "$_name" ] || [ -z "$_link" ]; then not_implemented "ip link add with null name or link" fi mkdir -p "${FAKE_IP_STATE}/interfaces-vlan" echo "$_link" >"${FAKE_IP_STATE}/interfaces-vlan/${_name}" ip_link_set_down "$_name" ;; esac } ip_link_delete() { mkdir -p "${FAKE_IP_STATE}/interfaces-deleted" touch "${FAKE_IP_STATE}/interfaces-deleted/$1" rm -f "${FAKE_IP_STATE}/interfaces-vlan/$1" } ip_link_set_up() { rm -f "${FAKE_IP_STATE}/interfaces-down/$1" rm -f "${FAKE_IP_STATE}/interfaces-deleted/$1" } ip_link_set_down() { rm -f "${FAKE_IP_STATE}/interfaces-deleted/$1" mkdir -p "${FAKE_IP_STATE}/interfaces-down" touch "${FAKE_IP_STATE}/interfaces-down/$1" } ip_link_show() { dev="$1" if [ "$dev" = "dev" ] && [ -n "$2" ]; then dev="$2" fi if [ -e "${FAKE_IP_STATE}/interfaces-deleted/$dev" ]; then echo "Device \"${dev}\" does not exist." >&2 exit 255 fi if [ -r "${FAKE_IP_STATE}/interfaces-vlan/${dev}" ]; then read -r _link <"${FAKE_IP_STATE}/interfaces-vlan/${dev}" dev="${dev}@${_link}" fi _state="UP" _flags=",UP,LOWER_UP" if [ -e "${FAKE_IP_STATE}/interfaces-down/$dev" ]; then _state="DOWN" _flags="" fi case "$dev" in lo) _mac="00:00:00:00:00:00" _brd="00:00:00:00:00:00" _type="loopback" _state="UNKNOWN" _status="" _opts="mtu 65536 qdisc noqueue state ${_state}" ;; *) _mac=$(echo "$dev" | cksum | sed -r -e 's@(..)(..)(..).*@fe:fe:fe:\1:\2:\3@') _brd="ff:ff:ff:ff:ff:ff" _type="ether" _status="" _opts="mtu 1500 qdisc pfifo_fast state ${_state} qlen 1000" ;; esac if $brief; then printf '%-16s %-14s %-17s %s\n' \ "$dev" "$_status" "$_mac" "$_status" else echo "${n:-42}: ${dev}: ${_status} ${_opts}" echo " link/${_type} ${_mac} brd ${_brd}" fi } # This is incomplete because it doesn't actually look up table ids in # /etc/iproute2/rt_tables. The rules/routes are actually associated # with the name instead of the number. However, we include a variable # to fake a bad table id. [ -n "$IP_ROUTE_BAD_TABLE_ID" ] || IP_ROUTE_BAD_TABLE_ID=false ip_check_table() { _cmd="$1" if [ "$_cmd" = "route" ] && [ -z "$_table" ]; then _table="main" fi [ -n "$_table" ] || not_implemented "ip rule/route without \"table\"" # Only allow tables names from 13.per_ip_routing and "main". This # is a cheap way of avoiding implementing the default/local # tables. case "$_table" in ctdb.* | main) if $IP_ROUTE_BAD_TABLE_ID; then # Ouch. Simulate inconsistent errors from ip. :-( case "$_cmd" in route) echo "Error: argument \"${_table}\" is wrong: table id value is invalid" >&2 ;; *) echo "Error: argument \"${_table}\" is wrong: invalid table ID" >&2 ;; esac exit 255 fi ;; *) not_implemented "table=${_table} ${orig_args}" ;; esac } ###################################################################### ip_addr() { case "$1" in show | list | "") shift ip_addr_show "$@" ;; add*) shift ip_addr_add "$@" ;; del*) shift ip_addr_del "$@" ;; *) not_implemented "\"$1\" in \"$orig_args\"" ;; esac } ip_addr_show() { dev="" primary=true secondary=true _to="" if $brief; then not_implemented "ip -br addr show in \"$orig_args\"" fi while [ -n "$1" ]; do case "$1" in dev) dev="$2" shift 2 ;; # Do stupid things and stupid things will happen! primary) primary=true secondary=false shift ;; secondary) secondary=true primary=false shift ;; to) _to="$2" shift 2 ;; *) # Assume an interface name dev="$1" shift 1 ;; esac done devices="$dev" if [ -z "$devices" ]; then # No device specified? Get all the primaries... devices=$(find "${FAKE_IP_STATE}/addresses" -name "*-primary" | sed -e 's@.*/@@' -e 's@-.*-primary$@@' | sort -u) fi calc_brd() { case "${local#*/}" in 24) brd="${local%.*}.255" ;; 32) brd="" ;; *) not_implemented "list ... fake bits other than 24/32: ${local#*/}" ;; esac } show_iface() { ip_link_show "$dev" nets=$(find "${FAKE_IP_STATE}/addresses" -name "${dev}-*-primary" | sed -e 's@.*/@@' -e "s@${dev}-\(.*\)-primary\$@\1@") for net in $nets; do pf="${FAKE_IP_STATE}/addresses/${dev}-${net}-primary" sf="${FAKE_IP_STATE}/addresses/${dev}-${net}-secondary" if $primary && [ -r "$pf" ]; then read -r local scope <"$pf" if [ -z "$_to" ] || [ "${_to%/*}" = "${local%/*}" ]; then calc_brd echo " inet ${local} ${brd:+brd ${brd} }scope ${scope} ${dev}" fi fi if $secondary && [ -r "$sf" ]; then while read -r local scope; do if [ -z "$_to" ] || [ "${_to%/*}" = "${local%/*}" ]; then calc_brd echo " inet ${local} ${brd:+brd }${brd} scope ${scope} secondary ${dev}" fi done <"$sf" fi if [ -z "$_to" ]; then echo " valid_lft forever preferred_lft forever" fi done } n=1 for dev in $devices; do if [ -z "$_to" ] || grep -F "${_to%/*}/" "${FAKE_IP_STATE}/addresses/${dev}-"* >/dev/null; then show_iface fi n=$((n + 1)) done } # Copied from 13.per_ip_routing for now... so this is lazy testing :-( ipv4_host_addr_to_net() { _addr="$1" _host="${_addr%/*}" _maskbits="${_addr#*/}" # Convert the host address to an unsigned long by splitting out # the octets and doing the math. _host_ul=0 # Want word splitting here # shellcheck disable=SC2086 for _o in $( export IFS="." echo $_host ); do _host_ul=$(((_host_ul << 8) + _o)) # work around Emacs color bug done # Calculate the mask and apply it. _mask_ul=$((0xffffffff << (32 - _maskbits))) _net_ul=$((_host_ul & _mask_ul)) # Now convert to a network address one byte at a time. _net="" for _o in $(seq 1 4); do _net="$((_net_ul & 255))${_net:+.}${_net}" _net_ul=$((_net_ul >> 8)) done echo "${_net}/${_maskbits}" } ip_addr_add() { local="" dev="" brd="" scope="global" while [ -n "$1" ]; do case "$1" in *.*.*.*/*) local="$1" shift ;; local) local="$2" shift 2 ;; broadcast | brd) # For now assume this is always '+'. if [ "$2" != "+" ]; then not_implemented "addr add ... brd $2 ..." fi shift 2 ;; dev) dev="$2" shift 2 ;; scope) scope="$2" shift 2 ;; *) not_implemented "$@" ;; esac done if [ -z "$dev" ]; then not_implemented "addr add (without dev)" fi mkdir -p "${FAKE_IP_STATE}/addresses" net_str=$(ipv4_host_addr_to_net "$local") net_str=$(echo "$net_str" | sed -e 's@/@_@') pf="${FAKE_IP_STATE}/addresses/${dev}-${net_str}-primary" sf="${FAKE_IP_STATE}/addresses/${dev}-${net_str}-secondary" # We could lock here... but we should be the only ones playing # around here with these stubs. if [ ! -f "$pf" ]; then echo "$local $scope" >"$pf" elif grep -Fq "$local" "$pf"; then echo "RTNETLINK answers: File exists" >&2 exit 254 elif [ -f "$sf" ] && grep -Fq "$local" "$sf"; then echo "RTNETLINK answers: File exists" >&2 exit 254 else echo "$local $scope" >>"$sf" fi } ip_addr_del() { local="" dev="" while [ -n "$1" ]; do case "$1" in *.*.*.*/*) local="$1" shift ;; local) local="$2" shift 2 ;; dev) dev="$2" shift 2 ;; *) not_implemented "addr del ... $1 ..." ;; esac done if [ -z "$dev" ]; then not_implemented "addr del (without dev)" fi mkdir -p "${FAKE_IP_STATE}/addresses" net_str=$(ipv4_host_addr_to_net "$local") net_str=$(echo "$net_str" | sed -e 's@/@_@') pf="${FAKE_IP_STATE}/addresses/${dev}-${net_str}-primary" sf="${FAKE_IP_STATE}/addresses/${dev}-${net_str}-secondary" # We could lock here... but we should be the only ones playing # around here with these stubs. if [ ! -f "$pf" ]; then echo "RTNETLINK answers: Cannot assign requested address" >&2 exit 254 elif grep -Fq "$local" "$pf"; then if $promote_secondaries && [ -s "$sf" ]; then head -n 1 "$sf" >"$pf" sed -i -e '1d' "$sf" else # Remove primaries AND SECONDARIES. rm -f "$pf" "$sf" fi elif [ -f "$sf" ] && grep -Fq "$local" "$sf"; then grep -Fv "$local" "$sf" >"${sf}.new" mv "${sf}.new" "$sf" else echo "RTNETLINK answers: Cannot assign requested address" >&2 exit 254 fi } ###################################################################### ip_rule() { case "$1" in show | list | "") shift ip_rule_show "$@" ;; add) shift ip_rule_add "$@" ;; del*) shift ip_rule_del "$@" ;; *) not_implemented "$1 in \"$orig_args\"" ;; esac } # All non-default rules are in $FAKE_IP_STATE_RULES/rules. As with # the real version, rules can be repeated. Deleting just deletes the # 1st match. ip_rule_show() { if $brief; then not_implemented "ip -br rule show in \"$orig_args\"" fi ip_rule_show_1() { _pre="$1" _table="$2" _selectors="$3" # potentially more options printf "%d:\t%s lookup %s \n" "$_pre" "$_selectors" "$_table" } ip_rule_show_some() { _min="$1" _max="$2" [ -f "${FAKE_IP_STATE}/rules" ] || return while read -r _pre _table _selectors; do # Only print those in range if [ "$_min" -le "$_pre" ] && [ "$_pre" -le "$_max" ]; then ip_rule_show_1 "$_pre" "$_table" "$_selectors" fi done <"${FAKE_IP_STATE}/rules" } ip_rule_show_1 0 "local" "from all" ip_rule_show_some 1 32765 ip_rule_show_1 32766 "main" "from all" ip_rule_show_1 32767 "default" "from all" ip_rule_show_some 32768 2147483648 } ip_rule_common() { _from="" _pre="" _table="" while [ -n "$1" ]; do case "$1" in from) _from="$2" shift 2 ;; pref) _pre="$2" shift 2 ;; table) _table="$2" shift 2 ;; *) not_implemented "$1 in \"$orig_args\"" ;; esac done [ -n "$_pre" ] || not_implemented "ip rule without \"pref\"" ip_check_table "rule" # Relax this if more selectors added later... [ -n "$_from" ] || not_implemented "ip rule without \"from\"" } ip_rule_add() { ip_rule_common "$@" _f="${FAKE_IP_STATE}/rules" touch "$_f" ( flock 0 # Filter order must be consistent with the comparison in ip_rule_del() echo "$_pre $_table${_from:+ from }$_from" >>"$_f" ) <"$_f" } ip_rule_del() { ip_rule_common "$@" _f="${FAKE_IP_STATE}/rules" touch "$_f" # ShellCheck doesn't understand this flock pattern # shellcheck disable=SC2094 ( flock 0 _tmp="${_f}.new" : >"$_tmp" _found=false while read -r _p _t _s; do if ! $_found && [ "$_p" = "$_pre" ] && [ "$_t" = "$_table" ] && [ "$_s" = "${_from:+from }$_from" ]; then # Found. Skip this one but not future ones. _found=true else echo "$_p $_t $_s" >>"$_tmp" fi done if cmp -s "$_tmp" "$_f"; then # No changes, must not have found what we wanted to delete echo "RTNETLINK answers: No such file or directory" >&2 rm -f "$_tmp" exit 2 else mv "$_tmp" "$_f" fi ) <"$_f" || exit $? } ###################################################################### ip_route() { case "$1" in show | list) shift ip_route_show "$@" ;; flush) shift ip_route_flush "$@" ;; add) shift ip_route_add "$@" ;; del*) shift ip_route_del "$@" ;; *) not_implemented "$1 in \"ip route\"" ;; esac } ip_route_common() { if [ "$1" = table ]; then _table="$2" shift 2 fi ip_check_table "route" } # Routes are in a file per table in the directory # $FAKE_IP_STATE/routes. These routes just use the table ID # that is passed and don't do any lookup. This could be "improved" if # necessary. ip_route_show() { ip_route_common "$@" # Missing file is just an empty table sort "$FAKE_IP_STATE/routes/${_table}" 2>/dev/null || true } ip_route_flush() { ip_route_common "$@" rm -f "$FAKE_IP_STATE/routes/${_table}" } ip_route_add() { _prefix="" _dev="" _gw="" _table="" _metric="" while [ -n "$1" ]; do case "$1" in *.*.*.*/* | *.*.*.*) _prefix="$1" shift 1 ;; local) _prefix="$2" shift 2 ;; dev) _dev="$2" shift 2 ;; via) _gw="$2" shift 2 ;; table) _table="$2" shift 2 ;; metric) _metric="$2" shift 2 ;; *) not_implemented "$1 in \"$orig_args\"" ;; esac done ip_check_table "route" [ -n "$_prefix" ] || not_implemented "ip route without inet prefix in \"$orig_args\"" # This can't be easily deduced, so print some garbage. [ -n "$_dev" ] || _dev="ethXXX" # Alias or add missing bits case "$_prefix" in 0.0.0.0/0) _prefix="default" ;; */*) : ;; *) _prefix="${_prefix}/32" ;; esac _f="$FAKE_IP_STATE/routes/${_table}" mkdir -p "$FAKE_IP_STATE/routes" touch "$_f" # Check for duplicate _prefix_regexp=$(echo "^${_prefix}" | sed -e 's@\.@\\.@g') if [ -n "$_metric" ]; then _prefix_regexp="${_prefix_regexp} .*metric ${_metric} " fi if grep -q "$_prefix_regexp" "$_f"; then echo "RTNETLINK answers: File exists" >&2 exit 1 fi ( flock 0 _out="${_prefix} " [ -z "$_gw" ] || _out="${_out}via ${_gw} " [ -z "$_dev" ] || _out="${_out}dev ${_dev} " [ -n "$_gw" ] || _out="${_out} scope link " [ -z "$_metric" ] || _out="${_out} metric ${_metric} " echo "$_out" >>"$_f" ) <"$_f" } ip_route_del() { _prefix="" _dev="" _gw="" _table="" _metric="" while [ -n "$1" ]; do case "$1" in *.*.*.*/* | *.*.*.*) _prefix="$1" shift 1 ;; local) _prefix="$2" shift 2 ;; dev) _dev="$2" shift 2 ;; via) _gw="$2" shift 2 ;; table) _table="$2" shift 2 ;; metric) _metric="$2" shift 2 ;; *) not_implemented "$1 in \"$orig_args\"" ;; esac done ip_check_table "route" [ -n "$_prefix" ] || not_implemented "ip route without inet prefix in \"$orig_args\"" # This can't be easily deduced, so print some garbage. [ -n "$_dev" ] || _dev="ethXXX" # Alias or add missing bits case "$_prefix" in 0.0.0.0/0) _prefix="default" ;; */*) : ;; *) _prefix="${_prefix}/32" ;; esac _f="$FAKE_IP_STATE/routes/${_table}" mkdir -p "$FAKE_IP_STATE/routes" touch "$_f" # ShellCheck doesn't understand this flock pattern # shellcheck disable=SC2094 ( flock 0 # Escape some dots [ -z "$_gw" ] || _gw=$(echo "$_gw" | sed -e 's@\.@\\.@g') _prefix=$(echo "$_prefix" | sed -e 's@\.@\\.@g' -e 's@/@\\/@') _re="^${_prefix}\>.*" [ -z "$_gw" ] || _re="${_re}\.*" [ -z "$_dev" ] || _re="${_re}\.*" [ -z "$_metric" ] || _re="${_re}.*\.*" sed -i -e "/${_re}/d" "$_f" ) <"$_f" } ###################################################################### orig_args="$*" brief=false case "$1" in -br*) brief=true shift ;; esac case "$1" in link) shift ip_link "$@" ;; addr*) shift ip_addr "$@" ;; rule) shift ip_rule "$@" ;; route) shift ip_route "$@" ;; *) not_implemented "$1" ;; esac exit 0