From f945ec79133241d84d2221fc5eac76af7f82be11 Mon Sep 17 00:00:00 2001 From: Simon Marsh Date: Thu, 21 Feb 2019 14:13:35 +0000 Subject: [PATCH] Initial commit --- README.md | 8 ++ contrib/lgregmapper.service | 20 ++++ fakememcache.go | 134 ++++++++++++++++++++++++ lgregmapper.go | 109 +++++++++++++++++++ regclient.go | 201 ++++++++++++++++++++++++++++++++++++ 5 files changed, 472 insertions(+) create mode 100644 README.md create mode 100644 contrib/lgregmapper.service create mode 100644 fakememcache.go create mode 100644 lgregmapper.go create mode 100644 regclient.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..f60743a --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# lgregmapper + +A micro server to interface between bird-lg and dn42regsrv and provide registry +data for the bird-lg bgpmap functionality. + +The server works by querying the dn42regsrv API then providing a limited, memcached +like service that distributes the registry data to bird-lg. + diff --git a/contrib/lgregmapper.service b/contrib/lgregmapper.service new file mode 100644 index 0000000..1cb05fd --- /dev/null +++ b/contrib/lgregmapper.service @@ -0,0 +1,20 @@ +########################################################################## +# lgregmapper systemd service file +########################################################################## + +[Unit] +Description=Looking Glass Registry Mapper +After=network.target + +[Install] +WantedBy=multi-user.target + +[Service] +User=lglass +Group=lglass +Type=simple +Restart=on-failure +ExecStart=/opt/lgregmapper/lgregmapper -b ":11211" + +######################################################################### +# end of file diff --git a/fakememcache.go b/fakememcache.go new file mode 100644 index 0000000..908c29d --- /dev/null +++ b/fakememcache.go @@ -0,0 +1,134 @@ +////////////////////////////////////////////////////////////////////////// +// lgregmapper, a small utility to provide DN42 registry data to bird-lg +////////////////////////////////////////////////////////////////////////// + +package main + +////////////////////////////////////////////////////////////////////////// + +import ( + // "fmt" + "bufio" + log "github.com/sirupsen/logrus" + "net" + "strings" + "sync" +) + +////////////////////////////////////////////////////////////////////////// + +type FakeMemcache struct { + done chan bool + wait *sync.WaitGroup + regClient *RegClient +} + +////////////////////////////////////////////////////////////////////////// +// create a new fakememcache object + +func NewFakeMemcache(regClient *RegClient) *FakeMemcache { + + fmc := &FakeMemcache{ + done: make(chan bool), + wait: &sync.WaitGroup{}, + regClient: regClient, + } + + fmc.wait.Add(1) + return fmc +} + +////////////////////////////////////////////////////////////////////////// +// do the serving thing + +func (fmc *FakeMemcache) Start(listener net.Listener) { + defer listener.Close() + defer fmc.wait.Done() + + // listen and respond to connection or errors + go func() { + for { + + conn, err := listener.Accept() + if err != nil { + + // check if due to shutdown + select { + case <-fmc.done: + return + default: + } + + // not shutdown, log an error and try again + log.WithFields(log.Fields{ + "Error": err, + "addr": listener.Addr(), + }).Error("Listener accept error") + + } else { + fmc.wait.Add(1) + go fmc.Serve(conn) + } + } + }() + + // block waiting for done + <-fmc.done +} + +////////////////////////////////////////////////////////////////////////// +// serve a single client + +func (fmc *FakeMemcache) Serve(conn net.Conn) { + defer conn.Close() + defer fmc.wait.Done() + + log.WithFields(log.Fields{ + "Remote": conn.RemoteAddr(), + }).Debug("Accepted Connection") + + // read lines from the client + scanner := bufio.NewScanner(conn) + for scanner.Scan() { + command := strings.TrimSpace(scanner.Text()) + + // only respond to 'get' requests + if strings.HasPrefix(command, "get ") { + key := strings.TrimPrefix(command, "get ") + + if data := fmc.regClient.ASN[key]; data == nil { + log.WithFields(log.Fields{ + "command": command, + "remote": conn.RemoteAddr(), + }).Debug("Unknown key") + } else { + + // return the cached data + conn.Write(data) + } + + } else { + log.WithFields(log.Fields{ + "command": command, + "remote": conn.RemoteAddr(), + }).Debug("Received unknown command") + } + } + + log.WithFields(log.Fields{ + "Remote": conn.RemoteAddr(), + }).Debug("Closing Connection") +} + +////////////////////////////////////////////////////////////////////////// +// shutdown the server + +func (fmc *FakeMemcache) Shutdown() { + // signal shutdown and wait to complete + close(fmc.done) + // fmc.wait.Wait() + log.Info("FakeMemcache shutdown") +} + +////////////////////////////////////////////////////////////////////////// +// end of code diff --git a/lgregmapper.go b/lgregmapper.go new file mode 100644 index 0000000..307dbc6 --- /dev/null +++ b/lgregmapper.go @@ -0,0 +1,109 @@ +////////////////////////////////////////////////////////////////////////// +// lgregmapper, a small utility to provide DN42 registry data to bird-lg +////////////////////////////////////////////////////////////////////////// + +package main + +////////////////////////////////////////////////////////////////////////// + +import ( + // "fmt" + log "github.com/sirupsen/logrus" + flag "github.com/spf13/pflag" + "net" + "os" + "os/signal" + "time" +) + +////////////////////////////////////////////////////////////////////////// +// utility function to set the log level + +func setLogLevel(levelStr string) { + + if level, err := log.ParseLevel(levelStr); err != nil { + // failed to set the level + + // set a sensible default and, of course, log the error + log.SetLevel(log.InfoLevel) + log.WithFields(log.Fields{ + "loglevel": levelStr, + "error": err, + }).Error("Failed to set requested log level") + + } else { + + // set the requested level + log.SetLevel(level) + + } +} + +////////////////////////////////////////////////////////////////////////// +// everything starts here + +func main() { + + // set a default log level, so that logging can be used immediately + // the level will be overidden later on once the command line + // options are loaded + log.SetLevel(log.InfoLevel) + log.Info("LG Reg Mapper Server Starting") + + // declare cmd line options + var ( + logLevel = flag.StringP("LogLevel", "l", "Info", "Log level") + refreshInterval = flag.StringP("Refresh", "i", "60m", "Refresh interval") + bindAddress = flag.StringP("BindAddress", "b", "localhost:11211", + "Server bind address") + apiAddress = flag.StringP("APIAddress", "a", "http://collector.dn42:8042", + "DN42 API server address") + ) + flag.Parse() + + // now initialise logging properly based on the cmd line options + setLogLevel(*logLevel) + + // parse the refreshInterval and start data collection + interval, err := time.ParseDuration(*refreshInterval) + if err != nil { + log.WithFields(log.Fields{ + "error": err, + "interval": *refreshInterval, + }).Fatal("Unable to parse registry refresh interval") + } + + // fetch initial registry data and start regular updates + regc := NewRegClient(*apiAddress) + regc.Update() + go regc.Start(interval) + + listener, err := net.Listen("tcp", *bindAddress) + if err != nil { + log.WithFields(log.Fields{ + "BindAddress": *bindAddress, + "Error": err, + }).Fatal("Failed to bind listener") + } + log.WithFields(log.Fields{ + "BindAddress": *bindAddress, + }).Info("Listening") + + // create a new server and start it working on the listen port + fmc := NewFakeMemcache(regc) + go fmc.Start(listener) + + // sigint will perform a graceful exit + csig := make(chan os.Signal) + signal.Notify(csig, os.Interrupt) + + // and block + <-csig + + log.Info("Server shutting down") + regc.Shutdown() + fmc.Shutdown() +} + +////////////////////////////////////////////////////////////////////////// +// end of code diff --git a/regclient.go b/regclient.go new file mode 100644 index 0000000..10f8361 --- /dev/null +++ b/regclient.go @@ -0,0 +1,201 @@ +////////////////////////////////////////////////////////////////////////// +// lgregmapper, a small utility to provide DN42 registry data to bird-lg +////////////////////////////////////////////////////////////////////////// + +package main + +////////////////////////////////////////////////////////////////////////// + +import ( + "fmt" + log "github.com/sirupsen/logrus" + "net/http" + "strings" + "sync" + // "net" + "encoding/json" + // "io/ioutil" + "time" +) + +////////////////////////////////////////////////////////////////////////// + +type RegClient struct { + done chan bool + wait *sync.WaitGroup + endpoint string + client *http.Client + ASN map[string][]byte +} + +type APIAttributeResponse map[string]map[string][]string + +type ASNData struct { + mntner []string + descr string + prefixes []string +} + +////////////////////////////////////////////////////////////////////////// +// create a new RegClient object + +func NewRegClient(apiAddr string) *RegClient { + + rc := &RegClient{ + done: make(chan bool), + wait: &sync.WaitGroup{}, + client: &http.Client{Timeout: 20 * time.Second}, + endpoint: apiAddr, + ASN: nil, + } + + rc.wait.Add(1) + return rc +} + +////////////////////////////////////////////////////////////////////////// +// regularly update the registry + +func (rc *RegClient) Start(interval time.Duration) { + defer rc.wait.Done() + + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-rc.done: + return + + case <-ticker.C: + rc.Update() + } + } +} + +////////////////////////////////////////////////////////////////////////// +// shutdown the client + +func (rc *RegClient) Shutdown() { + close(rc.done) + rc.wait.Wait() + log.Info("Registry client shutdown") +} + +////////////////////////////////////////////////////////////////////////// +// fetch data from the registry API + +func (rc *RegClient) queryAPI(query string, target interface{}) error { + + query = rc.endpoint + "/api/registry/" + query + + log.WithFields(log.Fields{ + "Query": query, + }).Debug("Querying API") + + response, err := rc.client.Get(query) + if err != nil { + log.WithFields(log.Fields{ + "Error": err, + "Query": query, + }).Error("Unable to query API") + return err + } + defer response.Body.Close() + + if err := json.NewDecoder(response.Body).Decode(target); err != nil { + log.WithFields(log.Fields{ + "Error": err, + }).Error("Failed to decode JSON") + return err + } + return nil +} + +////////////////////////////////////////////////////////////////////////// +// perform an update + +func (rc *RegClient) Update() { + + // query the API to collect ASN maintainers + mnts := make(APIAttributeResponse) + if err := rc.queryAPI("aut-num/*/mnt-by?raw", &mnts); err != nil { + return + } + + // and ASN descriptions + descr := make(APIAttributeResponse) + if err := rc.queryAPI("aut-num/*/descr?raw", &descr); err != nil { + return + } + + // and query again to collect origin data + origins := make(APIAttributeResponse) + if err := rc.queryAPI("*route/*/origin?raw", &origins); err != nil { + return + } + + // normalise the returned data in to a struct + asnData := make(map[string]*ASNData) + for asnpath, adata := range mnts { + + asn := strings.TrimPrefix(asnpath, "aut-num/") + var description string + + dmap := descr[asnpath] + if dmap != nil && dmap["descr"] != nil && len(dmap["descr"]) > 0 { + description = dmap["descr"][0] + } + + asnData[asn] = &ASNData{ + mntner: adata["mnt-by"], + descr: description, + prefixes: nil, + } + } + + // and add the origin data + for prefix, odata := range origins { + ix := strings.IndexRune(prefix, '/') + prefix = strings.Replace(prefix[ix+1:], "_", "/", 1) + for _, origin := range odata["origin"] { + if asnd := asnData[origin]; asnd != nil { + asnd.prefixes = append(asnd.prefixes, prefix) + } + } + } + + // finally, turn the collated data in to a string suitable + // for returning from the fake memcache server + asns := make(map[string][]byte) + for asn, adata := range asnData { + + asn = "lg_" + strings.TrimPrefix(asn, "AS") + + plist := strings.Join(adata.prefixes, "\r") + if plist != "" { + // add spacer if not empty + plist = "\r" + plist + } + + if adata.descr != "" { + adata.descr += "\r" + } + + data := fmt.Sprintf("%s\r%s%s", + strings.Join(adata.mntner, "\r"), + adata.descr, plist) + + asns[asn] = []byte(fmt.Sprintf("VALUE %s 0 %d\r\n%s\r\nEND\r\n", + asn, len(data), data)) + } + + log.WithFields(log.Fields{ + "ASN Count": len(asns), + }).Debug("Updated ASN data") + + rc.ASN = asns +} + +////////////////////////////////////////////////////////////////////////// +// end of code