#!/usr/bin/perl -w

# This file is part of secnet.
# See README for full list of copyright holders.
#
# secnet is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
# 
# secnet is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# version 3 along with secnet; if not, see
# https://www.gnu.org/licenses/gpl.html.

use strict;
use IO::Handle;

my $us = $0;
$us =~ s{.*/}{};

open DEBUG, ">/dev/null" or die $!;

if (@ARGV && $ARGV[0] eq '-D') {
    shift @ARGV;
    open DEBUG, ">&STDERR" or die $!;
}

die "$us: no arguments permitted\n" if @ARGV;

our ($monh,$monchild);

our %reported;
#  no entry: not reported, does not exist
#  /ry+/: reported, entry exists
# during processing only:
#  /r/: reported, may not still exist
#  /y+/: not reported, entry exists

sub killmonitor () {
    return unless $monchild;
    kill 9, $monchild
	or warn "$us: cannot kill monitor child [$monchild]: $!\n";
    $monchild=undef;
    close $monh;
}

END { killmonitor(); }

my $restart;

for (;;) {
    my $o;
    eval {
	if (!$monh) {
	    killmonitor();
	    $monh = new IO::File;
	    $monchild = open $monh, "-|", qw(ip -o monitor addr)
		or die "spawn monitor: $!\n";
	    sleep(1) if $restart++;
	} else {
	    my $discard;
	    my $got = sysread $monh, $discard, 4096;
	    die "read monitor: $!\n" unless defined $got;
	    die "monitor failed\n" unless $got;
	}
	$_='r' foreach values %reported;
	print DEBUG "#########################################\n";
	foreach my $ip (qw(4 6)) {
	    print DEBUG "###### $ip:\n";
	    my $addrh = new IO::File;
	    open $addrh, "-|", qw(ip -o), "-$ip", qw(addr show)
		or die "spawn addr $ip show: $!\n";
	    my $afstr = $ip==4 ? 'inet' : $ip==6 ? 'inet6' : die;
	    while (<$addrh>) {
		print DEBUG "#$_";
		if (m{^\d+\:\s*(\S+)\s+$afstr\s+([0-9a-z.:]+)(?:/\d+)?\s}) {
		    my $rhs=$'; #';
		    my $outline = "$ip $1 $2";
		    # "ip -o addr show" has a ridiculous output format.  In
		    # particular, it mixes output keywords which introduce
		    # values with ones which don't, and there seems to be
		    # no way to tell without knowing all the possible
		    # keywords.  We hope that before the \ there is nothing
		    # which contains arbitrary text (specifically, which
		    # might be `tentative' other than to specify IPv6
		    # tentativeness).  We have to do this for IPv6 only
		    # because in the IPv4 output, the interface name
		    # appears here!
		    next if $ip==6 && $rhs=~m{[^\\]* tentative\s};
		    $reported{$outline} .= "y";
		} else {
		    chomp;
		    warn "unexpected output from addr $ip show: $_\n";
		}
	    }
	    my $r = close $addrh;
	    die "addr $ip show failed $!\n" unless $r;
	    $o = '';
	}
	foreach my $k (keys %reported) {
	    local $_ = $reported{$k};
	    if (m/^r$/) {
		$o .= "-$k\n";
		delete $reported{$k};
	    } elsif (m/^y/) {
		$o .= "+$k\n";
	    }
	}
    };
    if ($@) {
	print STDERR "$us: $@";
	sleep 5;
	next;
    }
    print $o or die $!;
    STDOUT->flush or die $!;
}
