mirror of https://gerrit.hackerspace.pl/hscloud
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: I8b240ffe2cd3553a25ce33dbd3917c0aef64e804changes/03/3/1
parent
0607abae1d
commit
6eaaaf9bab
12
WORKSPACE
12
WORKSPACE
|
@ -599,3 +599,15 @@ go_repository(
|
||||||
commit = "c182affec369e30f25d3eb8cd8a478dee585ae7d",
|
commit = "c182affec369e30f25d3eb8cd8a478dee585ae7d",
|
||||||
importpath = "github.com/matttproud/golang_protobuf_extensions",
|
importpath = "github.com/matttproud/golang_protobuf_extensions",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
go_repository(
|
||||||
|
name = "com_github_golang_collections_go_datastructures",
|
||||||
|
commit = "59788d5eb2591d3497ffb8fafed2f16fe00e7775",
|
||||||
|
importpath = "github.com/golang-collections/go-datastructures",
|
||||||
|
)
|
||||||
|
|
||||||
|
go_repository(
|
||||||
|
name = "com_github_go_test_deep",
|
||||||
|
commit = "cf67d735e69b4a4d50cdf571a92b0144786080f7",
|
||||||
|
importpath = "github.com/go-test/deep",
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "go_default_library",
|
||||||
|
srcs = ["main.go"],
|
||||||
|
importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/irr",
|
||||||
|
visibility = ["//visibility:private"],
|
||||||
|
deps = [
|
||||||
|
"//bgpwtf/cccampix/irr/provider:go_default_library",
|
||||||
|
"//bgpwtf/cccampix/proto:go_default_library",
|
||||||
|
"//go/mirko:go_default_library",
|
||||||
|
"@com_github_golang_glog//:go_default_library",
|
||||||
|
"@org_golang_google_grpc//codes:go_default_library",
|
||||||
|
"@org_golang_google_grpc//status:go_default_library",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_binary(
|
||||||
|
name = "irr",
|
||||||
|
embed = [":go_default_library"],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
|
@ -0,0 +1,69 @@
|
||||||
|
irr
|
||||||
|
===
|
||||||
|
|
||||||
|
A proxy to access IRR RPSL data. It queries IANA for the responsible IRR, then the IRR directly.
|
||||||
|
|
||||||
|
It currently support ARIN and RIPE.
|
||||||
|
|
||||||
|
It currently supports querying for information about an aut-num, and returns the following attributes:
|
||||||
|
- remarks
|
||||||
|
- import
|
||||||
|
- export
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
|
||||||
|
$ grpcurl -format text -plaintext -d 'as: "26625"' 127.0.0.1:4220 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"
|
||||||
|
>
|
||||||
|
>
|
||||||
|
attributes: <
|
||||||
|
import: <
|
||||||
|
expressions: <
|
||||||
|
peering: "AS20459"
|
||||||
|
actions: "pref=10"
|
||||||
|
>
|
||||||
|
filter: "ANY"
|
||||||
|
>
|
||||||
|
>
|
||||||
|
attributes: <
|
||||||
|
export: <
|
||||||
|
expressions: <
|
||||||
|
peering: "AS6083"
|
||||||
|
>
|
||||||
|
filter: "AS26625"
|
||||||
|
>
|
||||||
|
>
|
||||||
|
attributes: <
|
||||||
|
export: <
|
||||||
|
expressions: <
|
||||||
|
peering: "AS12491"
|
||||||
|
>
|
||||||
|
filter: "AS26625"
|
||||||
|
>
|
||||||
|
>
|
||||||
|
attributes: <
|
||||||
|
export: <
|
||||||
|
expressions: <
|
||||||
|
peering: "AS20459"
|
||||||
|
>
|
||||||
|
filter: "AS26625"
|
||||||
|
>
|
||||||
|
>
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"code.hackerspace.pl/hscloud/bgpwtf/cccampix/irr/provider"
|
||||||
|
pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto"
|
||||||
|
"code.hackerspace.pl/hscloud/go/mirko"
|
||||||
|
)
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
iana *provider.IANA
|
||||||
|
providers map[provider.IRR]provider.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
mi := mirko.New()
|
||||||
|
|
||||||
|
if err := mi.Listen(); err != nil {
|
||||||
|
glog.Exitf("Listen failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &service{
|
||||||
|
iana: provider.NewIANA(),
|
||||||
|
providers: map[provider.IRR]provider.Provider{
|
||||||
|
provider.IRR_RIPE: provider.NewRIPE(),
|
||||||
|
provider.IRR_ARIN: provider.NewARIN(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
pb.RegisterIRRServer(mi.GRPC(), s)
|
||||||
|
|
||||||
|
if err := mi.Serve(); err != nil {
|
||||||
|
glog.Exitf("Serve failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-mi.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query returns parsed RPSL data for a given aut-num objects in any of the
|
||||||
|
// supported IRRs.
|
||||||
|
func (s *service) Query(ctx context.Context, req *pb.IRRQueryRequest) (*pb.IRRQueryResponse, error) {
|
||||||
|
if req.As == "" {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "as must be given")
|
||||||
|
}
|
||||||
|
|
||||||
|
req.As = strings.ToLower(req.As)
|
||||||
|
if strings.HasPrefix(req.As, "as") {
|
||||||
|
req.As = req.As[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
asn, err := strconv.ParseUint(req.As, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "as is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
irr, err := s.iana.Who(ctx, asn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Unavailable, "could not find AS block delegation from IANA: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
prov, ok := s.providers[irr]
|
||||||
|
if !ok {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "AS belongs to unhandled IRR %s", irr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := prov.Query(ctx, asn)
|
||||||
|
return res, err
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "go_default_library",
|
||||||
|
srcs = [
|
||||||
|
"arin.go",
|
||||||
|
"iana.go",
|
||||||
|
"provider.go",
|
||||||
|
"ripe.go",
|
||||||
|
"rpsl.go",
|
||||||
|
],
|
||||||
|
importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/irr/provider",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//bgpwtf/cccampix/irr/whois:go_default_library",
|
||||||
|
"//bgpwtf/cccampix/proto:go_default_library",
|
||||||
|
"@com_github_golang_collections_go_datastructures//augmentedtree:go_default_library",
|
||||||
|
"@com_github_golang_glog//:go_default_library",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "go_default_test",
|
||||||
|
srcs = ["rpsl_test.go"],
|
||||||
|
embed = [":go_default_library"],
|
||||||
|
deps = [
|
||||||
|
"//bgpwtf/cccampix/proto:go_default_library",
|
||||||
|
"@com_github_go_test_deep//:go_default_library",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,78 @@
|
||||||
|
package provider
|
||||||
|
|
||||||
|
// Support for the ARIN IRR.
|
||||||
|
// ARIN is special. We have to query them via whois. It's also not the same
|
||||||
|
// whois as you usually see. And also not many autnums (even big players like
|
||||||
|
// AS15169 and AS112) have IRR entries at all.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.hackerspace.pl/hscloud/bgpwtf/cccampix/irr/whois"
|
||||||
|
pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ARINWhois = "rr.arin.net:43"
|
||||||
|
|
||||||
|
type arin struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewARIN() Provider {
|
||||||
|
return &arin{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *arin) Query(ctx context.Context, asn uint64) (*pb.IRRQueryResponse, error) {
|
||||||
|
data, err := whois.Query(ctx, ARINWhois, fmt.Sprintf("AS%d", asn))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not contact ARIN IRR: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(data, "\n")
|
||||||
|
|
||||||
|
// Convert possibly 'continued' RPSL entries into single-line entries.
|
||||||
|
// eg.
|
||||||
|
// import: from AS6083
|
||||||
|
// action pref=10;
|
||||||
|
// accept ANY
|
||||||
|
// into
|
||||||
|
// 'import', 'from AS6083, action pref=10; accept ANY'
|
||||||
|
|
||||||
|
attrs := []rpslRawAttribute{}
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(strings.TrimSpace(line), "%") {
|
||||||
|
// Comment
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
// Empty line
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, " ") {
|
||||||
|
// Continuation
|
||||||
|
if len(attrs) < 1 {
|
||||||
|
return nil, fmt.Errorf("unparseable IRR, continuation with no previous atribute name: %q", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs[len(attrs)-1].value += " " + strings.TrimSpace(line)
|
||||||
|
} else {
|
||||||
|
parts := strings.SplitN(line, ":", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return nil, fmt.Errorf("unparseable IRR, line with no attribute key: %q", line)
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(parts[0])
|
||||||
|
value := strings.TrimSpace(parts[1])
|
||||||
|
attrs = append(attrs, rpslRawAttribute{
|
||||||
|
name: name,
|
||||||
|
value: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.IRRQueryResponse{
|
||||||
|
Source: pb.IRRQueryResponse_SOURCE_ARIN,
|
||||||
|
Attributes: parseAttributes(attrs),
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provider is the interface exposed to the service by IRR proxies.
|
||||||
|
type Provider interface {
|
||||||
|
// Return a proto response for a given AS.
|
||||||
|
Query(ctx context.Context, as uint64) (*pb.IRRQueryResponse, error)
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package provider
|
||||||
|
|
||||||
|
// Support for the RIPE IRR.
|
||||||
|
// We use the RIPE REST DB API.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ripeResponse struct {
|
||||||
|
Objects struct {
|
||||||
|
Object []ripeObject `json:"object"`
|
||||||
|
} `json:"objects"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ripeObject struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Attributes struct {
|
||||||
|
Attribute []ripeAttribute `json:"attribute"`
|
||||||
|
} `json:"attributes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ripeAttribute struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ripe struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRIPE() Provider {
|
||||||
|
return &ripe{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ripe) Query(ctx context.Context, as uint64) (*pb.IRRQueryResponse, error) {
|
||||||
|
req, err := http.NewRequest("GET", fmt.Sprintf("http://rest.db.ripe.net/ripe/aut-num/AS%d.json", as), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
client := http.DefaultClient
|
||||||
|
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not run GET to RIPE: %v", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
bytes, err := ioutil.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not read response from RIPE: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := ripeResponse{}
|
||||||
|
err = json.Unmarshal(bytes, &data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not decode response from RIPE: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data.Objects.Object) != 1 {
|
||||||
|
return nil, fmt.Errorf("could not retriev aut-num from RIPE")
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes := make([]rpslRawAttribute, len(data.Objects.Object[0].Attributes.Attribute))
|
||||||
|
|
||||||
|
for i, attr := range data.Objects.Object[0].Attributes.Attribute {
|
||||||
|
attributes[i].name = attr.Name
|
||||||
|
attributes[i].value = attr.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pb.IRRQueryResponse{
|
||||||
|
Source: pb.IRRQueryResponse_SOURCE_RIPE,
|
||||||
|
Attributes: parseAttributes(attributes),
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A raw, unparsed RPSL name/value pair.
|
||||||
|
type rpslRawAttribute struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseAttributes converts raw RPSL attributes into parsed, structured protos.
|
||||||
|
func parseAttributes(attrs []rpslRawAttribute) []*pb.IRRAttribute {
|
||||||
|
res := []*pb.IRRAttribute{}
|
||||||
|
|
||||||
|
for _, attr := range attrs {
|
||||||
|
switch attr.name {
|
||||||
|
case "remarks":
|
||||||
|
res = append(res, &pb.IRRAttribute{
|
||||||
|
Value: &pb.IRRAttribute_Remarks{attr.value},
|
||||||
|
})
|
||||||
|
case "import":
|
||||||
|
ie := parseImportExport(attr.value)
|
||||||
|
if ie != nil {
|
||||||
|
res = append(res, &pb.IRRAttribute{
|
||||||
|
Value: &pb.IRRAttribute_Import{ie},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case "export":
|
||||||
|
ie := parseImportExport(attr.value)
|
||||||
|
if ie != nil {
|
||||||
|
res = append(res, &pb.IRRAttribute{
|
||||||
|
Value: &pb.IRRAttribute_Export{ie},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseImportExport tries to parse the RPSL subset for import/export
|
||||||
|
// attributes.
|
||||||
|
// It's a hand-written single-pass parser which makes it not very good.
|
||||||
|
// See rpsl_test.go for examples.
|
||||||
|
func parseImportExport(expression string) *pb.IRRAttribute_ImportExport {
|
||||||
|
popToken := func(s string) (string, string) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
fields := strings.Fields(s)
|
||||||
|
if len(fields) < 2 {
|
||||||
|
return "", s
|
||||||
|
}
|
||||||
|
return fields[0], strings.TrimSpace(s[len(fields[0]):])
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &pb.IRRAttribute_ImportExport{}
|
||||||
|
|
||||||
|
expr := expression
|
||||||
|
var t string
|
||||||
|
t, expr = popToken(expr)
|
||||||
|
|
||||||
|
for {
|
||||||
|
switch t {
|
||||||
|
case "":
|
||||||
|
break
|
||||||
|
case "protocol":
|
||||||
|
t, expr = popToken(expr)
|
||||||
|
res.ProtocolFrom = t
|
||||||
|
t, expr = popToken(expr)
|
||||||
|
continue
|
||||||
|
case "into":
|
||||||
|
t, expr = popToken(expr)
|
||||||
|
res.ProtocolInto = t
|
||||||
|
t, expr = popToken(expr)
|
||||||
|
continue
|
||||||
|
case "from":
|
||||||
|
fallthrough
|
||||||
|
case "to":
|
||||||
|
t, expr = popToken(expr)
|
||||||
|
peering := t
|
||||||
|
actions := []string{}
|
||||||
|
router_us := ""
|
||||||
|
router_them := ""
|
||||||
|
|
||||||
|
t2, expr2 := popToken(expr)
|
||||||
|
for {
|
||||||
|
switch t2 {
|
||||||
|
case "at":
|
||||||
|
t2, expr2 = popToken(expr2)
|
||||||
|
router_us = t2
|
||||||
|
t2, expr2 = popToken(expr2)
|
||||||
|
continue
|
||||||
|
case "action":
|
||||||
|
parts := strings.SplitN(expr2, ";", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
// malformed action, no ';' found - bail
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
actions = append(actions, parts[0])
|
||||||
|
expr2 = strings.TrimSpace(parts[1])
|
||||||
|
t2, expr2 = popToken(expr2)
|
||||||
|
continue
|
||||||
|
case "":
|
||||||
|
fallthrough
|
||||||
|
case "accept":
|
||||||
|
fallthrough
|
||||||
|
case "announce":
|
||||||
|
t = t2
|
||||||
|
expr = expr2
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
if router_them == "" {
|
||||||
|
router_them = t2
|
||||||
|
t2, expr2 = popToken(expr2)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t = t2
|
||||||
|
expr = expr2
|
||||||
|
break
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
res.Expressions = append(res.Expressions, &pb.IRRAttribute_ImportExport_Expression{
|
||||||
|
Peering: peering,
|
||||||
|
RouterUs: router_us,
|
||||||
|
RouterThem: router_them,
|
||||||
|
Actions: actions,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
case "accept":
|
||||||
|
fallthrough
|
||||||
|
case "announce":
|
||||||
|
res.Filter = expr
|
||||||
|
t = ""
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto"
|
||||||
|
"github.com/go-test/deep"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseImportExport(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ut string
|
||||||
|
want *pb.IRRAttribute_ImportExport
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ut: `
|
||||||
|
from AS10674 209.251.128.177 at 216.155.103.20 action pref = 1;
|
||||||
|
accept ANY AND NOT AS-ACCELERATION-CUST
|
||||||
|
`,
|
||||||
|
want: &pb.IRRAttribute_ImportExport{
|
||||||
|
Expressions: []*pb.IRRAttribute_ImportExport_Expression{
|
||||||
|
{
|
||||||
|
Peering: "AS10674",
|
||||||
|
RouterUs: "216.155.103.20",
|
||||||
|
RouterThem: "209.251.128.177",
|
||||||
|
Actions: []string{"pref = 1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Filter: "ANY AND NOT AS-ACCELERATION-CUST",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ut: `
|
||||||
|
to AS201054 94.246.185.174 at 94.246.185.175 announce AS-BGPWTF
|
||||||
|
`,
|
||||||
|
want: &pb.IRRAttribute_ImportExport{
|
||||||
|
Expressions: []*pb.IRRAttribute_ImportExport_Expression{
|
||||||
|
{
|
||||||
|
Peering: "AS201054",
|
||||||
|
RouterUs: "94.246.185.175",
|
||||||
|
RouterThem: "94.246.185.174",
|
||||||
|
Actions: []string{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Filter: "AS-BGPWTF",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Invalid - unterminated action.
|
||||||
|
ut: `
|
||||||
|
to AS201054 94.246.185.174 at 94.246.185.175 action foo = bar
|
||||||
|
accept ANY AND NOT AS-ACCELERATION-CUST
|
||||||
|
`,
|
||||||
|
want: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
res := parseImportExport(test.ut)
|
||||||
|
if diff := deep.Equal(test.want, res); diff != nil {
|
||||||
|
t.Error(diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "go_default_library",
|
||||||
|
srcs = ["whois.go"],
|
||||||
|
importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/irr/whois",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
|
@ -0,0 +1,32 @@
|
||||||
|
package whois
|
||||||
|
|
||||||
|
// Support for the WHOIS protocol.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Query returns a semi-raw response from a WHOIS server for a given query.
|
||||||
|
// We only convert \r\n to \n, and then do no other transformation to the data.
|
||||||
|
func Query(ctx context.Context, server string, query string) (string, error) {
|
||||||
|
var d net.Dialer
|
||||||
|
conn, err := d.DialContext(ctx, "tcp", server)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("while dialing %q: %v", server, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
fmt.Fprintf(conn, "%s\r\n", query)
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(conn)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("while receiving data from %q: %v", server, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ReplaceAll(string(data), "\r", ""), nil
|
||||||
|
}
|
|
@ -39,3 +39,45 @@ service PeeringDBProxy {
|
||||||
// GetIXMembers returns information about membership of a given PeeringDB IX.
|
// GetIXMembers returns information about membership of a given PeeringDB IX.
|
||||||
rpc GetIXMembers(GetIXMembersRequest) returns (GetIXMembersResponse);
|
rpc GetIXMembers(GetIXMembersRequest) returns (GetIXMembersResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message IRRQueryRequest {
|
||||||
|
// AS to query for. This needs be the AS number of the AS, possibly
|
||||||
|
// prefixed with 'as'/'AS'.
|
||||||
|
string as = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IRRAttribute {
|
||||||
|
message ImportExport {
|
||||||
|
message Expression {
|
||||||
|
string peering = 1;
|
||||||
|
string router_us = 2;
|
||||||
|
string router_them = 3;
|
||||||
|
repeated string actions = 4;
|
||||||
|
}
|
||||||
|
string protocol_from = 1;
|
||||||
|
string protocol_into = 2;
|
||||||
|
repeated Expression expressions = 3;
|
||||||
|
string filter = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
oneof value {
|
||||||
|
string remarks = 1;
|
||||||
|
ImportExport import = 2;
|
||||||
|
ImportExport export = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message IRRQueryResponse {
|
||||||
|
enum Source {
|
||||||
|
SOURCE_INVALID = 0;
|
||||||
|
SOURCE_RIPE = 1;
|
||||||
|
SOURCE_ARIN = 2;
|
||||||
|
}
|
||||||
|
Source source = 1;
|
||||||
|
repeated IRRAttribute attributes = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
service IRR {
|
||||||
|
// Query returns parsed RPSL data from supported IRRs for a given aut-num.
|
||||||
|
rpc Query(IRRQueryRequest) returns (IRRQueryResponse);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue