blob: c6b355c3a2f0b574fce70825edd93d740a28b2e4 [file] [log] [blame]
package main
import (
"context"
"encoding/hex"
"fmt"
"strings"
"sync"
"time"
pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto"
"code.hackerspace.pl/hscloud/bgpwtf/cccampix/verifier/model"
"code.hackerspace.pl/hscloud/go/pki"
"github.com/golang/glog"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
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, lastFailed bool) time.Time {
if lastFailed {
return now.Add(1 * time.Minute)
}
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, "irr", 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
}