From e653e6a620426aad4defeb2224e7d17dafd010c0 Mon Sep 17 00:00:00 2001 From: Sergiusz Bazanski Date: Sat, 20 Jul 2019 16:36:00 +0200 Subject: [PATCH] bgpwtf/cccampix/peeringdb: init First pass at a proxy to expose PeeringDB data. Change-Id: I844973755473b3abc5d334586744004b86d1c3a3 --- bgpwtf/cccampix/peeringdb/BUILD.bazel | 22 +++++ bgpwtf/cccampix/peeringdb/README.md | 41 +++++++++ bgpwtf/cccampix/peeringdb/main.go | 97 ++++++++++++++++++++ bgpwtf/cccampix/peeringdb/schema/BUILD.bazel | 11 +++ bgpwtf/cccampix/peeringdb/schema/schema.go | 34 +++++++ bgpwtf/cccampix/peeringdb/schema/urls.go | 61 ++++++++++++ bgpwtf/cccampix/proto/BUILD.bazel | 23 +++++ bgpwtf/cccampix/proto/ix.proto | 26 ++++++ 8 files changed, 315 insertions(+) create mode 100644 bgpwtf/cccampix/peeringdb/BUILD.bazel create mode 100644 bgpwtf/cccampix/peeringdb/README.md create mode 100644 bgpwtf/cccampix/peeringdb/main.go create mode 100644 bgpwtf/cccampix/peeringdb/schema/BUILD.bazel create mode 100644 bgpwtf/cccampix/peeringdb/schema/schema.go create mode 100644 bgpwtf/cccampix/peeringdb/schema/urls.go create mode 100644 bgpwtf/cccampix/proto/BUILD.bazel create mode 100644 bgpwtf/cccampix/proto/ix.proto diff --git a/bgpwtf/cccampix/peeringdb/BUILD.bazel b/bgpwtf/cccampix/peeringdb/BUILD.bazel new file mode 100644 index 00000000..22c90e3f --- /dev/null +++ b/bgpwtf/cccampix/peeringdb/BUILD.bazel @@ -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/peeringdb", + visibility = ["//visibility:private"], + deps = [ + "//bgpwtf/cccampix/peeringdb/schema: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 = "peeringdb", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) diff --git a/bgpwtf/cccampix/peeringdb/README.md b/bgpwtf/cccampix/peeringdb/README.md new file mode 100644 index 00000000..502760e8 --- /dev/null +++ b/bgpwtf/cccampix/peeringdb/README.md @@ -0,0 +1,41 @@ +PeeringDBProxy +============== + +Exposes PeeringDB data as gRPC. + +API defined in [ix.proto](../proto/ix.proto). + +Usage +----- + + $ bazel run //bgpwtf/cccampix/peeringdb:peeringdb -- -hspki_disable + $ grpcurl -plaintext -d '{"id": 2325}' 127.0.0.1:4200 ix.PeeringDBProxy.GetIXMembers + { + "members": [ + { + "asn": 206924, + "ipv4": "185.230.223.195", + "name": "BENJOJONET" + }, + { + "asn": 207080, + "ipv4": "185.230.223.194", + "ipv6": "fe80::8651:4050:1715:bc4f", + "name": "Basil Fillan" + }, + { + "asn": 39192, + "ipv4": "185.230.223.198", + "ipv6": "fe80::3:9192:1", + "name": "JackNet" + }, + { + "asn": 205271, + "ipv4": "185.230.223.199", + "ipv6": "fe80::20:5271:1", + "name": "Harry Reeder" + } + ] + } + + diff --git a/bgpwtf/cccampix/peeringdb/main.go b/bgpwtf/cccampix/peeringdb/main.go new file mode 100644 index 00000000..fa116516 --- /dev/null +++ b/bgpwtf/cccampix/peeringdb/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "context" + "flag" + + "github.com/golang/glog" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "code.hackerspace.pl/hscloud/bgpwtf/cccampix/peeringdb/schema" + pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto" + "code.hackerspace.pl/hscloud/go/mirko" +) + +type service struct { +} + +func (s *service) GetIXMembers(ctx context.Context, req *pb.GetIXMembersRequest) (*pb.GetIXMembersResponse, error) { + if req.Id == 0 { + return nil, status.Error(codes.InvalidArgument, "IX id must be given") + } + + // First, get netixlans (membership info) for the given IX. + js := struct { + Data []schema.NetIXLan `json:"Data"` + }{} + err := schema.Get(ctx, &js, schema.NetIXLanInIXURL(req.Id)) + if err != nil { + return nil, status.Errorf(codes.Unavailable, "PeeringDB query error: %v", err) + } + + // Build set of seen Nets/ASs. + netids := make(map[int64]bool) + for _, netixlan := range js.Data { + netids[netixlan.NetID] = true + } + + // Convert set to unique list. + nets := make([]int64, len(netids)) + i := 0 + for id, _ := range netids { + nets[i] = id + i += 1 + } + + // Request information about nets/ASNs: + js2 := struct { + Data []schema.Net `json:"Data"` + }{} + err = schema.Get(ctx, &js2, schema.NetURLMulti(nets)) + if err != nil { + return nil, status.Errorf(codes.Unavailable, "PeeringDB query error: %v", err) + } + + // Make map net id -> Net + netidsNet := make(map[int64]*schema.Net) + for _, net := range js2.Data { + net := net + netidsNet[net.ID] = &net + } + + // Build joined response. + + res := &pb.GetIXMembersResponse{ + Members: make([]*pb.GetIXMembersResponse_Member, len(js.Data)), + } + + for i, netixlan := range js.Data { + res.Members[i] = &pb.GetIXMembersResponse_Member{ + Asn: netixlan.ASN, + Ipv4: netixlan.IPv4, + Ipv6: netixlan.IPv6, + Name: netidsNet[netixlan.NetID].Name, + } + } + + return res, nil +} + +func main() { + flag.Parse() + mi := mirko.New() + + if err := mi.Listen(); err != nil { + glog.Exitf("Listen failed: %v", err) + } + + s := &service{} + pb.RegisterPeeringDBProxyServer(mi.GRPC(), s) + + if err := mi.Serve(); err != nil { + glog.Exitf("Serve failed: %v", err) + } + + <-mi.Done() +} diff --git a/bgpwtf/cccampix/peeringdb/schema/BUILD.bazel b/bgpwtf/cccampix/peeringdb/schema/BUILD.bazel new file mode 100644 index 00000000..8eded088 --- /dev/null +++ b/bgpwtf/cccampix/peeringdb/schema/BUILD.bazel @@ -0,0 +1,11 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "schema.go", + "urls.go", + ], + importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/peeringdb/schema", + visibility = ["//visibility:public"], +) diff --git a/bgpwtf/cccampix/peeringdb/schema/schema.go b/bgpwtf/cccampix/peeringdb/schema/schema.go new file mode 100644 index 00000000..0d48aed8 --- /dev/null +++ b/bgpwtf/cccampix/peeringdb/schema/schema.go @@ -0,0 +1,34 @@ +package schema + +// Partial definition from https://www.peeringdb.com/apidocs/ + +type IX struct { + ID int64 `json:"id"` + OrgID int64 `json:"org_id"` + Name string `json:"name"` + IXLanSet []IXLan `json:"ixlan_set"` +} + +type IXLan struct { + ID int64 `json:"id"` + Name string `json:"name"` + NetSet []Net `json:"net_set"` +} + +type Net struct { + ID int64 `json:"id"` + OrgID int64 `json:"org_id"` + Name string `json:"name"` + ASN int64 `json:"asn"` +} + +type NetIXLan struct { + ID int64 `json:"id"` + NetID int64 `json:"net_id"` + IXID int64 `json:"ix_id"` + Name string `json:"name"` + Speed int64 `json:"speed"` + ASN int64 `json:"asn"` + IPv4 string `json:"ipaddr4"` + IPv6 string `json:"ipaddr6"` +} diff --git a/bgpwtf/cccampix/peeringdb/schema/urls.go b/bgpwtf/cccampix/peeringdb/schema/urls.go new file mode 100644 index 00000000..abe5f56f --- /dev/null +++ b/bgpwtf/cccampix/peeringdb/schema/urls.go @@ -0,0 +1,61 @@ +package schema + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" +) + +func IXURL(id int64) string { + return fmt.Sprintf("https://peeringdb.com/api/ix/%d.json?depth=4", id) +} + +func NetIXLanInIXURL(id int64) string { + return fmt.Sprintf("https://peeringdb.com/api/netixlan?ix_id__in=%d", id) +} + +func NetURLMulti(ids []int64) string { + sid := make([]string, len(ids)) + for i, id := range ids { + sid[i] = fmt.Sprintf("%d", id) + } + return fmt.Sprintf("https://peeringdb.com/api/net?id__in=%s", strings.Join(sid, ",")) +} + +func Get(ctx context.Context, obj interface{}, url string) error { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return fmt.Errorf("http.NewRequest(GET, %q): %v", url, err) + } + + req.Header.Add("User-Agent", "bgpwtf-cccampix-peeringdbproxy/1.0 (https://code.hackerspace.pl/hscloud/bgpwtf/cccampix/peeringdb)") + + req = req.WithContext(ctx) + + client := http.DefaultClient + res, err := client.Do(req) + if err != nil { + return fmt.Errorf("client.Do(%v): %v", req, err) + } + + defer res.Body.Close() + + if res.StatusCode != 200 { + return fmt.Errorf("got status code %d", res.StatusCode) + } + + data, err := ioutil.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("could not read response: %v", err) + } + + err = json.Unmarshal(data, &obj) + if err != nil { + return fmt.Errorf("could not parse response JSON: %v", err) + } + + return nil +} diff --git a/bgpwtf/cccampix/proto/BUILD.bazel b/bgpwtf/cccampix/proto/BUILD.bazel new file mode 100644 index 00000000..023cd8da --- /dev/null +++ b/bgpwtf/cccampix/proto/BUILD.bazel @@ -0,0 +1,23 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") + +proto_library( + name = "ix_proto", + srcs = ["ix.proto"], + visibility = ["//visibility:public"], +) + +go_proto_library( + name = "ix_go_proto", + compilers = ["@io_bazel_rules_go//proto:go_grpc"], + importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto", + proto = ":ix_proto", + visibility = ["//visibility:public"], +) + +go_library( + name = "go_default_library", + embed = [":ix_go_proto"], + importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto", + visibility = ["//visibility:public"], +) diff --git a/bgpwtf/cccampix/proto/ix.proto b/bgpwtf/cccampix/proto/ix.proto new file mode 100644 index 00000000..73bc4d17 --- /dev/null +++ b/bgpwtf/cccampix/proto/ix.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; +package ix; + +message GetIXMembersRequest { + // IX ID from PeeringDB + int64 id = 1; +} + +message GetIXMembersResponse { + message Member { + int64 asn = 1; + // Per PeeringDB, at least one of the following two address families + // will be set. + string ipv4 = 2; + string ipv6 = 3; + // AS/network name. + string name = 4; + }; + + repeated Member members = 1; +} + +service PeeringDBProxy { + // GetIXMembers returns information about membership of a given PeeringDB IX. + rpc GetIXMembers(GetIXMembersRequest) returns (GetIXMembersResponse); +}