bgpwtf/cccampix: add IRR daemon

We add a small IRR service for getting a parsed RPSL from IRRs. For now,
we only support RIPE and ARIN, and only the following attributes:
 - remarks
 - import
 - export

Since RPSL/RFC2622 is fucking insane, there is no guarantee that the
parser, especially the import/export parser, is correct. But it should
be good enough for our use. We even throw in some tests for good
measure.

    $ grpcurl -format text -plaintext -d 'as: "26625"' 127.0.0.1:4200 ix.IRR.Query
    source: SOURCE_ARIN
    attributes: <
      import: <
        expressions: <
          peering: "AS6083"
          actions: "pref=10"
        >
        filter: "ANY"
      >
    >
    attributes: <
      import: <
        expressions: <
          peering: "AS12491"
          actions: "pref=10"
        >
        filter: "ANY"
      >
    >

Change-Id: I8b240ffe2cd3553a25ce33dbd3917c0aef64e804
diff --git a/bgpwtf/cccampix/irr/provider/iana.go b/bgpwtf/cccampix/irr/provider/iana.go
new file mode 100644
index 0000000..8f085d2
--- /dev/null
+++ b/bgpwtf/cccampix/irr/provider/iana.go
@@ -0,0 +1,163 @@
+package provider
+
+// IANA is not a full provider - we use it to determine the relevant IRR for a given aut-num.
+
+import (
+	"context"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/golang-collections/go-datastructures/augmentedtree"
+	"github.com/golang/glog"
+
+	"code.hackerspace.pl/hscloud/bgpwtf/cccampix/irr/whois"
+)
+
+// Possible IRRs that AS blocks can be delegated to.
+type IRR int
+
+const (
+	IRR_UNKNOWN IRR = iota
+	IRR_ARIN
+	IRR_RIPE
+)
+
+func (i IRR) String() string {
+	return []string{
+		"UNKNOWN",
+		"ARIN",
+		"RIPE",
+	}[i]
+}
+
+// IANA access service.
+type IANA struct {
+	// Interval tree of AS block delegation to IRRs. We use this to not
+	// keep hitting the IANA whois unnecessariy.
+	cache augmentedtree.Tree
+	// The tree library needs intervals to have a unique ID. We use a counter
+	// for this effect.
+	id uint64
+}
+
+func NewIANA() *IANA {
+	return &IANA{
+		cache: augmentedtree.New(1),
+	}
+}
+
+func (i *IANA) nextID() uint64 {
+	res := i.id
+	i.id += 1
+	return res
+}
+
+// delegation implements the Interval interface for the interval tree.
+type delegation struct {
+	to   IRR
+	id   uint64
+	low  int64
+	high int64
+}
+
+func (d *delegation) LowAtDimension(n uint64) int64 {
+	if n != 1 {
+		panic(fmt.Sprintf("dimension too high (%d)", n))
+	}
+	return d.low
+}
+
+func (d *delegation) HighAtDimension(n uint64) int64 {
+	if n != 1 {
+		panic(fmt.Sprintf("dimension too high (%d)", n))
+	}
+	return d.high
+}
+
+func (d *delegation) OverlapsAtDimension(i augmentedtree.Interval, n uint64) bool {
+	if n != 1 {
+		return false
+	}
+
+	if i.LowAtDimension(1) <= d.HighAtDimension(1) && i.HighAtDimension(1) >= d.LowAtDimension(1) {
+		return true
+	}
+
+	return false
+}
+
+func (d *delegation) ID() uint64 {
+	return d.id
+}
+
+// Who returns the responsible IRR (or UNKNOWN) for a given AS.
+func (i *IANA) Who(ctx context.Context, asn uint64) (IRR, error) {
+	q := &delegation{
+		id:   i.nextID(),
+		low:  int64(asn),
+		high: int64(asn),
+	}
+
+	res := i.cache.Query(q)
+	if len(res) > 0 {
+		return res[0].(*delegation).to, nil
+	}
+
+	// No cache entry, query whois.
+	glog.Infof("Cache miss for AS%d", asn)
+	data, err := whois.Query(ctx, "whois.iana.org:43", fmt.Sprintf("AS%d", asn))
+	if err != nil {
+		return IRR_UNKNOWN, err
+	}
+
+	// We not only find the responsible IRR, but also the delegation bounds
+	// to feed the interval tree.
+	lines := strings.Split(data, "\n")
+	var lower int64
+	var upper int64
+	var irr IRR
+	for _, line := range lines {
+		parts := strings.Fields(line)
+		if len(parts) == 2 && parts[0] == "as-block:" {
+			bounds := strings.Split(parts[1], "-")
+			if len(bounds) != 2 {
+				return IRR_UNKNOWN, fmt.Errorf("unparseable as-block: %v", parts[1])
+			}
+			lower, err = strconv.ParseInt(bounds[0], 10, 64)
+			if err != nil {
+				return IRR_UNKNOWN, fmt.Errorf("unparseable as-block: %v", parts[1])
+			}
+			upper, err = strconv.ParseInt(bounds[1], 10, 64)
+			if err != nil {
+				return IRR_UNKNOWN, fmt.Errorf("unparseable as-block: %v", parts[1])
+			}
+		}
+		if len(parts) > 2 && parts[0] == "organisation:" {
+			switch {
+			case strings.Contains(line, "RIPE NCC"):
+				irr = IRR_RIPE
+			case strings.Contains(line, "ARIN"):
+				irr = IRR_ARIN
+			}
+		}
+	}
+
+	if lower == 0 || upper == 0 {
+		return IRR_UNKNOWN, nil
+	}
+
+	glog.Infof("Saving %d-%d AS blocks delegation (%s) to cache", lower, upper, irr.String())
+
+	// Insert into tree.
+	q = &delegation{
+		id:   i.nextID(),
+		to:   irr,
+		low:  lower,
+		high: upper + 1,
+	}
+
+	i.cache.Add(q)
+
+	return irr, nil
+}