Last active
April 9, 2023 18:04
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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