├── .gitignore ├── bgpapi.go ├── README.md ├── types.go ├── trie.go ├── bgpreader.go └── http.go /.gitignore: -------------------------------------------------------------------------------- 1 | bgpapi 2 | bgp.log 3 | -------------------------------------------------------------------------------- /bgpapi.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/signal" 7 | "runtime" 8 | ) 9 | 10 | func main() { 11 | runtime.GOMAXPROCS(2) 12 | 13 | log.New(os.Stderr, "bgpapi", log.LstdFlags) 14 | log.SetPrefix("bgpapi ") 15 | 16 | log.Println("Starting") 17 | 18 | go bgpReader() 19 | httpServer() 20 | 21 | terminate := make(chan os.Signal) 22 | signal.Notify(terminate, os.Interrupt) 23 | 24 | <-terminate 25 | log.Printf("signal received, stopping") 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BGP API 2 | 3 | `bgpapi` runs as a "parsed-route-backend" to [ExaBGP](http://code.google.com/p/exabgp/) 4 | and provides an HTTP API to some BGP data. 5 | 6 | ## Compilation 7 | 8 | After installing Go (golang) and configuring the development environment, you can run 9 | 10 | go get -v github.com/abh/bgpapi 11 | go build 12 | go install 13 | 14 | ## Configuration 15 | 16 | bgpapi itself doesn't currently take any configuration. Configure a 17 | `neighbor` in ExaBGP and a `parsed-route-backend` process, like: 18 | 19 | process parsed-route-backend { 20 | parse-routes; 21 | peer-updates; 22 | run /Users/ask/go/bin/bgpapi; 23 | } 24 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/miekg/bitradix" 5 | "net" 6 | "sync" 7 | ) 8 | 9 | type ASN uint32 10 | 11 | type ASPath []ASN 12 | 13 | type Prefixes map[string]ASN 14 | 15 | type Neighbor struct { 16 | lock sync.RWMutex 17 | State string 18 | AsnPrefix map[ASN]Prefixes 19 | PrefixAsn Prefixes 20 | Updates int 21 | trie *bitradix.Radix32 22 | } 23 | 24 | type Route struct { 25 | Options map[string]string 26 | Prefix *net.IPNet 27 | ASPath ASPath 28 | PrimaryASN ASN 29 | } 30 | 31 | type Neighbors map[string]*Neighbor 32 | 33 | const ( 34 | parseKey = iota 35 | parseValue 36 | parseList 37 | parseSkip 38 | ) 39 | 40 | var DEBUG bool 41 | 42 | func (n *Neighbor) PrefixCount() int { 43 | n.lock.RLock() 44 | defer n.lock.RUnlock() 45 | return len(n.PrefixAsn) 46 | } 47 | 48 | func (n *Neighbor) AsnCount() int { 49 | n.lock.RLock() 50 | defer n.lock.RUnlock() 51 | return len(n.AsnPrefix) 52 | } 53 | -------------------------------------------------------------------------------- /trie.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/miekg/bitradix" 5 | "log" 6 | "net" 7 | "reflect" 8 | ) 9 | 10 | func addRoute(r *bitradix.Radix32, ipnet *net.IPNet, route *Route) { 11 | net, mask := ipNetToUint(ipnet) 12 | r.Insert(net, mask, route) 13 | } 14 | 15 | func removeRoute(r *bitradix.Radix32, ipnet *net.IPNet) { 16 | net, mask := ipNetToUint(ipnet) 17 | r.Remove(net, mask) 18 | } 19 | 20 | func ipNetToUint(n *net.IPNet) (i uint32, mask int) { 21 | i = ipToUint(&n.IP) 22 | mask, _ = n.Mask.Size() 23 | return 24 | } 25 | 26 | func ipToUint(nip *net.IP) (i uint32) { 27 | ip := nip.To4() 28 | fv := reflect.ValueOf(&i).Elem() 29 | fv.SetUint(uint64(uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[+3]))) 30 | return 31 | } 32 | 33 | func uintToIP(n uint32) net.IP { 34 | return net.IPv4(byte(n>>24), byte(n>>16), byte(n>>8), byte(n)) 35 | } 36 | 37 | func nodeToIPNet(node *bitradix.Radix32) *net.IPNet { 38 | ip := uintToIP(node.Key()) 39 | 40 | ipnet := net.IPNet{IP: ip, Mask: net.CIDRMask(node.Bits(), 32)} 41 | return &ipnet 42 | } 43 | 44 | func (n *Neighbor) FindNode(ip *net.IP) *bitradix.Radix32 { 45 | log.Println("Looking for ASN for", ip) 46 | i := ipToUint(ip) 47 | node := n.trie.Find(i, 32) 48 | return node 49 | } 50 | 51 | func (n *Neighbor) FindAsn(ip *net.IP) ASN { 52 | node := n.FindNode(ip) 53 | return node.Value.(Route).PrimaryASN 54 | } 55 | -------------------------------------------------------------------------------- /bgpreader.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/miekg/bitradix" 7 | "io" 8 | "log" 9 | "net" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | ) 15 | 16 | var neighbors Neighbors 17 | var neighbors_lock sync.RWMutex 18 | 19 | func bgpReader() { 20 | 21 | neighbors = make(Neighbors) 22 | 23 | r := bufio.NewReader(os.Stdin) 24 | 25 | var err error 26 | for line, err := r.ReadString('\n'); err == nil; line, err = r.ReadString('\n') { 27 | line = strings.TrimSpace(line) 28 | 29 | if line == "shutdown" { 30 | log.Println("Shutdown command received") 31 | return 32 | } 33 | 34 | if len(line) > 0 { 35 | processLine(line) 36 | } 37 | 38 | } 39 | 40 | if err != nil && err != io.EOF { 41 | log.Println(err) 42 | return 43 | } else { 44 | log.Println("EOF") 45 | } 46 | } 47 | 48 | func processLine(line string) { 49 | f := strings.SplitN(line, " ", 4) 50 | 51 | if len(f) < 3 { 52 | log.Printf("Did not split line into enough parts\nLINE: [%s]\nF: %#v\n", line, f) 53 | return 54 | } 55 | 56 | neighbor_ip := f[1] 57 | command := f[2] 58 | 59 | neighbors_lock.RLock() 60 | defer neighbors_lock.RUnlock() 61 | 62 | if neighbors[neighbor_ip] == nil { 63 | neighbors_lock.RUnlock() 64 | neighbors_lock.Lock() 65 | 66 | neighbor := new(Neighbor) 67 | neighbor.trie = bitradix.New32() 68 | neighbors[neighbor_ip] = neighbor 69 | 70 | neighbors_lock.Unlock() 71 | neighbors_lock.RLock() 72 | defer neighbors_lock.RUnlock() // double? 73 | } 74 | 75 | neighbor := neighbors[neighbor_ip] 76 | 77 | neighbor.lock.Lock() 78 | defer neighbor.lock.Unlock() 79 | 80 | switch command { 81 | case "up", "connected", "down": 82 | neighbor.State = command 83 | log.Println("State change", line) 84 | return 85 | case "update": 86 | neighbor.State = "update " + f[3] 87 | return 88 | case "announced": 89 | // fmt.Printf("R: %#v\n", r) 90 | 91 | neighbor.Updates++ 92 | 93 | route := parseRoute(f[3]) 94 | 95 | if ones, _ := route.Prefix.Mask.Size(); ones < 8 || ones > 25 { 96 | // fmt.Println("prefix mask too big or small", route.Prefix) 97 | } else { 98 | if neighbor.AsnPrefix == nil { 99 | neighbor.AsnPrefix = make(map[ASN]Prefixes) 100 | } 101 | if neighbor.PrefixAsn == nil { 102 | neighbor.PrefixAsn = make(Prefixes) 103 | } 104 | 105 | if neighbor.AsnPrefix[route.PrimaryASN] == nil { 106 | neighbor.AsnPrefix[route.PrimaryASN] = make(Prefixes) 107 | } 108 | 109 | neighbor.AsnPrefix[route.PrimaryASN][route.Prefix.String()] = 0 110 | neighbor.PrefixAsn[route.Prefix.String()] = route.PrimaryASN 111 | 112 | addRoute(neighbor.trie, route.Prefix, route) 113 | } 114 | case "withdrawn": 115 | 116 | neighbor.Updates++ 117 | 118 | route := parseRoute(f[3]) 119 | 120 | removeRoute(neighbor.trie, route.Prefix) 121 | 122 | if asn, exists := neighbor.PrefixAsn[route.Prefix.String()]; exists { 123 | // fmt.Println("Removing ASN from prefix", asn, route.Prefix) 124 | delete(neighbor.PrefixAsn, route.Prefix.String()) 125 | delete(neighbor.AsnPrefix[asn], route.Prefix.String()) 126 | } else { 127 | log.Println("Could not find prefix in PrefixAsn") 128 | log.Println("%#v", neighbor.PrefixAsn) 129 | } 130 | default: 131 | err_text := fmt.Sprintf("Command not implemented: %s\n%s\n", command, line) 132 | log.Println(err_text) 133 | err := fmt.Errorf(err_text) 134 | panic(err) 135 | } 136 | 137 | if neighbor.Updates%25000 == 0 { 138 | log.Printf("Processed %v updates from %s\n", neighbor.Updates, neighbor_ip) 139 | } 140 | 141 | } 142 | 143 | func parseRoute(input string) *Route { 144 | 145 | r := strings.Split(input, " ") 146 | 147 | route := new(Route) 148 | route.Options = make(map[string]string) 149 | aspath := make(ASPath, 0) 150 | 151 | var key string 152 | 153 | state := parseKey 154 | 155 | for _, v := range r { 156 | // fmt.Printf("k: %s, v: %s, state: %#v\n", key, v, state) 157 | 158 | switch state { 159 | case parseKey: 160 | { 161 | state = parseValue 162 | key = v 163 | continue 164 | } 165 | case parseValue: 166 | if v == "[" { 167 | state = parseList 168 | continue 169 | } 170 | state = parseKey 171 | 172 | if key == "as-path" { 173 | addASPath(&aspath, v) 174 | } 175 | route.Options[key] = v 176 | continue 177 | case parseList: 178 | { 179 | if v == "]" { 180 | state = parseKey 181 | continue 182 | } 183 | if key != "as-path" { 184 | log.Println("can only do list for as-path") 185 | log.Printf("key: %s, v: %s\n\n", key, v) 186 | log.Println("LINE", input) 187 | state = parseSkip 188 | continue 189 | } 190 | if v == "(" { 191 | state = parseSkip 192 | continue 193 | } 194 | 195 | addASPath(&aspath, v) 196 | 197 | } 198 | case parseSkip: 199 | switch v { 200 | case ")": 201 | state = parseList 202 | case "]": 203 | state = parseKey 204 | } 205 | } 206 | } 207 | // fmt.Printf("%#v / %#v\n", route, aspath) 208 | 209 | _, prefix, err := net.ParseCIDR(route.Options["route"]) 210 | if err != nil { 211 | log.Printf("Could not parse prefix %s %e\n", route.Options["route"], err) 212 | panic("bad prefix") 213 | } 214 | route.Prefix = prefix 215 | // fmt.Printf("IP: %s, PREFIX: %s\n", ip, prefix) 216 | 217 | if len(aspath) > 0 { 218 | route.PrimaryASN = ASN(aspath[len(aspath)-1]) 219 | route.ASPath = aspath 220 | } 221 | 222 | return route 223 | } 224 | 225 | func addASPath(aspath *ASPath, v string) { 226 | asn, err := strconv.Atoi(v) 227 | if err != nil { 228 | log.Println("Could not parse number", v) 229 | panic("Bad as-path") 230 | } 231 | *aspath = append(*aspath, ASN(asn)) 232 | } 233 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/gorilla/mux" 7 | "html/template" 8 | "log" 9 | "net" 10 | "net/http" 11 | "strconv" 12 | ) 13 | 14 | type homePageData struct { 15 | Title string 16 | Neighbors Neighbors 17 | Data map[string]string 18 | } 19 | 20 | func HomeHandler(w http.ResponseWriter, r *http.Request) { 21 | 22 | tmpl, err := template.New("index").Parse(index_tpl) 23 | if err != nil { 24 | log.Println("Could not parse template", err) 25 | fmt.Fprintln(w, "Problem parsing template", err) 26 | return 27 | } 28 | 29 | neighbors_lock.RLock() 30 | defer neighbors_lock.RUnlock() 31 | 32 | data := new(homePageData) 33 | data.Title = "BGP Status" 34 | data.Neighbors = neighbors 35 | 36 | // fmt.Fprintf(w, "%s\t%s\t%v\n", neighbor, data.State, data.Updates) 37 | // fmt.Printf("TMPL %s %#v\n", tmpl, tmpl) 38 | 39 | tmpl.Execute(w, data) 40 | 41 | } 42 | 43 | func StatusHandler(w http.ResponseWriter, r *http.Request) { 44 | neighbors_lock.RLock() 45 | defer neighbors_lock.RUnlock() 46 | 47 | for neighbor, data := range neighbors { 48 | data.lock.RLock() 49 | defer data.lock.RUnlock() 50 | fmt.Fprintf(w, "%s\t%s\t%v\n", neighbor, data.State, data.Updates) 51 | } 52 | } 53 | 54 | func ApiHandler(w http.ResponseWriter, r *http.Request) { 55 | w.Header().Set("Content-Type", "text/json") 56 | w.Header().Set("Access-Control-Allow-Origin", "*") 57 | 58 | vars := mux.Vars(r) 59 | 60 | neighbors_lock.RLock() 61 | defer neighbors_lock.RUnlock() 62 | 63 | type ipasnResult struct { 64 | ASN ASN 65 | ASPath ASPath `json:",omitempty"` 66 | Name string `json:",omitempty"` 67 | Prefix string `json:",omitempty"` 68 | } 69 | 70 | switch vars["method"] { 71 | case "ipasn": 72 | _ = r.ParseForm() 73 | ip := net.ParseIP(r.Form.Get("ip")) 74 | 75 | if ip == nil { 76 | w.WriteHeader(400) 77 | fmt.Fprintln(w, "Bad IP address") 78 | return 79 | } 80 | 81 | result := make(map[string]*ipasnResult) 82 | 83 | for neighbor, data := range neighbors { 84 | data.lock.RLock() 85 | defer data.lock.RUnlock() 86 | 87 | node := data.FindNode(&ip) 88 | if node.Bits() > 0 { 89 | result[neighbor] = new(ipasnResult) 90 | route := node.Value.(*Route) 91 | r := result[neighbor] 92 | r.ASN = ASN(route.PrimaryASN) 93 | r.ASPath = route.ASPath 94 | r.Name = "" 95 | r.Prefix = nodeToIPNet(node).String() 96 | } 97 | } 98 | 99 | json, err := json.Marshal(map[string]interface{}{"result": result}) 100 | if err != nil { 101 | w.WriteHeader(500) 102 | log.Println("Error generating json", err) 103 | fmt.Fprintln(w, "Could not generate JSON") 104 | } 105 | 106 | fmt.Fprint(w, string(json), "\n") 107 | 108 | default: 109 | w.WriteHeader(404) 110 | return 111 | } 112 | 113 | } 114 | 115 | func ApiNeighborHandler(w http.ResponseWriter, r *http.Request) { 116 | w.Header().Set("Content-Type", "text/json") 117 | 118 | vars := mux.Vars(r) 119 | 120 | neighbors_lock.RLock() 121 | defer neighbors_lock.RUnlock() 122 | 123 | neighbor_ip := vars["neighbor"] 124 | neighbor, ok := neighbors[neighbor_ip] 125 | if !ok { 126 | w.WriteHeader(404) 127 | return 128 | } 129 | 130 | neighbor.lock.RLock() 131 | defer neighbor.lock.RUnlock() 132 | 133 | // fmt.Printf("VARS: %#v\n", vars) 134 | 135 | switch vars["method"] { 136 | case "asn": 137 | asn, err := strconv.Atoi(vars["id"]) 138 | if err != nil { 139 | fmt.Fprintln(w, "Could not parse AS number") 140 | return 141 | } 142 | prefixes := neighbor.AsnPrefix[ASN(asn)] 143 | 144 | strPrefixes := make([]string, 0) 145 | 146 | for prefix, _ := range prefixes { 147 | strPrefixes = append(strPrefixes, prefix) 148 | } 149 | 150 | json, err := json.Marshal(map[string][]string{"prefixes": strPrefixes}) 151 | if err != nil { 152 | w.WriteHeader(500) 153 | log.Println("Error generating json", err) 154 | fmt.Fprintln(w, "Could not generate JSON") 155 | } 156 | fmt.Fprint(w, string(json)) 157 | 158 | case "ip": 159 | fmt.Fprintf(w, "/api/ip/%s not implemented\n", vars["id"]) 160 | return 161 | case "prefixes": 162 | prefixes := neighbor.PrefixAsn 163 | json, err := json.Marshal(map[string]Prefixes{"prefixes": prefixes}) 164 | if err != nil { 165 | fmt.Fprint(w, "error generating json", err) 166 | } 167 | fmt.Fprint(w, string(json)) 168 | default: 169 | w.WriteHeader(404) 170 | } 171 | fmt.Fprint(w, "\n") 172 | } 173 | 174 | func httpServer() { 175 | r := mux.NewRouter() 176 | r.HandleFunc("/", HomeHandler) 177 | r.HandleFunc("/api/{neighbor:[0-9.:]+}/{method:asn}/{id:[0-9]+}", ApiNeighborHandler) 178 | r.HandleFunc("/api/{neighbor:[0-9.:]+}/{method:ip}/{id:[0-9.:]+}", ApiNeighborHandler) 179 | r.HandleFunc("/api/{neighbor:[0-9.:]+}/{method:prefixes}", ApiNeighborHandler) 180 | r.HandleFunc("/api/{method:ipasn}", ApiHandler) 181 | 182 | r.HandleFunc("/status", StatusHandler) 183 | http.Handle("/", r) 184 | http.ListenAndServe(":8080", nil) 185 | } 186 | 187 | const index_tpl = ` 188 | 189 | BGP Status 190 | 191 | 198 | 199 | 200 | 201 |

{{.Title}}

202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | {{range $ip, $data := .Neighbors}} 216 | 217 | 220 | 223 | 226 | 229 | 232 | 233 | {{else}} 234 | 235 | {{end}} 236 | 237 |
IPStatePrefixesASNsUpdates
218 | {{$ip}} 219 | 221 | {{$data.State}} 222 | 224 | {{$data.PrefixCount}} 225 | 227 | {{$data.AsnCount}} 228 | 230 | {{$data.Updates}} 231 |
No neighbors
238 | 239 |

IP ASN Search

240 | 243 |
244 |
245 |
246 | 247 | 248 | 249 | 300 | 301 | 302 | 303 | ` 304 | --------------------------------------------------------------------------------