bgpwtf/cccampix: draw the rest of the fucking owl

Change-Id: I49fd5906e69512e8f2d414f406edc0179522f225
diff --git a/bgpwtf/cccampix/verifier/processor_irr.go b/bgpwtf/cccampix/verifier/processor_irr.go
new file mode 100644
index 0000000..49c4eb1
--- /dev/null
+++ b/bgpwtf/cccampix/verifier/processor_irr.go
@@ -0,0 +1,247 @@
+package main
+
+import (
+	"context"
+	"encoding/hex"
+	"fmt"
+	"strings"
+	"sync"
+	"time"
+
+	"code.hackerspace.pl/hscloud/go/pki"
+	"github.com/golang/glog"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+
+	pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto"
+	"code.hackerspace.pl/hscloud/bgpwtf/cccampix/verifier/model"
+)
+
+const (
+	RS_ASN   = "AS208521"
+	RS_ASSET = "AS-CCCAMP19-IX"
+)
+
+type irr struct {
+	irrc pb.IRRClient
+}
+
+func newIRR(addr string) (processor, error) {
+	conn, err := grpc.Dial(addr, pki.WithClientHSPKI())
+	if err != nil {
+		return nil, fmt.Errorf("could not connect to irr service: %v", err)
+	}
+
+	return &irr{
+		irrc: pb.NewIRRClient(conn),
+	}, nil
+}
+
+func (i *irr) Name() string {
+	return "IRR"
+}
+
+func (i *irr) NextRun(now time.Time) time.Time {
+	return now.Add(5 * time.Minute)
+}
+
+func (i *irr) RunAll(ctx context.Context, m model.Model) error {
+	peers, err := m.GetCheckablePeers(ctx)
+	if err != nil {
+		return fmt.Errorf("could not retrieve peers: %v", err)
+	}
+
+	results := make(chan *model.PeerCheckResult)
+	pcr := []*model.PeerCheckResult{}
+	pcrDone := make(chan struct{})
+
+	pgpKeys := make(chan *model.PeerPGPKey)
+	pk := []*model.PeerPGPKey{}
+	pkDone := make(chan struct{})
+
+	go func() {
+		for res := range results {
+			pcr = append(pcr, res)
+		}
+		pcrDone <- struct{}{}
+	}()
+	go func() {
+		for res := range pgpKeys {
+			pk = append(pk, res)
+		}
+		pkDone <- struct{}{}
+	}()
+
+	fail := func(p *model.Peer, hard bool, f string, args ...interface{}) {
+		status := model.PeerCheckStatus_SoftFailed
+		if hard {
+			status = model.PeerCheckStatus_Failed
+		}
+		results <- &model.PeerCheckResult{
+			PeerASN:   p.ASN,
+			CheckName: "irr",
+			Time:      time.Now(),
+			Status:    status,
+			Message:   fmt.Sprintf(f, args...),
+		}
+
+	}
+
+	var wg sync.WaitGroup
+	wg.Add(len(peers))
+
+	sem := make(chan struct{}, 10)
+
+	for _, peer := range peers {
+		go func(p *model.Peer) {
+			sem <- struct{}{}
+			defer func() {
+				<-sem
+				wg.Done()
+			}()
+
+			req := &pb.IRRQueryRequest{
+				As: fmt.Sprintf("%d", p.ASN),
+			}
+			res, err := i.irrc.Query(ctx, req)
+			if err != nil {
+				s, ok := status.FromError(err)
+				switch {
+				case ok && s.Code() == codes.NotFound:
+					fail(p, true, "ASN %d not found in IRR", p.ASN)
+				case ok && s.Code() == codes.Unimplemented:
+					fail(p, true, "ASN %d belongs to an unknown IRR/RIR", p.ASN)
+				case ok && s.Code() == codes.Unavailable:
+					fail(p, false, "could not contact IRR")
+				default:
+					glog.Errorf("IRR.Query(%d): %v", p.ASN, err)
+					fail(p, false, "unhandled IRR error")
+				}
+				return
+			}
+
+			importOkay := false
+			exportOkay := false
+			pgpKey := ""
+
+			for _, attr := range res.Attributes {
+				switch value := attr.Value.(type) {
+				case *pb.IRRAttribute_Remarks:
+					if ok, key := i.checkRemarks(value.Remarks); ok {
+						pgpKey = key
+					}
+				case *pb.IRRAttribute_Import:
+					if i.checkImport(value.Import) {
+						importOkay = true
+					}
+				case *pb.IRRAttribute_Export:
+					if i.checkExport(value.Export, p.ASN) {
+						exportOkay = true
+					}
+				}
+			}
+
+			switch {
+			case !importOkay:
+				fail(p, true, "no `import: from %s accept %s` entry", RS_ASN, RS_ASSET)
+				return
+			case !exportOkay:
+				fail(p, true, "no `export: to %s announce AS%d` entry", RS_ASN, p.ASN)
+				return
+			case pgpKey == "":
+				fail(p, true, "no `remarks: CCCAMP19-IX PGP: <...>` entry")
+				return
+			}
+
+			pgpKeys <- &model.PeerPGPKey{
+				PeerASN:     p.ASN,
+				Fingerprint: pgpKey,
+			}
+
+			results <- &model.PeerCheckResult{
+				PeerASN:   p.ASN,
+				CheckName: "irr",
+				Time:      time.Now(),
+				Status:    model.PeerCheckStatus_Okay,
+				Message:   "",
+			}
+		}(peer)
+	}
+
+	wg.Wait()
+	close(results)
+	close(pgpKeys)
+	<-pcrDone
+	<-pkDone
+
+	err = m.SubmitPeerCheckResults(ctx, pcr)
+	if err != nil {
+		return err
+	}
+
+	for _, k := range pk {
+		err = m.UpdatePGPKey(ctx, k)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (i *irr) checkRemarks(remarks string) (bool, string) {
+	label := "cccamp19-ix pgp:"
+	remarks = strings.TrimSpace(strings.ToLower(remarks))
+	if !strings.HasPrefix(remarks, label) {
+		return false, ""
+	}
+
+	data := strings.TrimSpace(strings.TrimPrefix(remarks, label))
+	data = strings.ReplaceAll(data, " ", "")
+	data = strings.ReplaceAll(data, "\t", "")
+
+	if len(data) != 40 {
+		return false, ""
+	}
+
+	if _, err := hex.DecodeString(data); err != nil {
+		return false, ""
+	}
+
+	return true, data
+}
+
+func (i *irr) checkImport(imp *pb.IRRAttribute_ImportExport) bool {
+	if imp.ProtocolFrom != "" && strings.ToLower(imp.ProtocolFrom) != "bgp" {
+		return false
+	}
+	if strings.ToUpper(imp.Filter) != RS_ASSET {
+		return false
+	}
+
+	for _, expression := range imp.Expressions {
+		if strings.ToUpper(expression.Peering) == RS_ASN {
+			return true
+		}
+	}
+
+	return false
+}
+
+func (i *irr) checkExport(exp *pb.IRRAttribute_ImportExport, asn int64) bool {
+	if exp.ProtocolInto != "" && strings.ToLower(exp.ProtocolInto) != "bgp" {
+		return false
+	}
+	if strings.ToUpper(exp.Filter) != fmt.Sprintf("AS%d", asn) {
+		return false
+	}
+
+	for _, expression := range exp.Expressions {
+		if strings.ToUpper(expression.Peering) == RS_ASN {
+			return true
+		}
+	}
+
+	return false
+}