1
mirror of https://github.com/xddxdd/bird-lg-go synced 2025-10-27 06:42:13 +01:00

14 Commits

Author SHA1 Message Date
Lan Tian
cdc34704b5 release: v1.3.0 2023-05-14 12:37:46 -07:00
Lan Tian
db58bd3354 frontend: add a test case for lookup DNS -> WHOIS fallback 2023-05-06 00:26:40 -07:00
Lan Tian
a0246ccee2 general: add unit tests for >80% coverage
Includes a few minor fixes:
- frontend: support setting port for WHOIS server
- proxy: fix handling of very long lines
- proxy: refactor IP allowlist logic, parse allow IP list at startup
2023-05-06 00:23:28 -07:00
James Lu
ccd14af0c8 settings: treat empty environment variables as set (#77)
This allows disabling specific options like dns_interface or whois via environment variables.

ref: https://github.com/spf13/viper#working-with-environment-variables
2023-05-05 21:36:38 -07:00
Lan Tian
594ca80f50 frontend: fix whois lookup & only show bgpmap nexthop info on the first hop 2023-05-05 20:20:12 -07:00
Lan Tian
5625058e71 frontend: use ASN as bgpmap node identifier (instead of resolved whois result) 2023-05-05 19:52:30 -07:00
Lan Tian
7efa3237a9 frontend: refactor bgpmap code to fix #75 2023-05-05 01:58:05 -07:00
Lan Tian
7b0c8c0556 general: bump go version in go.mod 2023-01-26 22:01:47 -08:00
dependabot[bot]
ffd9165062 build(deps): bump github.com/spf13/viper in /frontend (#73)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.14.0 to 1.15.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.14.0...v1.15.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-23 10:50:42 -08:00
dependabot[bot]
24fd5203e8 build(deps): bump github.com/spf13/viper from 1.14.0 to 1.15.0 in /proxy (#74)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.14.0 to 1.15.0.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.14.0...v1.15.0)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-23 10:50:30 -08:00
Lan Tian
49a05767c1 ci: bump version for go-release-action 2023-01-08 01:16:23 -06:00
Lan Tian
e7010f75f8 release: v1.2.0 2023-01-06 23:05:05 -06:00
Yuhui Xu
dba2af7634 proxy: fix description for --traceroute_flags (#70) 2022-12-27 15:38:41 -06:00
Yuhui Xu
049775319b proxy: autodetect traceroute args on startup (#69) 2022-12-25 15:41:29 -06:00
35 changed files with 2817 additions and 454 deletions

View File

@@ -19,14 +19,14 @@ jobs:
goos: windows
steps:
- uses: actions/checkout@v3
- uses: wangyoucao577/go-release-action@v1.30
- uses: wangyoucao577/go-release-action@v1.34
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
project_path: "./frontend"
binary_name: "bird-lg-go"
- uses: wangyoucao577/go-release-action@v1.30
- uses: wangyoucao577/go-release-action@v1.34
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}

View File

@@ -58,8 +58,6 @@ Configuration can be set in:
Configuration is handled by [viper](https://github.com/spf13/viper), any config format supported by it can be used.
> Note: the config system is replaced with viper only recently (2022-07-08). If some config items do not work, please open an issue, and use commit [892a7bee22a1bb02d3b4da6d270c65b6e4e1321a](https://github.com/xddxdd/bird-lg-go/tree/892a7bee22a1bb02d3b4da6d270c65b6e4e1321a) (last version before config system replace) for the time being.
| Config Key | Parameter | Environment Variable | Description |
| ---------- | --------- | -------------------- | ----------- |
| servers | --servers | BIRDLG_SERVERS | server name prefixes, separated by comma |
@@ -79,6 +77,8 @@ Configuration is handled by [viper](https://github.com/spf13/viper), any config
| name_filter | --name-filter | BIRDLG_NAME_FILTER | protocol names to hide in summary tables (RE2 syntax); defaults to none if not set |
| timeout | --time-out | BIRDLG_TIMEOUT | time before request timed out, in seconds; defaults to 120 if not set |
### Examples
Example: the following command starts the frontend with 2 BIRD nodes, with domain name "gigsgigscloud.dn42.lantian.pub" and "hostdare.dn42.lantian.pub", and proxies are running on port 8000 on both nodes.
```bash
@@ -122,16 +122,32 @@ Configuration can be set in:
Configuration is handled by [viper](https://github.com/spf13/viper), any config format supported by it can be used.
> Note: the config system is replaced with viper only recently (2022-07-08). If some config items do not work, please open an issue, and use commit [892a7bee22a1bb02d3b4da6d270c65b6e4e1321a](https://github.com/xddxdd/bird-lg-go/tree/892a7bee22a1bb02d3b4da6d270c65b6e4e1321a) (last version before config system replace) for the time being.
| Config Key | Parameter | Environment Variable | Description |
| ---------- | --------- | -------------------- | ----------- |
| allowed_ips | --allowed | ALLOWED_IPS | IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs. (default "") |
| bird_socket | --bird | BIRD_SOCKET | socket file for bird, set either in parameter or environment variable BIRD_SOCKET (default "/var/run/bird/bird.ctl") |
| listen | --listen | BIRDLG_PROXY_PORT | listen address, set either in parameter or environment variable BIRDLG_PROXY_PORT(default "8000") |
| traceroute_bin | --traceroute_bin | BIRDLG_TRACEROUTE_BIN | traceroute binary file, set either in parameter or environment variable BIRDLG_TRACEROUTE_BIN(default "traceroute") |
| traceroute_bin | --traceroute_bin | BIRDLG_TRACEROUTE_BIN | traceroute binary file, set either in parameter or environment variable BIRDLG_TRACEROUTE_BIN |
| traceroute_flags | --traceroute_flags | BIRDLG_TRACEROUTE_FLAGS | traceroute flags, supports multiple flags separated with space. |
| traceroute_raw | --traceroute_raw | BIRDLG_TRACEROUTE_RAW | whether to display traceroute outputs raw (default false) |
### Traceroute Binary Autodetection
If `traceroute_bin` or `traceroute_flags` is not set, then on startup, the proxy will try to `traceroute 127.0.0.1` with different traceroute binaries and arguments, in order to use the most optimized setting available, while maintaining compatibility with multiple variants of traceroute binaries.
Traceroute binaries will be autodetected in the following order:
1. If `traceroute_bin` is set:
1. `[traceroute_bin] -q1 -N32 -w1 127.0.0.1` (Corresponds to Traceroute on Debian)
2. `[traceroute_bin] -q1 -w1 127.0.0.1` (Corresponds to Traceroute on FreeBSD)
3. `[traceroute_bin] 127.0.0.1` (Corresponds to Busybox Traceroute)
2. `mtr -w -c1 -Z1 -G1 -b 127.0.0.1` (MTR)
3. `traceroute -q1 -N32 -w1 127.0.0.1` (Corresponds to Traceroute on Debian)
4. `traceroute -q1 -w1 127.0.0.1` (Corresponds to Traceroute on FreeBSD)
5. `traceroute 127.0.0.1` (Corresponds to Busybox Traceroute)
### Examples
Example: start proxy with default configuration, should work "out of the box" on Debian 9 with BIRDv1:
```bash

View File

@@ -1 +1 @@
v1.1.1
v1.3.0

View File

@@ -113,7 +113,7 @@ func apiHandler(w http.ResponseWriter, r *http.Request) {
} else {
handler := apiHandlerMap[request.Type]
if handler == nil {
response = apiErrorHandler(errors.New("Invalid request type"))
response = apiErrorHandler(errors.New("invalid request type"))
} else {
response = handler(request)
}

207
frontend/api_test.go Normal file
View File

@@ -0,0 +1,207 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/jarcoal/httpmock"
"github.com/magiconair/properties/assert"
)
func TestApiServerListHandler(t *testing.T) {
setting.servers = []string{"alpha", "beta", "gamma"}
response := apiServerListHandler(apiRequest{})
assert.Equal(t, len(response.Result), 3)
assert.Equal(t, response.Result[0].(apiGenericResultPair).Server, "alpha")
assert.Equal(t, response.Result[1].(apiGenericResultPair).Server, "beta")
assert.Equal(t, response.Result[2].(apiGenericResultPair).Server, "gamma")
}
func TestApiGenericHandlerFactory(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, BirdSummaryData)
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show protocols"), httpResponse)
setting.servers = []string{"alpha"}
setting.domain = ""
setting.proxyPort = 8000
request := apiRequest{
Servers: setting.servers,
Type: "bird",
Args: "show protocols",
}
handler := apiGenericHandlerFactory("bird")
response := handler(request)
assert.Equal(t, response.Error, "")
result := response.Result[0].(*apiGenericResultPair)
assert.Equal(t, result.Server, "alpha")
assert.Equal(t, result.Data, BirdSummaryData)
}
func TestApiSummaryHandler(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, BirdSummaryData)
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show protocols"), httpResponse)
setting.servers = []string{"alpha"}
setting.domain = ""
setting.proxyPort = 8000
request := apiRequest{
Servers: setting.servers,
Type: "summary",
Args: "",
}
response := apiSummaryHandler(request)
assert.Equal(t, response.Error, "")
summary := response.Result[0].(*apiSummaryResultPair)
assert.Equal(t, summary.Server, "alpha")
// Protocol list will be sorted
assert.Equal(t, summary.Data[1].Name, "device1")
assert.Equal(t, summary.Data[1].Proto, "Device")
assert.Equal(t, summary.Data[1].Table, "---")
assert.Equal(t, summary.Data[1].State, "up")
assert.Equal(t, summary.Data[1].Since, "2021-08-27")
assert.Equal(t, summary.Data[1].Info, "")
}
func TestApiSummaryHandlerError(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, "Mock backend error")
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show protocols"), httpResponse)
setting.servers = []string{"alpha"}
setting.domain = ""
setting.proxyPort = 8000
request := apiRequest{
Servers: setting.servers,
Type: "summary",
Args: "",
}
response := apiSummaryHandler(request)
assert.Equal(t, response.Error, "Mock backend error")
}
func TestApiWhoisHandler(t *testing.T) {
expectedData := "Mock Data"
server := WhoisServer{
t: t,
expectedQuery: "AS6939",
response: expectedData,
}
server.Listen()
go server.Run()
defer server.Close()
setting.whoisServer = server.server.Addr().String()
request := apiRequest{
Servers: []string{},
Type: "",
Args: "AS6939",
}
response := apiWhoisHandler(request)
assert.Equal(t, response.Error, "")
whoisResult := response.Result[0].(apiGenericResultPair)
assert.Equal(t, whoisResult.Server, "")
assert.Equal(t, whoisResult.Data, expectedData)
}
func TestApiErrorHandler(t *testing.T) {
err := errors.New("Mock Error")
response := apiErrorHandler(err)
assert.Equal(t, response.Error, "Mock Error")
}
func TestApiHandler(t *testing.T) {
setting.servers = []string{"alpha", "beta", "gamma"}
request := apiRequest{
Servers: []string{},
Type: "server_list",
Args: "",
}
requestJson, err := json.Marshal(request)
if err != nil {
t.Error(err)
}
r := httptest.NewRequest(http.MethodGet, "/api", bytes.NewReader(requestJson))
w := httptest.NewRecorder()
apiHandler(w, r)
var response apiResponse
err = json.Unmarshal(w.Body.Bytes(), &response)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(response.Result), 3)
// Hard to unmarshal JSON into apiGenericResultPair objects, won't check here
}
func TestApiHandlerBadJSON(t *testing.T) {
setting.servers = []string{"alpha", "beta", "gamma"}
r := httptest.NewRequest(http.MethodGet, "/api", strings.NewReader("{bad json}"))
w := httptest.NewRecorder()
apiHandler(w, r)
var response apiResponse
err := json.Unmarshal(w.Body.Bytes(), &response)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(response.Result), 0)
}
func TestApiHandlerInvalidType(t *testing.T) {
setting.servers = []string{"alpha", "beta", "gamma"}
request := apiRequest{
Servers: setting.servers,
Type: "invalid_type",
Args: "",
}
requestJson, err := json.Marshal(request)
if err != nil {
t.Error(err)
}
r := httptest.NewRequest(http.MethodGet, "/api", bytes.NewReader(requestJson))
w := httptest.NewRecorder()
apiHandler(w, r)
var response apiResponse
err = json.Unmarshal(w.Body.Bytes(), &response)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(response.Result), 0)
}

83
frontend/asn_cache.go Normal file
View File

@@ -0,0 +1,83 @@
package main
import (
"fmt"
"net"
"strings"
)
type ASNCache map[string]string
func (cache ASNCache) _lookup(asn string) string {
// Try to get ASN representation using DNS
if setting.dnsInterface != "" {
records, err := net.LookupTXT(fmt.Sprintf("AS%s.%s", asn, setting.dnsInterface))
if err == nil {
result := strings.Join(records, " ")
if resultSplit := strings.Split(result, " | "); len(resultSplit) > 1 {
result = strings.Join(resultSplit[1:], "\n")
}
return fmt.Sprintf("AS%s\n%s", asn, result)
}
}
// Try to get ASN representation using WHOIS
if setting.whoisServer != "" {
if setting.bgpmapInfo == "" {
setting.bgpmapInfo = "asn,as-name,ASName,descr"
}
records := whois(fmt.Sprintf("AS%s", asn))
if records != "" {
recordsSplit := strings.Split(records, "\n")
var result []string
for _, title := range strings.Split(setting.bgpmapInfo, ",") {
if title == "asn" {
result = append(result, "AS"+asn)
}
}
for _, title := range strings.Split(setting.bgpmapInfo, ",") {
allow_multiline := false
if title[0] == ':' && len(title) >= 2 {
title = title[1:]
allow_multiline = true
}
for _, line := range recordsSplit {
if len(line) == 0 || line[0] == '%' || !strings.Contains(line, ":") {
continue
}
linearr := strings.SplitN(line, ":", 2)
line_title := linearr[0]
content := strings.TrimSpace(linearr[1])
if line_title != title {
continue
}
result = append(result, content)
if !allow_multiline {
break
}
}
}
if len(result) > 0 {
return strings.Join(result, "\n")
}
}
}
return ""
}
func (cache ASNCache) Lookup(asn string) string {
cachedValue, cacheOk := cache[asn]
if cacheOk {
return cachedValue
}
result := cache._lookup(asn)
if len(result) == 0 {
result = fmt.Sprintf("AS%s", asn)
}
cache[asn] = result
return result
}

View File

@@ -0,0 +1,52 @@
package main
import (
"strings"
"testing"
"github.com/magiconair/properties/assert"
)
func TestGetASNRepresentationDNS(t *testing.T) {
checkNetwork(t)
setting.dnsInterface = "asn.cymru.com"
setting.whoisServer = ""
cache := make(ASNCache)
result := cache.Lookup("6939")
if !strings.Contains(result, "HURRICANE") {
t.Errorf("Lookup AS6939 failed, got %s", result)
}
}
func TestGetASNRepresentationDNSFallback(t *testing.T) {
checkNetwork(t)
setting.dnsInterface = "invalid.example.com"
setting.whoisServer = "whois.arin.net"
cache := make(ASNCache)
result := cache.Lookup("6939")
if !strings.Contains(result, "HURRICANE") {
t.Errorf("Lookup AS6939 failed, got %s", result)
}
}
func TestGetASNRepresentationWhois(t *testing.T) {
checkNetwork(t)
setting.dnsInterface = ""
setting.whoisServer = "whois.arin.net"
cache := make(ASNCache)
result := cache.Lookup("6939")
if !strings.Contains(result, "HURRICANE") {
t.Errorf("Lookup AS6939 failed, got %s", result)
}
}
func TestGetASNRepresentationFallback(t *testing.T) {
setting.dnsInterface = ""
setting.whoisServer = ""
cache := make(ASNCache)
result := cache.Lookup("6939")
assert.Equal(t, result, "AS6939")
}

File diff suppressed because it is too large Load Diff

173
frontend/bgpmap_graph.go Normal file
View File

@@ -0,0 +1,173 @@
package main
import (
"encoding/json"
"fmt"
"strings"
)
type RouteAttrs map[string]string
type RoutePoint struct {
performLookup bool
attrs RouteAttrs
}
type RouteEdgeKey struct {
src string
dest string
}
type RouteEdgeValue struct {
label []string
attrs RouteAttrs
}
type RouteGraph struct {
points map[string]RoutePoint
edges map[RouteEdgeKey]RouteEdgeValue
}
func makeRouteGraph() RouteGraph {
return RouteGraph{
points: make(map[string]RoutePoint),
edges: make(map[RouteEdgeKey]RouteEdgeValue),
}
}
func makeRoutePoint() RoutePoint {
return RoutePoint{
performLookup: false,
attrs: make(RouteAttrs),
}
}
func makeRouteEdgeValue() RouteEdgeValue {
return RouteEdgeValue{
label: []string{},
attrs: make(RouteAttrs),
}
}
func (graph *RouteGraph) attrsToString(attrs RouteAttrs) string {
if len(attrs) == 0 {
return ""
}
result := ""
isFirst := true
for k, v := range attrs {
if isFirst {
isFirst = false
} else {
result += ","
}
result += graph.escape(k) + "=" + graph.escape(v) + ""
}
return "[" + result + "]"
}
func (graph *RouteGraph) escape(s string) string {
result, err := json.Marshal(s)
if err != nil {
return err.Error()
} else {
return string(result)
}
}
func (graph *RouteGraph) AddEdge(src string, dest string, label string, attrs RouteAttrs) {
// Add edges with same src/dest separately, multiple edges with same src/dest could exist
edge := RouteEdgeKey{
src: src,
dest: dest,
}
newValue, exists := graph.edges[edge]
if !exists {
newValue = makeRouteEdgeValue()
}
if len(label) != 0 {
newValue.label = append(newValue.label, label)
}
for k, v := range attrs {
newValue.attrs[k] = v
}
graph.edges[edge] = newValue
}
func (graph *RouteGraph) AddPoint(name string, performLookup bool, attrs RouteAttrs) {
newValue, exists := graph.points[name]
if !exists {
newValue = makeRoutePoint()
}
newValue.performLookup = performLookup
for k, v := range attrs {
newValue.attrs[k] = v
}
graph.points[name] = newValue
}
func (graph *RouteGraph) GetEdge(src string, dest string) *RouteEdgeValue {
key := RouteEdgeKey{
src: src,
dest: dest,
}
value, ok := graph.edges[key]
if ok {
return &value
} else {
return nil
}
}
func (graph *RouteGraph) GetPoint(name string) *RoutePoint {
value, ok := graph.points[name]
if ok {
return &value
} else {
return nil
}
}
func (graph *RouteGraph) ToGraphviz() string {
var result string
asnCache := make(ASNCache)
for name, value := range graph.points {
var representation string
if value.performLookup {
representation = asnCache.Lookup(name)
} else {
representation = name
}
attrsCopy := value.attrs
if attrsCopy == nil {
attrsCopy = make(RouteAttrs)
}
attrsCopy["label"] = representation
result += fmt.Sprintf("%s %s;\n", graph.escape(name), graph.attrsToString(value.attrs))
}
for key, value := range graph.edges {
attrsCopy := value.attrs
if attrsCopy == nil {
attrsCopy = make(RouteAttrs)
}
if len(value.label) > 0 {
attrsCopy["label"] = strings.Join(value.label, "\n")
}
result += fmt.Sprintf("%s -> %s %s;\n", graph.escape(key.src), graph.escape(key.dest), graph.attrsToString(attrsCopy))
}
return "digraph {\n" + result + "}\n"
}

View File

@@ -1,84 +1,23 @@
package main
import (
"io/ioutil"
"path"
"runtime"
"strings"
"testing"
)
func contains(s []string, str string) bool {
for _, v := range s {
if v == str {
return true
}
}
return false
}
func TestGetASNRepresentationDNS(t *testing.T) {
checkNetwork(t)
setting.dnsInterface = "asn.cymru.com"
setting.whoisServer = ""
result := getASNRepresentation("6939")
if !strings.Contains(result, "HURRICANE") {
t.Errorf("Lookup AS6939 failed, got %s", result)
}
}
func TestGetASNRepresentationWhois(t *testing.T) {
checkNetwork(t)
setting.dnsInterface = ""
setting.whoisServer = "whois.arin.net"
result := getASNRepresentation("6939")
if !strings.Contains(result, "HURRICANE") {
t.Errorf("Lookup AS6939 failed, got %s", result)
}
}
func TestGetASNRepresentationFallback(t *testing.T) {
setting.dnsInterface = ""
setting.whoisServer = ""
result := getASNRepresentation("6939")
if result != "AS6939" {
t.Errorf("Lookup AS6939 failed, got %s", result)
}
}
// Broken due to random order of attributes
func TestBirdRouteToGraphviz(t *testing.T) {
setting.dnsInterface = ""
// Don't change formatting of the following strings!
fakeResult := `192.168.0.1/32 unicast [alpha 2021-01-14 from 192.168.0.2] * (100) [AS12345i]
via 192.168.0.2 on eth0
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242422601
BGP.next_hop: 172.18.0.2`
expectedLinesInResult := []string{
`"AS4242422601" [`,
`"AS4242422601" -> "Target: 192.168.0.1" [`,
`"Target: 192.168.0.1" [`,
`"alpha" [`,
`"alpha" -> "AS4242422601" [`,
}
result := birdRouteToGraphviz([]string{
"alpha",
}, []string{
fakeResult,
}, "192.168.0.1")
for _, line := range expectedLinesInResult {
if !strings.Contains(result, line) {
t.Errorf("Expected line in result not found: %s", line)
}
func readDataFile(t *testing.T, filename string) string {
_, sourceName, _, _ := runtime.Caller(0)
projectRoot := path.Join(path.Dir(sourceName), "..")
dir := path.Join(projectRoot, filename)
data, err := ioutil.ReadFile(dir)
if err != nil {
t.Fatal(err)
}
return string(data)
}
func TestBirdRouteToGraphvizXSS(t *testing.T) {
@@ -98,3 +37,48 @@ func TestBirdRouteToGraphvizXSS(t *testing.T) {
t.Errorf("XSS injection succeeded: %s", result)
}
}
func TestBirdRouteToGraph(t *testing.T) {
setting.dnsInterface = ""
input := readDataFile(t, "frontend/test_data/bgpmap_case1.txt")
result := birdRouteToGraph([]string{"node"}, []string{input}, "target")
// Source node must exist
if result.GetPoint("node") == nil {
t.Error("Result doesn't contain point node")
}
// Last hop must exist
if result.GetPoint("4242423914") == nil {
t.Error("Result doesn't contain point 4242423914")
}
// Destination must exist
if result.GetPoint("target") == nil {
t.Error("Result doesn't contain point target")
}
// Verify that a few paths exist
if result.GetEdge("node", "4242423914") == nil {
t.Error("Result doesn't contain edge from node to 4242423914")
}
if result.GetEdge("node", "4242422688") == nil {
t.Error("Result doesn't contain edge from node to 4242422688")
}
if result.GetEdge("4242422688", "4242423914") == nil {
t.Error("Result doesn't contain edge from 4242422688 to 4242423914")
}
if result.GetEdge("4242423914", "target") == nil {
t.Error("Result doesn't contain edge from 4242423914 to target")
}
}
func TestBirdRouteToGraphviz(t *testing.T) {
setting.dnsInterface = ""
input := readDataFile(t, "frontend/test_data/bgpmap_case1.txt")
result := birdRouteToGraphviz([]string{"node"}, []string{input}, "target")
if !strings.Contains(result, "digraph {") {
t.Error("Response is not Graphviz data")
}
}

View File

@@ -65,7 +65,7 @@ func shortenWhoisFilter(whois string) string {
shouldSkip := false
shouldSkip = shouldSkip || len(s) == 0
shouldSkip = shouldSkip || len(s) > 0 && s[0] == '#'
shouldSkip = shouldSkip || strings.Contains(strings.ToUpper(s), "REDACTED FOR PRIVACY")
shouldSkip = shouldSkip || strings.Contains(strings.ToUpper(s), "REDACTED")
if shouldSkip {
skippedLinesLonger++

View File

@@ -28,3 +28,79 @@ func TestDN42WhoisFilterUnneeded(t *testing.T) {
t.Errorf("Output doesn't match expected: %s", result)
}
}
func TestShortenWhoisFilterShorterMode(t *testing.T) {
input := `
Information line that will be removed
# Comment that will be removed
Name: Redacted for privacy
Descr: This is a vvvvvvvvvvvvvvvvvvvvvvveeeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrrrrrrrrrryyyyyyyyyyyyyyyyyyy long line that will be skipped.
Looooooooooooooooooooooong key: this line will be skipped.
Preserved1: this line isn't removed.
Preserved2: this line isn't removed.
Preserved3: this line isn't removed.
Preserved4: this line isn't removed.
Preserved5: this line isn't removed.
`
result := shortenWhoisFilter(input)
expectedResult := `Preserved1: this line isn't removed.
Preserved2: this line isn't removed.
Preserved3: this line isn't removed.
Preserved4: this line isn't removed.
Preserved5: this line isn't removed.
3 line(s) skipped.
`
if result != expectedResult {
t.Errorf("Output doesn't match expected: %s", result)
}
}
func TestShortenWhoisFilterLongerMode(t *testing.T) {
input := `
Information line that will be removed
# Comment that will be removed
Name: Redacted for privacy
Descr: This is a vvvvvvvvvvvvvvvvvvvvvvveeeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrrrrrrrrrryyyyyyyyyyyyyyyyyyy long line that will be skipped.
Looooooooooooooooooooooong key: this line will be skipped.
Preserved1: this line isn't removed.
`
result := shortenWhoisFilter(input)
expectedResult := `Information line that will be removed
Descr: This is a vvvvvvvvvvvvvvvvvvvvvvveeeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrrrrrrrrrryyyyyyyyyyyyyyyyyyy long line that will be skipped.
Looooooooooooooooooooooong key: this line will be skipped.
Preserved1: this line isn't removed.
7 line(s) skipped.
`
if result != expectedResult {
t.Errorf("Output doesn't match expected: %s", result)
}
}
func TestShortenWhoisFilterSkipNothing(t *testing.T) {
input := `Preserved1: this line isn't removed.
Preserved2: this line isn't removed.
Preserved3: this line isn't removed.
Preserved4: this line isn't removed.
Preserved5: this line isn't removed.
`
result := shortenWhoisFilter(input)
if result != input {
t.Errorf("Output doesn't match expected: %s", result)
}
}

View File

@@ -1,10 +1,29 @@
module github.com/xddxdd/bird-lg-go/frontend
go 1.16
go 1.17
require (
github.com/gorilla/handlers v1.5.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.15.0
)
require (
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/gorilla/handlers v1.5.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.14.0
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jarcoal/httpmock v1.3.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

File diff suppressed because it is too large Load Diff

View File

@@ -56,8 +56,8 @@ func batchRequest(servers []string, endpoint string, command string) []string {
buf := make([]byte, 65536)
n, err := io.ReadFull(response.Body, buf)
if err != nil && err != io.ErrUnexpectedEOF {
ch <- channelData{i, err.Error()}
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
ch <- channelData{i, "request failed: " + err.Error()}
} else {
ch <- channelData{i, string(buf[:n])}
}

163
frontend/lgproxy_test.go Normal file
View File

@@ -0,0 +1,163 @@
package main
import (
"errors"
"strings"
"testing"
"github.com/jarcoal/httpmock"
)
func TestBatchRequestIPv4(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, "Mock Result")
httpmock.RegisterResponder("GET", "http://1.1.1.1:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://2.2.2.2:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://3.3.3.3:8000/mock?q=cmd", httpResponse)
setting.servers = []string{
"1.1.1.1",
"2.2.2.2",
"3.3.3.3",
}
setting.domain = ""
setting.proxyPort = 8000
response := batchRequest(setting.servers, "mock", "cmd")
if len(response) != 3 {
t.Error("Did not get response of all three mock servers")
}
for i := 0; i < len(response); i++ {
if response[i] != "Mock Result" {
t.Error("HTTP response mismatch")
}
}
}
func TestBatchRequestIPv6(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, "Mock Result")
httpmock.RegisterResponder("GET", "http://[2001:db8::1]:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://[2001:db8::2]:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://[2001:db8::3]:8000/mock?q=cmd", httpResponse)
setting.servers = []string{
"2001:db8::1",
"2001:db8::2",
"2001:db8::3",
}
setting.domain = ""
setting.proxyPort = 8000
response := batchRequest(setting.servers, "mock", "cmd")
if len(response) != 3 {
t.Error("Did not get response of all three mock servers")
}
for i := 0; i < len(response); i++ {
if response[i] != "Mock Result" {
t.Error("HTTP response mismatch")
}
}
}
func TestBatchRequestEmptyResponse(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, "")
httpmock.RegisterResponder("GET", "http://alpha:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://beta:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://gamma:8000/mock?q=cmd", httpResponse)
setting.servers = []string{
"alpha",
"beta",
"gamma",
}
setting.domain = ""
setting.proxyPort = 8000
response := batchRequest(setting.servers, "mock", "cmd")
if len(response) != 3 {
t.Error("Did not get response of all three mock servers")
}
for i := 0; i < len(response); i++ {
if !strings.Contains(response[i], "node returned empty response") {
t.Error("Did not produce error for empty response")
}
}
}
func TestBatchRequestDomainSuffix(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpResponse := httpmock.NewStringResponder(200, "Mock Result")
httpmock.RegisterResponder("GET", "http://alpha.suffix:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://beta.suffix:8000/mock?q=cmd", httpResponse)
httpmock.RegisterResponder("GET", "http://gamma.suffix:8000/mock?q=cmd", httpResponse)
setting.servers = []string{
"alpha",
"beta",
"gamma",
}
setting.domain = "suffix"
setting.proxyPort = 8000
response := batchRequest(setting.servers, "mock", "cmd")
if len(response) != 3 {
t.Error("Did not get response of all three mock servers")
}
for i := 0; i < len(response); i++ {
if response[i] != "Mock Result" {
t.Error("HTTP response mismatch")
}
}
}
func TestBatchRequestHTTPError(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpError := httpmock.NewErrorResponder(errors.New("Oops!"))
httpmock.RegisterResponder("GET", "http://alpha:8000/mock?q=cmd", httpError)
httpmock.RegisterResponder("GET", "http://beta:8000/mock?q=cmd", httpError)
httpmock.RegisterResponder("GET", "http://gamma:8000/mock?q=cmd", httpError)
setting.servers = []string{
"alpha",
"beta",
"gamma",
}
setting.domain = ""
setting.proxyPort = 8000
response := batchRequest(setting.servers, "mock", "cmd")
if len(response) != 3 {
t.Error("Did not get response of all three mock servers")
}
for i := 0; i < len(response); i++ {
if !strings.Contains(response[i], "request failed") {
t.Error("Did not produce HTTP error")
}
}
}
func TestBatchRequestInvalidServer(t *testing.T) {
setting.servers = []string{}
setting.domain = ""
setting.proxyPort = 8000
response := batchRequest([]string{"invalid"}, "mock", "cmd")
if len(response) != 1 {
t.Error("Did not get response of all mock servers")
}
if !strings.Contains(response[0], "invalid server") {
t.Error("Did not produce invalid server error")
}
}

View File

@@ -8,6 +8,17 @@ import (
"testing"
)
const BirdSummaryData = `BIRD 2.0.8 ready.
Name Proto Table State Since Info
static1 Static master4 up 2021-08-27
static2 Static master6 up 2021-08-27
device1 Device --- up 2021-08-27
kernel1 Kernel master6 up 2021-08-27
kernel2 Kernel master4 up 2021-08-27
direct1 Direct --- up 2021-08-27
int_babel Babel --- up 2021-08-27
`
func initSettings() {
setting.servers = []string{"alpha"}
setting.serversDisplay = []string{"alpha"}
@@ -101,17 +112,8 @@ func TestSummaryTableXSS(t *testing.T) {
func TestSummaryTableProtocolFilter(t *testing.T) {
initSettings()
setting.protocolFilter = []string{"Static", "Direct", "Babel"}
data := `BIRD 2.0.8 ready.
Name Proto Table State Since Info
static1 Static master4 up 2021-08-27
static2 Static master6 up 2021-08-27
device1 Device --- up 2021-08-27
kernel1 Kernel master6 up 2021-08-27
kernel2 Kernel master4 up 2021-08-27
direct1 Direct --- up 2021-08-27
int_babel Babel --- up 2021-08-27 `
result := string(summaryTable(data, "testserver"))
result := string(summaryTable(BirdSummaryData, "testserver"))
expectedInclude := []string{"static1", "static2", "int_babel", "direct1"}
expectedExclude := []string{"device1", "kernel1", "kernel2"}
@@ -134,17 +136,8 @@ int_babel Babel --- up 2021-08-27 `
func TestSummaryTableNameFilter(t *testing.T) {
initSettings()
setting.nameFilter = "^static"
data := `BIRD 2.0.8 ready.
Name Proto Table State Since Info
static1 Static master4 up 2021-08-27
static2 Static master6 up 2021-08-27
device1 Device --- up 2021-08-27
kernel1 Kernel master6 up 2021-08-27
kernel2 Kernel master4 up 2021-08-27
direct1 Direct --- up 2021-08-27
int_babel Babel --- up 2021-08-27 `
result := string(summaryTable(data, "testserver"))
result := string(summaryTable(BirdSummaryData, "testserver"))
expectedInclude := []string{"device1", "kernel1", "kernel2", "direct1", "int_babel"}
expectedExclude := []string{"static1", "static2"}

View File

@@ -33,6 +33,7 @@ func parseSettings() {
viper.AddConfigPath(".")
viper.AddConfigPath("/etc/bird-lg")
viper.SetConfigName("bird-lg")
viper.AllowEmptyEnv(true)
viper.AutomaticEnv()
viper.SetEnvPrefix("birdlg")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))

View File

@@ -0,0 +1,8 @@
package main
import "testing"
func TestParseSettings(t *testing.T) {
parseSettings()
// Good as long as it doesn't panic
}

File diff suppressed because it is too large Load Diff

25
frontend/template_test.go Normal file
View File

@@ -0,0 +1,25 @@
package main
import (
"testing"
"github.com/magiconair/properties/assert"
)
func TestSummaryRowDataNameHasPrefix(t *testing.T) {
data := SummaryRowData{
Name: "mock",
}
assert.Equal(t, data.NameHasPrefix("m"), true)
assert.Equal(t, data.NameHasPrefix("n"), false)
}
func TestSummaryRowDataNameContains(t *testing.T) {
data := SummaryRowData{
Name: "mock",
}
assert.Equal(t, data.NameContains("oc"), true)
assert.Equal(t, data.NameContains("no"), false)
}

View File

@@ -0,0 +1,151 @@
Table master4:
172.20.0.53/32 unicast [ibgp_sjc2 2023-04-29 from fd86:bad:11b7:22::1] * (100/38) [AS4242423914i]
via 169.254.108.122 on igp-sjc2
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423914
BGP.next_hop: 172.20.229.122
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,1) (64511,24) (64511,34)
BGP.large_community: (4242421080, 101, 44) (4242421080, 103, 122) (4242421080, 104, 1)
unicast [miaotony_2688 2023-04-29 from fe80::2688] (100) [AS4242423914i]
via 172.23.6.6 on dn42las-miaoton
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242422688 4242423914
BGP.next_hop: 172.23.6.6
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,3) (64511,24) (64511,34)
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [imlonghao_1888 2023-04-17] (100) [AS4242423914i]
via fe80::1888 on dn42-imlonghao
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242421888 4242423914
BGP.next_hop: :: fe80::1888
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,1) (64511,24) (64511,34)
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [ciplc_3021 2023-04-29 from fe80::943e] (100) [AS4242423914i]
via 172.23.33.161 on dn42-ciplc
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423021 4242423914
BGP.next_hop: 172.23.33.161
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,1) (64511,24) (64511,34)
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [iedon_2189 2023-04-29 from fe80::2189:ef] (100) [AS4242423914i]
via 172.23.91.114 on dn42-iedon
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242422189 4242423914
BGP.next_hop: 172.23.91.114
BGP.med: 65
BGP.local_pref: 100
BGP.community: (64511,24) (64511,33) (64511,3)
BGP.large_community: (4242422189, 1, 4) (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [prevarinite_2475 2023-04-19] (100) [AS4242423914i]
via fe80::7072:6576:6172:1 on dn42-prevarinit
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242422475 4242423192 4242423914
BGP.next_hop: :: fe80::7072:6576:6172:1
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,1) (64511,24) (64511,34)
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [lare_3035 2023-04-29] (100) [AS4242423914i]
via fe80::3035:132 on dn42-lare
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423035 4242423914
BGP.next_hop: :: fe80::3035:132
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,3) (64511,34) (64511,24)
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [hinata_3724 2023-04-29 from fe80::3724] (100) [AS4242423914i]
via 172.23.215.228 on dn42las-hinata
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423724 4201271111 4242423914
BGP.next_hop: 172.23.215.228
BGP.med: 70
BGP.local_pref: 100
BGP.community: (64511,22) (64511,1) (64511,34)
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [liki4_0927 2023-04-21] (100) [AS4242423914i]
via fe80::927 on dn42-liki4
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242420927 4242421888 4242423914
BGP.next_hop: :: fe80::927
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,2) (64511,24) (64511,34)
BGP.large_community: (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [eastbound_2633 2023-04-29 from fe80::2633] (100) [AS4242423914i]
via 172.23.250.42 on dn42las-eastbnd
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242422633 4242423914
BGP.next_hop: 172.23.250.42
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,24) (64511,34) (64511,3)
BGP.large_community: (4242422633, 101, 44) (4242422633, 103, 36) (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [yura_2464 2023-04-29] (100) [AS4242423914i]
via fe80::2464 on dn42las-yura
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242422464 4242423914
BGP.next_hop: :: fe80::2464
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,1) (64511,24) (64511,34)
BGP.large_community: (4242422464, 2, 4242423914) (4242422464, 64511, 44) (4242422464, 64511, 1840) (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)
unicast [ibgp_fra 2023-04-29 from fd86:bad:11b7:117::1] (100/186) [AS4242423914i]
via 169.254.108.113 on igp-chi
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423914
BGP.next_hop: 172.20.229.117
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,1) (64511,24) (64511,34)
BGP.large_community: (4242421080, 101, 41) (4242421080, 103, 117) (4242421080, 104, 3)
unicast [ibgp_sgp 2023-04-29 from fd86:bad:11b7:239::1] (100/200) [AS4242423914i]
via 169.254.108.39 on igp-sgp
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423914
BGP.next_hop: 172.22.108.39
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,4) (64511,24) (64511,34)
BGP.large_community: (4242421080, 101, 51) (4242421080, 103, 39) (4242421080, 104, 4)
unicast [ibgp_ymq 2023-04-30 from fd86:bad:11b7:23::1] (100/105) [AS4242423914i]
via 169.254.108.113 on igp-chi
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423914
BGP.next_hop: 172.20.229.123
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,3) (64511,24) (64511,34)
BGP.large_community: (4242421080, 101, 42) (4242421080, 103, 123) (4242421080, 104, 2)
unicast [cola_3391 18:41:16.608 from fe80::3391] (100) [AS4242423914i]
via 172.22.96.65 on dn42-cola
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 4242423391 4242420604 4242423914
BGP.next_hop: 172.22.96.65
BGP.med: 50
BGP.local_pref: 100
BGP.community: (64511,4) (64511,34) (64511,24)
BGP.large_community: (4242420604, 2, 50) (4242420604, 501, 4242423914) (4242420604, 502, 44) (4242420604, 504, 4) (4242421080, 104, 1) (4242421080, 101, 44) (4242421080, 103, 126)

View File

@@ -0,0 +1,89 @@
package main
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/jarcoal/httpmock"
"github.com/magiconair/properties/assert"
)
func TestServerError(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/error", nil)
w := httptest.NewRecorder()
serverError(w, r)
assert.Equal(t, w.Code, http.StatusInternalServerError)
}
func TestWebHandlerWhois(t *testing.T) {
server := WhoisServer{
t: t,
expectedQuery: "AS6939",
response: AS6939Response,
}
server.Listen()
go server.Run()
defer server.Close()
setting.netSpecificMode = ""
setting.whoisServer = server.server.Addr().String()
r := httptest.NewRequest(http.MethodGet, "/whois/AS6939", nil)
w := httptest.NewRecorder()
webHandlerWhois(w, r)
assert.Equal(t, w.Code, http.StatusOK)
if !strings.Contains(w.Body.String(), "HURRICANE") {
t.Error("Body does not contain whois result")
}
}
func TestWebBackendCommunicator(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
input := readDataFile(t, "frontend/test_data/bgpmap_case1.txt")
httpResponse := httpmock.NewStringResponder(200, input)
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show route for 1.1.1.1 all"), httpResponse)
setting.servers = []string{"alpha"}
setting.domain = ""
setting.proxyPort = 8000
setting.dnsInterface = ""
setting.whoisServer = ""
r := httptest.NewRequest(http.MethodGet, "/route_bgpmap/alpha/1.1.1.1", nil)
w := httptest.NewRecorder()
handler := webBackendCommunicator("bird", "route_all")
handler(w, r)
assert.Equal(t, w.Code, http.StatusOK)
}
func TestWebHandlerBGPMap(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
input := readDataFile(t, "frontend/test_data/bgpmap_case1.txt")
httpResponse := httpmock.NewStringResponder(200, input)
httpmock.RegisterResponder("GET", "http://alpha:8000/bird?q="+url.QueryEscape("show route for 1.1.1.1 all"), httpResponse)
setting.servers = []string{"alpha"}
setting.domain = ""
setting.proxyPort = 8000
setting.dnsInterface = ""
setting.whoisServer = ""
r := httptest.NewRequest(http.MethodGet, "/route_bgpmap/alpha/1.1.1.1", nil)
w := httptest.NewRecorder()
handler := webHandlerBGPMap("bird", "route_bgpmap")
handler(w, r)
assert.Equal(t, w.Code, http.StatusOK)
}

View File

@@ -6,6 +6,8 @@ import (
"os/exec"
"strings"
"time"
"github.com/google/shlex"
)
// Send a whois request
@@ -15,7 +17,13 @@ func whois(s string) string {
}
if strings.HasPrefix(setting.whoisServer, "/") {
cmd := exec.Command(setting.whoisServer, s)
args, err := shlex.Split(setting.whoisServer)
if err != nil {
return err.Error()
}
args = append(args, s)
cmd := exec.Command(args[0], args[1:]...)
output, err := cmd.CombinedOutput()
if err != nil {
return err.Error()
@@ -26,7 +34,13 @@ func whois(s string) string {
return string(output)
} else {
buf := make([]byte, 65536)
conn, err := net.DialTimeout("tcp", setting.whoisServer+":43", 5*time.Second)
whoisServer := setting.whoisServer
if !strings.Contains(whoisServer, ":") {
whoisServer = whoisServer + ":43"
}
conn, err := net.DialTimeout("tcp", whoisServer, 5*time.Second)
if err != nil {
return err.Error()
}
@@ -35,7 +49,7 @@ func whois(s string) string {
conn.Write([]byte(s + "\r\n"))
n, err := io.ReadFull(conn, buf)
if err != nil && err != io.ErrUnexpectedEOF {
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return err.Error()
}
return string(buf[:n])

View File

@@ -1,14 +1,78 @@
package main
import (
"bufio"
"net"
"strings"
"testing"
)
func TestWhois(t *testing.T) {
checkNetwork(t)
type WhoisServer struct {
t *testing.T
expectedQuery string
response string
server net.Listener
}
setting.whoisServer = "whois.arin.net"
const AS6939Response = `
ASNumber: 6939
ASName: HURRICANE
ASHandle: AS6939
RegDate: 1996-06-28
Updated: 2003-11-04
Ref: https://rdap.arin.net/registry/autnum/6939
`
func (s *WhoisServer) Listen() {
var err error
s.server, err = net.Listen("tcp", "127.0.0.1:0")
if err != nil {
s.t.Error(err)
}
}
func (s *WhoisServer) Run() {
for {
conn, err := s.server.Accept()
if err != nil {
break
}
if conn == nil {
break
}
reader := bufio.NewReader(conn)
query, err := reader.ReadBytes('\n')
if err != nil {
break
}
if strings.TrimSpace(string(query)) != s.expectedQuery {
s.t.Errorf("Query %s doesn't match expectation %s", string(query), s.expectedQuery)
}
conn.Write([]byte(s.response))
conn.Close()
}
}
func (s *WhoisServer) Close() {
if s.server == nil {
return
}
s.server.Close()
}
func TestWhois(t *testing.T) {
server := WhoisServer{
t: t,
expectedQuery: "AS6939",
response: AS6939Response,
}
server.Listen()
go server.Run()
defer server.Close()
setting.whoisServer = server.server.Addr().String()
result := whois("AS6939")
if !strings.Contains(result, "HURRICANE") {
t.Errorf("Whois AS6939 failed, got %s", result)
@@ -22,3 +86,43 @@ func TestWhoisWithoutServer(t *testing.T) {
t.Errorf("Whois AS6939 without server produced output, got %s", result)
}
}
func TestWhoisConnectionError(t *testing.T) {
setting.whoisServer = "127.0.0.1:0"
result := whois("AS6939")
if !strings.Contains(result, "connect: connection refused") {
t.Errorf("Whois AS6939 without server produced output, got %s", result)
}
}
func TestWhoisHostProcess(t *testing.T) {
setting.whoisServer = "/bin/sh -c \"echo Mock Result\""
result := whois("AS6939")
if result != "Mock Result\n" {
t.Errorf("Whois didn't produce expected result, got %s", result)
}
}
func TestWhoisHostProcessMalformedCommand(t *testing.T) {
setting.whoisServer = "/bin/sh -c \"mock"
result := whois("AS6939")
if result != "EOF found when expecting closing quote" {
t.Errorf("Whois didn't produce expected result, got %s", result)
}
}
func TestWhoisHostProcessError(t *testing.T) {
setting.whoisServer = "/nonexistent"
result := whois("AS6939")
if !strings.Contains(result, "no such file or directory") {
t.Errorf("Whois didn't produce expected result, got %s", result)
}
}
func TestWhoisHostProcessVeryLong(t *testing.T) {
setting.whoisServer = "/bin/sh -c \"for i in $(seq 1 131072); do printf 'A'; done\""
result := whois("AS6939")
if len(result) != 65535 {
t.Errorf("Whois result incorrectly truncated, actual len %d", len(result))
}
}

View File

@@ -8,19 +8,23 @@ import (
"strings"
)
const MAX_LINE_SIZE = 1024
// Read a line from bird socket, removing preceding status number, output it.
// Returns if there are more lines.
func birdReadln(bird io.Reader, w io.Writer) bool {
// Read from socket byte by byte, until reaching newline character
c := make([]byte, 1024, 1024)
c := make([]byte, MAX_LINE_SIZE)
pos := 0
for {
if pos >= 1024 {
// Leave one byte for newline character
if pos >= MAX_LINE_SIZE-1 {
break
}
_, err := bird.Read(c[pos : pos+1])
if err != nil {
panic(err)
w.Write([]byte(err.Error()))
return false
}
if c[pos] == byte('\n') {
break
@@ -29,6 +33,7 @@ func birdReadln(bird io.Reader, w io.Writer) bool {
}
c = c[:pos+1]
c[pos] = '\n'
// print(string(c[:]))
// Remove preceding status number, different situations

213
proxy/bird_test.go Normal file
View File

@@ -0,0 +1,213 @@
package main
import (
"bufio"
"bytes"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"net/url"
"path"
"strings"
"testing"
"github.com/magiconair/properties/assert"
)
type BirdServer struct {
t *testing.T
expectedQuery string
response string
server net.Listener
socket string
injectError string
}
func (s *BirdServer) initSocket() {
tmpDir, err := ioutil.TempDir("", "bird-lgproxy-go-mock")
if err != nil {
s.t.Fatal(err)
}
s.socket = path.Join(tmpDir, "mock.socket")
}
func (s *BirdServer) Listen() {
s.initSocket()
var err error
s.server, err = net.Listen("unix", s.socket)
if err != nil {
s.t.Error(err)
}
}
func (s *BirdServer) Run() {
for {
conn, err := s.server.Accept()
if err != nil {
break
}
if conn == nil {
break
}
reader := bufio.NewReader(conn)
conn.Write([]byte("1234 Hello from mock bird\n"))
query, err := reader.ReadBytes('\n')
if err != nil {
break
}
if strings.TrimSpace(string(query)) != "restrict" {
s.t.Errorf("Did not restrict bird permissions")
}
if s.injectError == "restriction" {
conn.Write([]byte("1234 Restriction is disabled!\n"))
} else {
conn.Write([]byte("1234 Access restricted\n"))
}
query, err = reader.ReadBytes('\n')
if err != nil {
break
}
if strings.TrimSpace(string(query)) != s.expectedQuery {
s.t.Errorf("Query %s doesn't match expectation %s", string(query), s.expectedQuery)
}
responseList := strings.Split(s.response, "\n")
for i := range responseList {
if i == len(responseList)-1 {
if s.injectError == "eof" {
conn.Write([]byte("0000 " + responseList[i]))
} else {
conn.Write([]byte("0000 " + responseList[i] + "\n"))
}
} else {
conn.Write([]byte("1234 " + responseList[i] + "\n"))
}
}
conn.Close()
}
}
func (s *BirdServer) Close() {
if s.server == nil {
return
}
s.server.Close()
}
func TestBirdReadln(t *testing.T) {
input := strings.NewReader("1234 Bird Message\n")
var output bytes.Buffer
birdReadln(input, &output)
assert.Equal(t, output.String(), "Bird Message\n")
}
func TestBirdReadlnNoPrefix(t *testing.T) {
input := strings.NewReader(" Message without prefix\n")
var output bytes.Buffer
birdReadln(input, &output)
assert.Equal(t, output.String(), "Message without prefix\n")
}
func TestBirdReadlnVeryLongLine(t *testing.T) {
input := strings.NewReader(strings.Repeat("A", 4096))
var output bytes.Buffer
birdReadln(input, &output)
assert.Equal(t, output.String(), strings.Repeat("A", 1022)+"\n")
}
func TestBirdWriteln(t *testing.T) {
var output bytes.Buffer
birdWriteln(&output, "Test command")
assert.Equal(t, output.String(), "Test command\n")
}
func TestBirdHandlerWithoutQuery(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/bird", nil)
w := httptest.NewRecorder()
birdHandler(w, r)
}
func TestBirdHandlerWithQuery(t *testing.T) {
server := BirdServer{
t: t,
expectedQuery: "show protocols",
response: "Mock Response\nSecond Line",
injectError: "",
}
server.Listen()
go server.Run()
defer server.Close()
setting.birdSocket = server.socket
r := httptest.NewRequest(http.MethodGet, "/bird?q="+url.QueryEscape(server.expectedQuery), nil)
w := httptest.NewRecorder()
birdHandler(w, r)
assert.Equal(t, w.Code, http.StatusOK)
assert.Equal(t, w.Body.String(), server.response+"\n")
}
func TestBirdHandlerWithBadSocket(t *testing.T) {
setting.birdSocket = "/nonexistent.sock"
r := httptest.NewRequest(http.MethodGet, "/bird?q="+url.QueryEscape("mock"), nil)
w := httptest.NewRecorder()
birdHandler(w, r)
assert.Equal(t, w.Code, http.StatusInternalServerError)
}
func TestBirdHandlerWithoutRestriction(t *testing.T) {
server := BirdServer{
t: t,
expectedQuery: "show protocols",
response: "Mock Response",
injectError: "restriction",
}
server.Listen()
go server.Run()
defer server.Close()
setting.birdSocket = server.socket
r := httptest.NewRequest(http.MethodGet, "/bird?q="+url.QueryEscape("mock"), nil)
w := httptest.NewRecorder()
birdHandler(w, r)
assert.Equal(t, w.Code, http.StatusInternalServerError)
}
func TestBirdHandlerEOF(t *testing.T) {
server := BirdServer{
t: t,
expectedQuery: "show protocols",
response: "Mock Response\nSecond Line",
injectError: "eof",
}
server.Listen()
go server.Run()
defer server.Close()
setting.birdSocket = server.socket
r := httptest.NewRequest(http.MethodGet, "/bird?q="+url.QueryEscape("show protocols"), nil)
w := httptest.NewRecorder()
birdHandler(w, r)
assert.Equal(t, w.Code, http.StatusOK)
assert.Equal(t, w.Body.String(), "Mock Response\nEOF")
}

View File

@@ -1,11 +1,28 @@
module github.com/xddxdd/bird-lg-go/proxy
go 1.15
go 1.17
require (
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/gorilla/handlers v1.5.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.14.0
github.com/spf13/viper v1.15.0
)
require (
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
package main
import (
"fmt"
"net"
"net/http"
"os"
@@ -20,40 +21,51 @@ func invalidHandler(httpW http.ResponseWriter, httpR *http.Request) {
httpW.Write([]byte("Invalid Request\n"))
}
func hasAccess(remoteAddr string) bool {
// setting.allowedIPs will always have at least one element because of how it's defined
if len(setting.allowedIPs) == 0 {
return true
}
if !strings.Contains(remoteAddr, ":") {
return false
}
// Remove port from IP and remove brackets that are around IPv6 addresses
remoteAddr = remoteAddr[0:strings.LastIndex(remoteAddr, ":")]
remoteAddr = strings.Trim(remoteAddr, "[]")
ipObject := net.ParseIP(remoteAddr)
if ipObject == nil {
return false
}
for _, allowedIP := range setting.allowedIPs {
if ipObject.Equal(allowedIP) {
return true
}
}
return false
}
// Access handler, check to see if client IP in allowed IPs, continue if it is, send to invalidHandler if not
func accessHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(httpW http.ResponseWriter, httpR *http.Request) {
// setting.allowedIPs will always have at least one element because of how it's defined
if setting.allowedIPs[0] == "" {
if hasAccess(httpR.RemoteAddr) {
next.ServeHTTP(httpW, httpR)
return
} else {
invalidHandler(httpW, httpR)
}
IPPort := httpR.RemoteAddr
// Remove port from IP and remove brackets that are around IPv6 addresses
requestIp := IPPort[0:strings.LastIndex(IPPort, ":")]
requestIp = strings.Replace(requestIp, "[", "", -1)
requestIp = strings.Replace(requestIp, "]", "", -1)
for _, allowedIP := range setting.allowedIPs {
if requestIp == allowedIP {
next.ServeHTTP(httpW, httpR)
return
}
}
invalidHandler(httpW, httpR)
return
})
}
type settingType struct {
birdSocket string
listen string
allowedIPs []string
allowedIPs []net.IP
tr_bin string
tr_flags []string
tr_raw bool
}
@@ -62,6 +74,9 @@ var setting settingType
// Wrapper of tracer
func main() {
parseSettings()
tracerouteAutodetect()
fmt.Printf("Listening on %s...\n", setting.listen)
var l net.Listener
var err error

78
proxy/main_test.go Normal file
View File

@@ -0,0 +1,78 @@
package main
import (
"net"
"net/http"
"net/http/httptest"
"testing"
"github.com/magiconair/properties/assert"
)
func TestHasAccessNotConfigured(t *testing.T) {
setting.allowedIPs = []net.IP{}
assert.Equal(t, hasAccess("whatever"), true)
}
func TestHasAccessAllowIPv4(t *testing.T) {
setting.allowedIPs = []net.IP{net.ParseIP("1.2.3.4")}
assert.Equal(t, hasAccess("1.2.3.4:4321"), true)
}
func TestHasAccessDenyIPv4(t *testing.T) {
setting.allowedIPs = []net.IP{net.ParseIP("4.3.2.1")}
assert.Equal(t, hasAccess("1.2.3.4:4321"), false)
}
func TestHasAccessAllowIPv6(t *testing.T) {
setting.allowedIPs = []net.IP{net.ParseIP("2001:db8::1")}
assert.Equal(t, hasAccess("[2001:db8::1]:4321"), true)
}
func TestHasAccessAllowIPv6DifferentForm(t *testing.T) {
setting.allowedIPs = []net.IP{net.ParseIP("2001:0db8::1")}
assert.Equal(t, hasAccess("[2001:db8::1]:4321"), true)
}
func TestHasAccessDenyIPv6(t *testing.T) {
setting.allowedIPs = []net.IP{net.ParseIP("2001:db8::2")}
assert.Equal(t, hasAccess("[2001:db8::1]:4321"), false)
}
func TestHasAccessBadClientIP(t *testing.T) {
setting.allowedIPs = []net.IP{net.ParseIP("1.2.3.4")}
assert.Equal(t, hasAccess("not an IP"), false)
}
func TestHasAccessBadClientIPPort(t *testing.T) {
setting.allowedIPs = []net.IP{net.ParseIP("1.2.3.4")}
assert.Equal(t, hasAccess("not an IP:not a port"), false)
}
func TestAccessHandlerAllow(t *testing.T) {
baseHandler := http.NotFoundHandler()
wrappedHandler := accessHandler(baseHandler)
r := httptest.NewRequest(http.MethodGet, "/mock", nil)
r.RemoteAddr = "1.2.3.4:4321"
w := httptest.NewRecorder()
setting.allowedIPs = []net.IP{net.ParseIP("1.2.3.4")}
wrappedHandler.ServeHTTP(w, r)
assert.Equal(t, w.Code, http.StatusNotFound)
}
func TestAccessHandlerDeny(t *testing.T) {
baseHandler := http.NotFoundHandler()
wrappedHandler := accessHandler(baseHandler)
r := httptest.NewRequest(http.MethodGet, "/mock", nil)
r.RemoteAddr = "1.2.3.4:4321"
w := httptest.NewRecorder()
setting.allowedIPs = []net.IP{net.ParseIP("4.3.2.1")}
wrappedHandler.ServeHTTP(w, r)
assert.Equal(t, w.Code, http.StatusInternalServerError)
}

View File

@@ -2,18 +2,21 @@ package main
import (
"fmt"
"net"
"strings"
"github.com/google/shlex"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
type viperSettingType struct {
BirdSocket string `mapstructure:"bird_socket"`
Listen string `mapstructure:"listen"`
AllowedIPs string `mapstructure:"allowed_ips"`
TracerouteBin string `mapstructure:"traceroute_bin"`
TracerouteRaw bool `mapstructure:"traceroute_raw"`
BirdSocket string `mapstructure:"bird_socket"`
Listen string `mapstructure:"listen"`
AllowedIPs string `mapstructure:"allowed_ips"`
TracerouteBin string `mapstructure:"traceroute_bin"`
TracerouteFlags string `mapstructure:"traceroute_flags"`
TracerouteRaw bool `mapstructure:"traceroute_raw"`
}
// Parse settings with viper, and convert to legacy setting format
@@ -21,6 +24,7 @@ func parseSettings() {
viper.AddConfigPath(".")
viper.AddConfigPath("/etc/bird-lg")
viper.SetConfigName("bird-lgproxy")
viper.AllowEmptyEnv(true)
viper.AutomaticEnv()
viper.SetEnvPrefix("birdlg")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
@@ -39,9 +43,12 @@ func parseSettings() {
pflag.String("allowed", "", "IPs allowed to access this proxy, separated by commas. Don't set to allow all IPs.")
viper.BindPFlag("allowed_ips", pflag.Lookup("allowed"))
pflag.String("traceroute_bin", "traceroute", "traceroute binary file, set either in parameter or environment variable BIRDLG_TRACEROUTE_BIN")
pflag.String("traceroute_bin", "", "traceroute binary file, set either in parameter or environment variable BIRDLG_TRACEROUTE_BIN")
viper.BindPFlag("traceroute_bin", pflag.Lookup("traceroute_bin"))
pflag.String("traceroute_flags", "", "traceroute flags, supports multiple flags separated with space.")
viper.BindPFlag("traceroute_flags", pflag.Lookup("traceroute_flags"))
pflag.Bool("traceroute_raw", false, "whether to display traceroute outputs raw; set via parameter or environment variable BIRDLG_TRACEROUTE_RAW")
viper.BindPFlag("traceroute_raw", pflag.Lookup("traceroute_raw"))
@@ -60,12 +67,26 @@ func parseSettings() {
setting.listen = viperSettings.Listen
if viperSettings.AllowedIPs != "" {
setting.allowedIPs = strings.Split(viperSettings.AllowedIPs, ",")
for _, ip := range strings.Split(viperSettings.AllowedIPs, ",") {
ipObject := net.ParseIP(ip)
if ipObject == nil {
fmt.Printf("Parse IP %s failed\n", ip)
continue
}
setting.allowedIPs = append(setting.allowedIPs, ipObject)
}
} else {
setting.allowedIPs = []string{""}
setting.allowedIPs = []net.IP{}
}
var err error
setting.tr_bin = viperSettings.TracerouteBin
setting.tr_flags, err = shlex.Split(viperSettings.TracerouteFlags)
if err != nil {
panic(err)
}
setting.tr_raw = viperSettings.TracerouteRaw
fmt.Printf("%#v\n", setting)

Some files were not shown because too many files have changed in this diff Show More