diff --git a/README.md b/README.md index 398407c..98fbdd6 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,34 @@ -Bird-lg-go -========== +# Bird-lg-go An alternative implementation for [bird-lg](https://github.com/sileht/bird-lg) written in Go. Both frontend and backend (proxy) are implemented, and can work with either the original Python implementation or the Go implementation. > The code on master branch no longer support BIRDv1. Branch "bird1" is the last version that supports BIRDv1. -Frontend --------- +## Table of Contents + + * [Bird-lg-go](#bird-lg-go) + * [Table of Contents](#table-of-contents) + * [Frontend](#frontend) + * [Proxy](#proxy) + * [Advanced Features](#advanced-features) + * [API](#api) + * [Request fields](#request-fields) + * [Response fields (when type is summary)](#response-fields-when-type-is-summary) + * [Fields for apiSummaryResultPair](#fields-for-apisummaryresultpair) + * [Fields for SummaryRowData](#fields-for-summaryrowdata) + * [Example response](#example-response) + * [Response fields (when type is bird, traceroute or whois)](#response-fields-when-type-is-bird-traceroute-or-whois) + * [Fields for apiGenericResultPair](#fields-for-apigenericresultpair) + * [Example response](#example-response-1) + * [Telegram Bot Webhook](#telegram-bot-webhook) + * [Example of setting the webhook](#example-of-setting-the-webhook) + * [Supported commands](#supported-commands) + * [Credits](#credits) + * [License](#license) + +Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc) + +## Frontend The frontend directory contains the code for the web frontend, where users see BGP states, do traceroutes and whois, etc. It's a replacement for "lg.py" in original bird-lg project. @@ -50,8 +72,7 @@ Example: the following docker-compose.yml entry does the same as above, but by s Demo: https://lg.lantian.pub -Proxy ------ +## Proxy The proxy directory contains the code for the "proxy" for bird commands and traceroutes. It's a replacement for "lgproxy.py" in original bird-lg project. @@ -91,14 +112,185 @@ Example: the following docker-compose.yml entry does the same as above, but by s You can use source IP restriction to increase security. You should also bind the proxy to a specific interface and use an external firewall/iptables for added security. -Credits -------- +## Advanced Features + +### API + +The frontend provides an API for running BIRD/traceroute/whois queries. + +API Endpoint: `https://your.frontend.com:5000/api/` (the last slash must not be omitted!) + +Requests are sent as POSTS with JSON bodies. + +#### Request fields + +| Name | Type | Value | +| ---- | ---- | -------- | +| `servers` | `[]string` | List of servers to be queried | +| `type` | `string` | Can be `summary`, `bird`, `traceroute` or `whois` | +| `args` | `string` | Arguments to be passed, see below | + +Argument examples for each type: + +- `summary`: `args` is ignored. Recommended to set to empty string. +- `bird`: `args` is the command to be passed to bird, e.g. `show route for 8.8.8.8` +- `traceroute`: `args` is the traceroute target, e.g. `8.8.8.8` or `google.com` +- `whois`: `args` is the whois target, e.g. `8.8.8.8` or `google.com` + +Example request: + +```json +{ + "servers": [ + "alpha" + ], + "type": "bird", + "args": "show route for 8.8.8.8" +} +``` + +#### Response fields (when `type` is `summary`) + +| Name | Type | Value | +| ---- | ---- | -------- | +| `error` | `string` | Error message when something is wrong. Empty when everything good | +| `result` | array of `apiSummaryResultPair` | See below | + +##### Fields for `apiSummaryResultPair` + +| Name | Type | Value | +| ---- | ---- | -------- | +| `server` | `string` | Name of the server | +| `data` | array of `SummaryRowData` | Summaries of the server, see below | + +##### Fields for `SummaryRowData` + +All fields below is 1:1 correspondent to the output of `birdc show protocols`. + +| Name | Type | +| ---- | ---- | +| `name` | `string` | +| `proto` | `string` | +| `table` | `string` | +| `state` | `string` | +| `since` | `string` | +| `info` | `string` | + +##### Example response + +Request: +```json +{ + "servers": [ + "alpha" + ], + "type": "summary", + "args": "" +} +``` + +Response: + +```json +{ + "error": "", + "result": [ + { + "server": "alpha", + "data": [ + { + "name": "bgp1", + "proto": "BGP", + "table": "---", + "state": "start", + "since": "2021-01-15 22:40:01", + "info": "Active Socket: Operation timed out" + }, + { + "name": "bgp2", + "proto": "BGP", + "table": "---", + "state": "start", + "since": "2021-01-03 08:15:48", + "info": "Established" + } + ] + } + ] +} +``` + +#### Response fields (when `type` is `bird`, `traceroute` or `whois`) + +| Name | Type | Value | +| ---- | ---- | -------- | +| `error` | `string` | Error message, empty when everything is good | +| `result` | array of `apiGenericResultPair` | See below | + +##### Fields for `apiGenericResultPair` + +| Name | Type | Value | +| ---- | ---- | -------- | +| `server` | `string` | Name of the server; is empty when type is `whois` | +| `data` | `string` | Result from the server | + +##### Example response + +Request: + +```json +{ + "servers": [ + "alpha" + ], + "type": "bird", + "args": "show status" +} +``` + +Response: + +```json +{ + "error": "", + "result": [ + { + "server": "alpha", + "data": "BIRD v2.0.7-137-g61dae32b\nRouter ID is 1.2.3.4\nCurrent server time is 2021-01-17 04:21:14.792\nLast reboot on 2021-01-03 08:15:48.494\nLast reconfiguration on 2021-01-17 00:49:10.573\nDaemon is up and running\n" + } + ] +} +``` + +### Telegram Bot Webhook + +The frontend can act as a Telegram Bot webhook endpoint, to add BGP route/traceroute/whois lookup functionality to your tech group. + +There is no configuration necessary on the frontend, just start it up normally. + +Set your Telegram Bot webhook URL to `https://your.frontend.com:5000/telegram/alpha+beta+gamma`, where `alpha+beta+gamma` is the list of servers to be queried on Telegram commands, separated by `+`. + +You may omit `alpha+beta+gamma` to use all your servers, but it is not recommended when you have lots of servers, or the message would be too long and hard to read. + +#### Example of setting the webhook + +```bash +curl "https://api.telegram.org/bot${BOT_TOKEN}/setWebhook?url=https://your.frontend.com:5000/telegram/alpha+beta+gamma" +``` + +#### Supported commands + +- `path`: Show bird's ASN path to target IP +- `route`: Show bird's preferred route to target IP +- `trace`: Traceroute to target IP/domain +- `whois`: Whois query + +## Credits - Everyone who contributed to this project (see Contributors section on the right) - Mehdi Abaakouk for creating [the original bird-lg project](https://github.com/sileht/bird-lg) - [Bootstrap](https://getbootstrap.com/) as web UI framework -License -------- +## License GPL 3.0 diff --git a/frontend/api.go b/frontend/api.go new file mode 100644 index 0000000..a7538a2 --- /dev/null +++ b/frontend/api.go @@ -0,0 +1,116 @@ +package main + +import ( + "encoding/json" + "errors" + "net/http" +) + +type apiRequest struct { + Servers []string `json:"servers"` + Type string `json:"type"` + Args string `json:"args"` +} + +type apiGenericResultPair struct { + Server string `json:"server"` + Data string `json:"data"` +} + +type apiSummaryResultPair struct { + Server string `json:"server"` + Data []SummaryRowData `json:"data"` +} + +type apiResponse struct { + Error string `json:"error"` + Result []interface{} `json:"result"` +} + +var apiHandlerMap = map[string](func(request apiRequest) apiResponse){ + "summary": apiSummaryHandler, + "bird": apiGenericHandlerFactory("bird"), + "traceroute": apiGenericHandlerFactory("traceroute"), + "whois": apiWhoisHandler, +} + +func apiGenericHandlerFactory(endpoint string) func(request apiRequest) apiResponse { + return func(request apiRequest) apiResponse { + results := batchRequest(request.Servers, endpoint, request.Args) + var response apiResponse + + for i, result := range results { + response.Result = append(response.Result, &apiGenericResultPair{ + Server: request.Servers[i], + Data: result, + }) + } + + return response + } +} + +func apiSummaryHandler(request apiRequest) apiResponse { + results := batchRequest(request.Servers, "bird", "show protocols") + var response apiResponse + + for i, result := range results { + parsedSummary, err := summaryParse(result, request.Servers[i]) + if err != nil { + return apiResponse{ + Error: err.Error(), + } + } + + response.Result = append(response.Result, &apiSummaryResultPair{ + Server: request.Servers[i], + Data: parsedSummary.Rows, + }) + } + + return response +} + +func apiWhoisHandler(request apiRequest) apiResponse { + return apiResponse{ + Error: "", + Result: []interface{}{ + apiGenericResultPair{ + Server: "", + Data: whois(request.Args), + }, + }, + } +} + +func apiErrorHandler(err error) apiResponse { + return apiResponse{ + Error: err.Error(), + } +} + +func apiHandler(w http.ResponseWriter, r *http.Request) { + var request apiRequest + var response apiResponse + err := json.NewDecoder(r.Body).Decode(&request) + if err != nil { + response = apiResponse{ + Error: err.Error(), + } + } else { + handler := apiHandlerMap[request.Type] + if handler == nil { + response = apiErrorHandler(errors.New("Invalid request type")) + } else { + response = handler(request) + } + } + + w.Header().Add("Content-Type", "application/json") + bytes, err := json.Marshal(response) + if err != nil { + println(err.Error()) + return + } + w.Write(bytes) +} diff --git a/frontend/render.go b/frontend/render.go index 899fd6a..b2710f8 100644 --- a/frontend/render.go +++ b/frontend/render.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "errors" "fmt" "net/http" "regexp" @@ -99,18 +100,17 @@ func smartFormatter(s string) string { return result } -// Output a table for the summary page -func summaryTable(data string, serverName string) string { +// Parse bird show protocols result +func summaryParse(data string, serverName string) (TemplateSummary, error) { + args := TemplateSummary{ + ServerName: serverName, + Raw: data, + } lines := strings.Split(strings.TrimSpace(data), "\n") if len(lines) <= 1 { // Likely backend returned an error message - return "
" + template.HTMLEscapeString(strings.TrimSpace(data)) + "
" - } - - args := TemplateSummary{ - ServerName: serverName, - Raw: data, + return args, errors.New(strings.TrimSpace(data)) } // extract the table header @@ -167,10 +167,21 @@ func summaryTable(data string, serverName string) string { args.Rows = append(args.Rows, row) } - // finally, render the summary template + return args, nil +} + +// Output a table for the summary page +func summaryTable(data string, serverName string) string { + result, err := summaryParse(data, serverName) + + if err != nil { + return "
" + template.HTMLEscapeString(err.Error()) + "
" + } + + // render the summary template tmpl := TemplateLibrary["summary"] var buffer bytes.Buffer - err := tmpl.Execute(&buffer, args) + err = tmpl.Execute(&buffer, result) if err != nil { fmt.Println("Error rendering summary:", err.Error()) } diff --git a/frontend/template.go b/frontend/template.go index 89b8ce9..87e0da9 100644 --- a/frontend/template.go +++ b/frontend/template.go @@ -32,15 +32,14 @@ type TemplatePage struct { } // summary - type SummaryRowData struct { - Name string - Proto string - Table string - State string - MappedState string - Since string - Info string + Name string `json:"name"` + Proto string `json:"proto"` + Table string `json:"table"` + State string `json:"state"` + MappedState string `json:"-"` + Since string `json:"since"` + Info string `json:"info"` } // utility functions to allow filtering of results in the template diff --git a/frontend/webserver.go b/frontend/webserver.go index 11900c0..7bb25b5 100644 --- a/frontend/webserver.go +++ b/frontend/webserver.go @@ -210,6 +210,7 @@ func webServerStart() { http.HandleFunc("/generic/", webBackendCommunicator("bird", "generic")) http.HandleFunc("/traceroute/", webBackendCommunicator("traceroute", "traceroute")) http.HandleFunc("/whois/", webHandlerWhois) + http.HandleFunc("/api/", apiHandler) http.HandleFunc("/telegram/", webHandlerTelegramBot) // Start HTTP server