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
+}