Pages

12/17/2002

Perl::Citrix::Internet::Network::Server Failover


Background
Internally we use a Citrix server farm to provide desktop and application services to our small remote offices for business hours operation. The server farm also provides these desktop and application services to all our users for after hours remote access. The server farm provides application load balancing based on server load at the time the user makes a connection.
The published applications and load balancing is provided by a Citrix ICA Browser server process. The server that runs this process is "elected" by the servers in the farm. So, when the server providing this function goes down, it is picked up by another.
Inside our network our remote offices access the Citrix farm via the Citrix client configured with a published application.
From the internet our remote users access the server via their web browser which launches a client plugin. The web page used for this access is configured for the published application and must specify the outside IP address of the Citrix ICA Browser.
The Problem
If the primary Citrix ICA Browser server is down then the Citrix desktop is not accessible from the internet since the web page provided to users must include a specific IP number.
Workaround
I considered a number of workarounds/solutions. On the front end, round robin DNS or "load balancing the DNS name" would still result in failure when it happens to direct the user to the IP address of the down server. On the back end, Windows Network Load Balancing would theoretically work okay for only Windows Terminal Services. However, it does not work with Citrix server farm. This is because for some reason the ICA browser service doesn't always bind with the Windows Network Load Balancing network adapter, according to this news posting.
I ended up with a simple idea - have my web server present a different web page with a different IP address to the user if the primary server is down. Using Perl I created a surprisingly simple script that will check for response on the ICA service port on the primary server, if it doesn't response it will check for response on another server, if that server doesn't respond the user get's a web page telling them that the server is unavailable.
I haven't pursued the idea of "load balancing" using this method. The jist of it would be to start with a different server each time alternating between a list of servers. This concept would be useful for other services/ports.
The script is below:
#!/usr/local/bin/perl

use IO::Socket;
#ICA server is at port 1494
$port = ("1494");
#change a.b.c.d & w.x.y.z to your hosts' ip numbers
$primary = "a.b.c.d";
$secondary = "w.x.y.z";
#Check Primary
$connected = 0;
$checkport = IO::Socket::INET->new(
PeerAddr => "$primary",
PeerPort => "$port",
Proto => 'tcp',
Timeout => '0') or $connected = 1;
if (!($connected)) {
#port is up, assign $link to the HTML for a link to the file for this server.
$link = "<a href=\"file1.ica\"><img src=\"icon.jpg\"></a>";
}
else {
#port is down, check secondary
$connected = 0;
$checkport = IO::Socket::INET->new(
PeerAddr => "$secondary",
PeerPort => "$port",
Proto => 'tcp',
Timeout => '0') or $connected = 1;
if (!($connected)) {
#port is up, assign $link to the HTML for a link to the file for the next server.
$link = "<a href=\"file2.ica\"\"><img src=\"icon.jpg\"></a>";
}
else {
#port is down, first two servers are down - there has been a noticably long timout by now
#assign $link with HTML error message.
$link = "<P>Remote Desktop is unavailable.<BR>Please call the support line or<BR><a href=\"mailto://helpdesk\@mycompany.com\">e-mail support</a></P>";
}
}
close $checkport;
#merge variable with template
#the html template contains variable name enclosed by double angle brackets. i.e. <<$link>>
print "Content-type: text/html\n\n";
# Read HTML from template.
merge_file("\\wwwroot\\templates\\remote.html");
exit;
sub merge_file {
# Read HTML from template.
my $template_file = shift;
open(TEMPLATE, $template_file) or print "Error opening $template_file $!";
# temporarily disable "uninitialized value" warnings
$^W = 0;
while (