#!/usr/bin/env ruby

#############################################################################
## This accepts a raw list of IPv6 destination prefixes and filters out
## various classes of prefixes that should not be probed.
##
## $Id: filter-ipv6-prefixes,v 1.5 2008/12/12 02:10:43 youngh Exp $
#############################################################################

if $0 =~ /dev$/
  $user = `whoami`.chomp!
  $: << "/Users/#{$user}/Work/archipelago/util/lib"
else
  require 'rubygems'
end

require 'ostruct'
require 'optparse'

require 'arkutil/ip6utils'

Thread.abort_on_exception = true

$donotprobe = "/etc/ark/badips6.prefixes.aggr"

$options = OpenStruct.new

opts = OptionParser.new

opts.on("-v", "--[no-]verbose", TrueClass,  "show detailed progress") do |v|
  $options.verbose = v
end

begin
  ARGV.replace opts.parse(*ARGV)
rescue OptionParser::ParseError
  $stderr.puts "ERROR: " + $!
  $stderr.puts opts
  exit 1
end


#============================================================================

# This class is needed because IPAddr in the standard library falls short
# in two ways; namely,
#
#   (1) IPAddr#include? doesn't take the prefix length into account
#
#       >> x = IPAddr.new "2001:200::/32"
#       >> z = IPAddr.new "2001:200::/28"
#       >> x.include? z
#       => true
#
#   (2) There is no way to retrieve the prefix length from an IPAddr.

class IPPrefix

  attr_reader :address, :length

  def initialize(prefix)
    if prefix =~ /^([0-9a-zA-Z:]+)\/(\d+)$/
      @prefix = prefix
      @ipaddr = IPAddr.new prefix
      @address = $1
      @length = $2.to_i
    else
      raise ArgumentError, "malformed IPv6 prefix"
    end
  end

  # XXX might be nice to catch & report:
  #   .../ipaddr.rb:467:in `initialize': invalid address (ArgumentError)
  def include?(prefix)
    if prefix.kind_of? IPPrefix
      return @ipaddr.include?(prefix.to_ipaddr) && @length <= prefix.length
    elsif prefix =~ /^[0-9a-zA-Z:]+\/(\d+)$/
      length = $1.to_i
      return @ipaddr.include?(IPAddr.new(prefix)) && @length <= length
    else
      raise ArgumentError, "malformed IPv6 prefix"
    end
  end

  def to_ipaddr
    @ipaddr
  end

  def to_s
    @prefix
  end

  def self.test
    p1 = IPPrefix.new "2001:200::/32"
    p2 = IPPrefix.new "2001:200::/28"
    p3 = IPPrefix.new "2002:200::/48"
    p p1.include?(p1)
    p p1.include?(p2)
    p p2.include?(p1)
    p p1.include?(p3)
    p p2.include?(p3)

    p p1.include?("2001:200::/32")
    p p1.include?("2001:200::/28")
    p p2.include?("2001:200::/32")
  end

end

#============================================================================

def is_valid_ipv6(prefix)
  begin
    IPAddr.new prefix
  rescue ArgumentError
    false
  else
    true
  end
end


# There are some malformed IPv6 prefixes in
# http://bgp.potaroo.net/v6/as6447/bgptable.txt.
# Specifically, there appears to be a bug in address compression.
#
#       invalid prefixes            correct prefixes
#       ----------------            ----------------
#       2001:504::1::/64            2001:504:0:1::/64
#       2001:7fa::2::/64            2001:7fa:0:2::/64
#       2620::30::/48               2620:0:30::/48
#       2620::ccf:8000::/56         2620:0:ccf:8000::/56
#       (lots more)
#
# >> "2620::30::".split("::")
# => ["2620", "30"]
# >> "::30::".split("::")
# => ["", "30"]
# >> "::".split("::")
# => []
def fix_prefix_compression(prefix)
  if prefix =~ /^([0-9a-zA-Z:]+)\/(\d+)$/
    address = $1
    length = $2.to_i
    if address.split("::").length == 2 && address =~ /::$/ && address !~ /^::/
      address.sub! /::/, ":0:"
      binary = IPAddr.new(address).hton.unpack("B128").first
      min_length = binary.rindex("1") + 1
      if min_length <= length
        $stderr.printf "fixing %s => %s/%d %d %s\n", prefix, address, length, min_length, binary
        return address + "/" + length.to_s
      end
    end
  end
  nil
end


#============================================================================
#============================================================================

# See http://www.iana.org/assignments/ipv6-address-space
# See http://www.iana.org/assignments/ipv6-unicast-address-assignments
#
# * IANA Special Purpose Address Block
#
#     See: http://www.iana.org/assignments/iana-ipv6-special-registry
#     See: RFC 4773 - Administration of the IANA Special Purpose IPv6
#                     Address Block
#
#     Currently includes Teredo (2001:0000::/32), benchmarking (2001:0200::/48),
#     and Orchid (2001:0010::/28).
#
# RFC 5156 - Special-Use IPv6 Addresses

global_unicast_prefix = IPPrefix.new "2000::/3"
prefix_exclusions = [
  IPPrefix.new("2001:0000::/23"),   # IANA Special Purpose Address Block
  IPPrefix.new("2002::/16"),        # 6to4
  IPPrefix.new("2001:db8::/32"),    # documentation
]

# IPPrefix.new "2001:0506::/31", # ARIN micro-alloc: internal infrastructures
# IPPrefix.new "2001:0504::/31", # ARIN micro-alloc: exchange points
# IPPrefix.new "2001:0500::/30", # ARIN micro-alloc: critical infrastructures

# load other prefix exclusions from the do-not-probe file
if File.exist? $donotprobe
  File.readlines($donotprobe, chomp: true).each do |line|
    prefix_exclusions << IPPrefix.new(line)
  end
end

MAX_PREFIX_LEN = 48  # block all prefixes longer than this length

ARGF.each do |prefix|
  next if prefix =~ /^\s*$/
  next if prefix =~ /^\s*\#/
  prefix.chomp!

  if prefix =~ /^[0-9a-zA-Z:]+\/(\d+)$/
    unless is_valid_ipv6 prefix
      input_prefix = prefix
      prefix = fix_prefix_compression prefix
      unless prefix
        $stderr.puts "ERROR: invalid IPv6 prefix #{input_prefix}"
        next
      end
    end

    length = $1.to_i
    if length > MAX_PREFIX_LEN
      $stderr.puts "rejecting #{prefix}: prefix length > #{MAX_PREFIX_LEN}"
      next
    end

    unless global_unicast_prefix.include? prefix
      $stderr.puts "rejecting #{prefix}: not global unicast"
      next
    end

    is_blocked = false
    prefix_exclusions.each do |blocked_prefix|
      if blocked_prefix.include? prefix
        $stderr.puts "rejecting #{prefix}: blocked by #{blocked_prefix}"
        is_blocked = true
        break
      end
    end
    next if is_blocked

    puts prefix
  else
    abort "ERROR: line #{$.} malformed: #{prefix}"
  end
end
