Running cron jobs on a Juniper Router

Gabriel L. Somlo, June 2005

Update (March 2009): Importing binaries compiled elsewhere may no longer work on current JunOS versions. I no longer have access to a Juniper router to verify this, so please take the information below with a grain of salt.

1. Introduction

Among other things, this document is a ringing endorsement of Juniper's choice to build their routers on top of a UNIX platform. Viewed from this angle, a Juniper router (such as the M10) is really a Pentium-class PC running FreeBSD v4, with a really, really beefy network card (i.e., the forwarding engine).

One of the great things about the router being a UNIX box is that one can write cron jobs on it. The router updates itself automatically from within, avoiding the requirement to have an expect script drive the update from a remote host. Why does this matter? For an expect script to work, the cleartext password to the router (or an ssh key allowing access to it) must be stored on the machine running expect, which weakens security.

This document describes how to set up a cron job that automatically updates two prefix lists:

2. The Juniper cli command

Just like any other UNIX box, the Juniper router has user accounts, stored in the /etc/passwd file. The root user's entry looks something like this:

root:*:0:0:Root User:/root:/bin/csh
which means that root gets /bin/csh as the default login shell. So far, so good. On the other hand, regular user accounts look something like this:
somlo:*:1005:20:Gabriel Somlo:/var/home/somlo:/usr/sbin/cli
Notice that somlo's default shell is /usr/sbin/cli. This means that, upon login to the Juniper router, somlo gets the familiar Juniper command line interface instead of a shell prompt.

The really nice thing about the cli program is that it accepts commands on stdin (and hence allows scripting), and doesn't restrict its use to interactive mode. In other words, logging into the router and executing the following commands at the cli prompt:

somlo@gw1> configure
Entering configuration mode

[edit]
somlo@gw1# load replace foo 
load complete

[edit]
somlo@gw1# commit check 
configuration check succeeds

[edit]
somlo@gw1# commit 
commit complete

[edit]
somlo@gw1# exit 
Exiting configuration mode

somlo@gw1> exit 

Connection to gw1 closed.
is equivalent to running the following shell script:
{
  echo "configure"
  echo "load replace foo"
  echo "commit check"
  echo "commit"
  echo "exit"
} | /usr/sbin/cli

3. Obtaining and compiling additional binaries

The Juniper runs a stripped-down version of FreeBSD v.4, which means that some required binaries had to be compiled and copied over from elsewhere. This list includes:

Since the Juniper doesn't come with a C compiler (or any other development tools, for that matter), a separate FreeBSD 4.11 machine was built to allow these programs to be compiled. Because some of these programs require the existence of certain libraries which may or may not be included with the Juniper's version of BSD, each binary was built with the -static flag. For instance:

gcc -o iprange iprange.c -O2 -Wall -static
If the build process was controlled by a Makefile, the last command executed by make, which built the binary, was rerun manually with the -static option added. Aditionally, each binary was stripped to minimize its size:
strip iprange
before being copied over to the Juniper.

4. The Shell Script

Firewall terms were written which match addresses based on a source- or destination-prefix-list. These firewall terms can remain static, and changes occur by re-loading the prefix lists themselves from a shell script that drives cli and is driven in turn by cron. First, the shell script builds a prefix of I2 addresses, by dumping out routes received via BGP that contain the Abilene AS number in their AS Path and aggregating them with iprange.c. Next, the SPAMHAUS DROP list is downloaded using wget and aggregated with iprange.c. Then, both lists are loaded and committed into the router config using the cli command. A log of the transaction (consisting of the stdout output of the scripted commands) is then emailed out to the router's administrator(s). The script is shown below:

#!/bin/sh
#
# Automatically update portions of the Juniper config file
#
# Gabriel L. Somlo,    Mar. 2005

TIMESTAMP=$(date +%Y-%m-%d)


###############################################################################
# Build prefix list of I2 addresses, by aggregating routes
# received via the Abilene AS number (11537)
###############################################################################

BGP_PEER="AAA.BBB.CCC.DDD"
{
  echo "policy-options {"
  echo "replace:"
  echo "    prefix-list I2_prefixes {"
  {
    echo "set cli screen-length 0"
    echo "show route receive-protocol bgp ${BGP_PEER} aspath-regex .*11537.* table inet.0"
  } | cli \
    | grep '^*' \
    | awk '{print $2}' \
    | ./iprange -J \
    | sed -e 's/^\(.*\)$/        \1;/'
  echo "    }"
  echo "}"
} > pfx_i2_${TIMESTAMP}


###############################################################################
# Build prefix list for firewall term blocking traffic from networks listed by
# the Spamhaus DROP list
###############################################################################

{
  echo "policy-options {"
  echo "replace:"
  echo "    prefix-list SPAMHAUS_prefixes {"
  ./wget -q 'http://www.spamhaus.org/DROP/drop.lasso' -O - \
    | grep -v ^\; \
    | grep -v ^$ \
    | awk '{print $1}' \
    | sort -t. -k1,1n -k2,2n -k3,3n -k4,4n \
    | ./iprange -J \
    | sed -e 's/^\(.*\)$/        \1;/'
  echo "    }"
  echo "}"
} > pfx_spamhaus_${TIMESTAMP}


###############################################################################
# load/replace the new prefix lists and commit changes;
# email report
###############################################################################

REPORT_RCPT="noc@acns.colostate.edu"
MAIL_GW="TTT.XXX.YYY.ZZZ"
{
  echo "Subject: gw1 AutoUpdate report: ${TIMESTAMP}"
  echo
  echo "I2 Prefix list line count:"
  echo
  wc -l pfx_i2_${TIMESTAMP}
  echo
  echo "Spamhaus DROP Prefix list line count:"
  echo
  wc -l pfx_spamhaus_${TIMESTAMP}
  echo
  echo "Loading and committing changes:"
  echo
  {
    echo "configure"
    echo "load replace pfx_i2_${TIMESTAMP}"
    echo "load replace pfx_spamhaus_${TIMESTAMP}"
    echo "commit check"
    echo "commit"
    echo "exit"
  } | cli
  echo
  echo "Done."
} | ./mini_sendmail -s${MAIL_GW} ${REPORT_RCPT}


###############################################################################
# delete log files older than two weeks (14 days)
###############################################################################

for F in $(find ./ -name pfx\* -ctime +14); do
  rm -f "${F}"
done

5. Odds and Ends

For this script to work, it must be called from one of the admin users' crontab, like so:

# Run the AutoUpdate script daily at 1:05am
5 1 * * * (cd /var/home/somlo/AutoUpdate; ./AutoUpdate.sh)
It is assumed that established TCP responses from the mail gateway and SPAMHAUS's Web server are allowed by the route-engine filter, and so are DNS packets from the name servers configured in system / name-server.


Last modified: June 17, 2005; Send corrections and comments to somlo [at] acns dot colostate dot edu