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