Skip to content

Instantly share code, notes, and snippets.

@olicooper
Last active April 9, 2023 18:04
Show Gist options
  • Save olicooper/f9c825dddd60cdb1a57037457cb369e2 to your computer and use it in GitHub Desktop.
Save olicooper/f9c825dddd60cdb1a57037457cb369e2 to your computer and use it in GitHub Desktop.
Cloudflare nginx Real-IP header generation written in pearl. Only updates if etag has changed.
#!/usr/bin/perl
###
### Writes a list of 'set_real_ip_from' directives based on current CloudFlare proxy IP addresses.
### This is used to ensure the real client IP is known by NGINX and correctly logged.
### Script usage: /etc/nginx/nginx_cloudflare_ip_headers.pl --silent --update-hook '/usr/sbin/nginx -s reload' '/etc/nginx/snippets/cloudflare_real_ip_header.conf'
### NOTE: Additional perl packages are required to run this script, use 'apt install libwww-perl libjson-perl libreadonly-perl perl-doc'
### Don't forget to add execute permissions too! chmod ug+x /etc/nginx/nginx_cloudflare_ip_headers.pl
use strict;
use warnings;
use LWP::UserAgent;
use HTTP::Request::Common;
use JSON qw(decode_json);
use Getopt::Long qw(GetOptions);
use Pod::Usage qw(pod2usage);
use File::Basename qw(basename);
use Readonly;
use version;
use Cwd qw(abs_path);
use Data::Dumper;
Readonly my $VERSION => qv('0.0.1');
Readonly my $EXE => basename($0);
my $update_hook_timeout;
##
## Process command arguments
##
GetOptions(
'update-hook=s' => \my $update_hook,
# 'updatehooktimeout=i' => \$update_hook_timeout,
'silent' => \my $silent,
'version' => \my $version,
'usage' => \my $usage,
'help|?' => \my $help,
) or pod2usage(-verbose => 0);
pod2usage(-verbose => 0) if $usage;
pod2usage(-verbose => 1) if $help;
if ($version) {
print "$EXE version $VERSION\n";
exit 0;
}
## Check for output file argument
pod2usage("No filename specified!\n") unless @ARGV;
my $out_file = $ARGV[0];
pod2usage("'$out_file' is a directory!\n") if -d $out_file;
## Check update_hook argument
if (defined($update_hook) && length($update_hook) > 1) {
die("--update-hook '$update_hook' is a directory!\n") if -d $update_hook;
## if hook is file
if (-e $update_hook) {
die("--update-hook '$update_hook' is not executable!\n") unless -x $update_hook;
$update_hook = Cwd::abs_path($update_hook);
die("Don't specify this script as the updatehook!\n")
if $update_hook eq Cwd::abs_path($0);
}
}
$update_hook_timeout = 10 unless defined($update_hook_timeout) && $update_hook_timeout > 0;
##
## Main routine
##
my $errstr_json = '[Error] Failed to decode JSON data';
my $request_url = 'https://api.cloudflare.com/client/v4/ips';
print "[Info] Updating cloudflare IP list\n" unless $silent;
my $ua = LWP::UserAgent->new;
my $req = GET $request_url;
my $res = $ua->request($req);
## Check for OK response and valid json data
if ($res->is_success && $res->content_type eq 'application/json' && length($res->content) > 100 && length($res->content) < 1000) {
my $json_data = decode_json($res->content) or die "$errstr_json: $!\n";
if ($json_data->{'success'}) {
my @ipv4Addrs = @{ $json_data->{'result'}{'ipv4_cidrs'} } or die "$errstr_json\n";
my @ipv6Addrs = @{ $json_data->{'result'}{'ipv6_cidrs'} } or die "$errstr_json\n";
my $etag = $json_data->{'result'}{'etag'} or die "$errstr_json\n";
if (open(OUT_FILE,'<',$out_file)) {
my $etagMatch = 0;
my $fCount = 0;
while(<OUT_FILE>) {
$fCount++;
if (!$etagMatch && $_ =~ m/^# etag: ([0-9a-zA-Z]{32})$/) {
if ($1 eq $etag) {
$etagMatch = 1;
}
}
}
print "[Info] Read $fCount lines from file\n" unless $silent;
close(OUT_FILE);
if ($etagMatch) {
print "[Info] File not updated, IP's have not changed\n" unless $silent;
exit 0;
}
}
## Now open the file for writing
open(OUT_FILE,'>',$out_file) or die $!;
my $datestring = gmtime();
print OUT_FILE "# WARNING: Autogenerated file, any modifications will be overwritten!\n";
print OUT_FILE "# Generated at $datestring from '$request_url'\n";
print OUT_FILE "# etag: $etag\n\n";
print OUT_FILE "# IPv4\n";
for my $ipv4 (@ipv4Addrs) {
print OUT_FILE "set_real_ip_from $ipv4;\n";
}
print OUT_FILE "\n# IPv6\n";
for my $ipv6 (@ipv6Addrs) {
print OUT_FILE "set_real_ip_from $ipv6;\n";
}
print OUT_FILE "\nreal_ip_header CF-Connecting-IP;\n\n";
close(OUT_FILE);
print "[Info] Updated IPs in file: '$out_file'\n" unless $silent;
print "[Info] Running update hook: '$update_hook'\n" unless $silent;
## Run update hook if defined
if (defined($update_hook)) {
print &runcmd($update_hook, $update_hook_timeout);
}
} else {
die "$errstr_json\n";
}
} else {
die "[Error] Failed to download latest data\n\t HTTP Status: $res->status_line\n";
exit 1;
}
sub runcmd {
my ($cmd, $timeout) = @_;
my $childpid;
my $output;
eval {
local $SIG{ALRM} = sub { die "alarm\n" };
alarm $timeout;
$childpid = open(my $fh, "exec $cmd 2>&1 |");
return "Failed to run '$cmd'\n" if (!defined($childpid));
$output = <$fh>;
close($fh);
alarm 0;
};
if ($@) {
if ($@ eq "alarm\n" && defined($childpid)) {
my $killproc = `killall $childpid 2>/dev/null`;
warn "[Warn] Timeout occured for '$cmd' (PID: $childpid)\n" unless $silent;
} else {
warn "[Warn] An error occurred in command '$cmd'\n" unless $silent;
}
}
if (!defined($output)) {
$output = '';
}
return $output;
}
1;
__END__
=head1 NAME
gen_nginx_cf_ip_headers.pl - Writes a list of 'set_real_ip_from' directive based on current CloudFlare proxy IP addresses.
=head1 SYNOPSIS
./gen_nginx_cf_ip_headers.pl [OUTPUT_FILE]
=head1 OPTIONS
=over 5
=item --silent
Silence non-error output for use in scripts or scheduled tasks.
=item --update-hook
The bash script to run after the output file has been updated.
This will not be called if there were no changes to the output file.
Note: There is a limit of 10 seconds for the script to run.
=item --version
Print the version information
=item --usage
Print the usage line of this summary
=item --help
Print this summary.
=back
=head1 DESCRIPTION
Writes a list of 'set_real_ip_from' directives based on current CloudFlare proxy IP addresses.
This is used to ensure the real client IP is known by NGINX and correctly logged.
NOTE: additional perl packages are required to run, use 'apt install libwww-perl libjson-perl libreadonly-perl perl-doc'
=head1 AUTHOR
Written by Oliver Cooper
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment