While this blog might be still interesting to read, I consider the script shown below to be OBSOLETE in favor of the new script blogged here.
In my last post I have been outlining an alternative approach to load-balance TFTP services using NetScaler’s Direct Server Return method. However, a still missing piece is a protocol-level monitor that does not only show that a tftp daemon is listening on port 69 but also can deliver a certain file verified by checking its string contents.

In a good first approach is to use a udp-ecv monitor because it gives you notice of the tftp service not being available. However, it still would not be able to detect if a certain file exists.

The Citrix solution to this is to employ a user space monitor for this. The framework for this is to provide a perl script that follows the user monitor API, i. e., it contains a function that is registered with nsumond, the NetScaler user monitor daemon a process living in FreeBSD country that is contacted by the NetScaler via TCP/IP.

While the perl library has a nice TFTP implementation, the NS does not include it. Thus, I have decided to implement TFTP just because it so trivial (the FTP ) and because I did not want to have the dependency of installing something on the NS. It works by sending a custom created UDP packet to the TFTP server that constitutes a TFTP request for a specific file. If the server has the file, it answers with a UDP packet containing the beginning of the file. This content is checked against a user-provided string. If it matches, the monitor goes green, if it doesn’t or the server doesn’t answer, it goes red. The rest of the protocol is ignored.

Usage

Usage is quite simple.

  1. Download an unpack the attached script.
  2. In the root of your TFTP server create a short file monitor.txt containing a custom string, e. g. COOKIE.
  3. Create a user-space monitor uploading the script below. In script arguments, specify the name of the file and the content to be looked for, like file=monitor.txt;content=COOKIE
  4. If the service uses port *, set the monitor’s destination port to 69.
  5. Bind the monitor to the respective service.

That’s all. Don’t forget that there’s NO WARRANTY WHATSOEVER. (Tested on 9.2.49.8)

Future Work

While dusting off the two-year-old code, I have found lots of room for improvement. One is that your TFTP server might create a warning for each run of the monitor, because it does not close the connection properly. If I find time, I’ll redo this in a much nicer way without the downsides.

Only three things would be more cooler: If NetScaler would be packaged with perl’s TFTP library. Also, it would be nice if the packaged cURL would be compiled with TFTP support or if it would have a kernel-level TFTP monitor implementation.

Well it’s christmas. No better time to wish for something. But the message should be: No limits with NetScaler even if – in rare cases – there is no simple check-box to make something work. This product is UNSTOPPABLE.

The Code

Download

#!/usr/bin/perl -w
## This script is used to <span class="code-keyword">do</span> ftp monitoring using KAS feature.
## The mandatory arguments are:
##   1. File name whose existence will be checked.
## The optional arguments are:
## The arguments must be of the following form:
##      file=&lt;filename&gt;[;content=&lt;string&gt;]
## Example:
##  set monitor ...  -scriptArgs <span class="code-quote">"file=file"</span>

# This sample code is provided to you <span class="code-quote">"AS IS"</span> with no representations,
# warranties or conditions of any kind. You may use, modify and
# distribute it at your own risk.  CITRIX DISCLAIMS ALL WARRANTIES
# WHATSOEVER, EXPRESS, IMPLIED, WRITTEN, ORAL OR STATUTORY, INCLUDING
# WITHOUT LIMITATION WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE, TITLE AND NONINFRINGEMENT.  Without limiting the
# generality of the foregoing, you acknowledge and agree that (a) the
# sample code may exhibit errors, design flaws or other problems,
# possibly resulting in loss of data or damage to property; (b) it may
# not be possible to make the sample code fully functional; and (c)
# Citrix may, without notice or liability to you, cease to make
# available the current version and/or any <span class="code-keyword">future</span> versions of the
# sample code.  In no event should the code be used to support of
# ultra-hazardous activities, including but not limited to life
# support or blasting activities.  NEITHER CITRIX NOR ITS AFFILIATES
# OR AGENTS WILL BE LIABLE, UNDER BREACH OF CONTRACT OR ANY OTHER
# THEORY OF LIABILITY, FOR ANY DAMAGES WHATSOEVER ARISING FROM USE OF
# THE SAMPLE CODE, INCLUDING WITHOUT LIMITATION DIRECT, SPECIAL,
# INCIDENTAL, PUNITIVE, CONSEQUENTIAL OR OTHER DAMAGES, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.  Although the copyright
# in the code belongs to Citrix, any distribution of the code should
# include only your own standard copyright attribution, and not that
# of Citrix.  You agree to indemnify and defend Citrix against any and
# all claims arising from your use, modification or distribution of
# the code.
#

use strict;
use Socket;
use Netscaler::KAS;

## This function is a handler <span class="code-keyword">for</span> performing tftp probe in KAS mode
sub tftp_probe
{
  ## There must be at least 3 arguments to <span class="code-keyword">this</span> function.
  ##   1. First argument is the IP that has to be probed.
  ##  2. Second argument is the port to connect to.
  ##  3. Arguments to be used during probing.

  <span class="code-keyword">if</span>(scalar(@_) &lt; 3)
    {
      <span class="code-keyword">return</span> (1,<span class="code-quote">"Insufficient number of arguments"</span>);
    }

  ## Parse the argument given, to get file and the content
  ## If parsing fails, it is monitoring probe failure.
  $_[2]=~/file=([^;]+)(;content=([^;]+))/
    or <span class="code-keyword">return</span> (1, <span class="code-quote">"Invalid argument format"</span>);

  # please ignore <span class="code-keyword">this</span>
  # Variable Declaration and Initialization
  my $HOSTNAME = $_[0];
  my $PORTNO = $_[1];
  my $MAXLEN = 1024;
  my ($FILENAME, $CONTENT)=($1,$3);
  my ($MSG, $ipaddr, $portaddr, $host);

  # Create TFTP read request packet <span class="code-keyword">for</span> $FILENAME
  $MSG = pack(<span class="code-quote">"ccZ*Z*"</span>, 0, 1, $FILENAME,  <span class="code-quote">"netascii"</span>);

  # create socket
  socket(SOCKET, PF_INET, SOCK_DGRAM, getprotobyname(<span class="code-quote">"udp"</span>))
    or <span class="code-keyword">return</span>(1, <span class="code-quote">"socket: $!"</span>);
  # the next line actually sends the packet
  send(SOCKET, $MSG, 0, sockaddr_in($PORTNO, inet_aton($HOSTNAME))) == length($MSG)
    or <span class="code-keyword">return</span>(1, <span class="code-quote">"cannot send to $HOSTNAME($PORTNO): $!"</span>);
  # now receive on the same socket
  $portaddr = recv(SOCKET, $MSG, $MAXLEN, 0) or <span class="code-keyword">return</span>(1, <span class="code-quote">"recv: $!"</span>);

  unless ($MSG =~ /$CONTENT/ )
    {
      <span class="code-keyword">return</span> (1, <span class="code-quote">"Probe failed - Content not Found"</span>)
    }

  ## Probe succeeded
  <span class="code-keyword">return</span> 0;
}

## Register ftp probe handler, to the KAS module.

probe(\&amp;tftp_probe);