You've already forked bird-lg-go
mirror of
https://github.com/xddxdd/bird-lg-go
synced 2025-10-27 06:42:13 +01:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdc34704b5 | ||
|
|
db58bd3354 | ||
|
|
a0246ccee2 | ||
|
|
ccd14af0c8 | ||
|
|
594ca80f50 | ||
|
|
5625058e71 | ||
|
|
7efa3237a9 | ||
|
|
7b0c8c0556 | ||
|
|
ffd9165062 | ||
|
|
24fd5203e8 | ||
|
|
49a05767c1 | ||
|
|
e7010f75f8 | ||
|
|
dba2af7634 | ||
|
|
049775319b |
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -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 }}
|
||||
|
||||
26
README.md
26
README.md
@@ -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
|
||||
|
||||
@@ -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
207
frontend/api_test.go
Normal 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
83
frontend/asn_cache.go
Normal 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
|
||||
}
|
||||
52
frontend/asn_cache_test.go
Normal file
52
frontend/asn_cache_test.go
Normal 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
173
frontend/bgpmap_graph.go
Normal 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"
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
287
frontend/go.sum
287
frontend/go.sum
File diff suppressed because it is too large
Load Diff
@@ -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
163
frontend/lgproxy_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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"}
|
||||
|
||||
|
||||
@@ -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("-", "_", ".", "_"))
|
||||
|
||||
8
frontend/settings_test.go
Normal file
8
frontend/settings_test.go
Normal 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
25
frontend/template_test.go
Normal 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)
|
||||
}
|
||||
151
frontend/test_data/bgpmap_case1.txt
Normal file
151
frontend/test_data/bgpmap_case1.txt
Normal 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)
|
||||
89
frontend/webserver_test.go
Normal file
89
frontend/webserver_test.go
Normal 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)
|
||||
}
|
||||
@@ -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])
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
213
proxy/bird_test.go
Normal 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")
|
||||
}
|
||||
23
proxy/go.mod
23
proxy/go.mod
@@ -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
|
||||
)
|
||||
|
||||
283
proxy/go.sum
283
proxy/go.sum
File diff suppressed because it is too large
Load Diff
@@ -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
78
proxy/main_test.go
Normal 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)
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user