bgpwtf/cccampix/peeringdb: init
First pass at a proxy to expose PeeringDB data.
Change-Id: I844973755473b3abc5d334586744004b86d1c3a3
diff --git a/bgpwtf/cccampix/peeringdb/BUILD.bazel b/bgpwtf/cccampix/peeringdb/BUILD.bazel
new file mode 100644
index 0000000..22c90e3
--- /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 0000000..502760e
--- /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 0000000..fa11651
--- /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 0000000..8eded08
--- /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 0000000..0d48aed
--- /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 0000000..abe5f56
--- /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 0000000..023cd8d
--- /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 0000000..73bc4d1
--- /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);
+}