CI / changes (push) Successful in 11s
CI / commitlint (push) Has been skipped
CI / openapi (push) Successful in 29s
CI / web (push) Successful in 1m6s
CI / go (push) Successful in 1m23s
CI / bird2 (push) Successful in 17s
CI / release (push) Successful in 4m42s
Enhanced the firewall client functionality by introducing packet statistics tracking, including the cumulative count of packets dropped and accepted. Updated the API to support these new fields and modified the database schema accordingly. Improved the firewall scripts to collect and report packet statistics, ensuring better visibility into client performance. Adjusted the UI components to display packet counts in the clients table, enhancing user experience and monitoring capabilities.
305 lines
9.8 KiB
Go
305 lines
9.8 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"evobgp/internal/authkey"
|
|
"evobgp/internal/store"
|
|
)
|
|
|
|
func TestFirewallEnrollAndBlocklist(t *testing.T) {
|
|
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer srv.Close()
|
|
tenant, _, _, _, _ := srv.Store().DemoIDs()
|
|
mustSetTestAPIKeys(t, srv, "opkey|"+tenant+"|operator")
|
|
|
|
ts := httptest.NewServer(srv.Handler())
|
|
defer ts.Close()
|
|
client := ts.Client()
|
|
|
|
tok := "evobgp_fw_testtoken123456789012345678901234"
|
|
enrollBody := `{"name":"web-01","hostname":"web-01.local","client_token":"` + tok + `","client_version":"test/1"}`
|
|
reqEnroll, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/firewall/enroll", strings.NewReader(enrollBody))
|
|
reqEnroll.Header.Set("Content-Type", "application/json")
|
|
reqEnroll.Header.Set("X-EvoBGP-Seed", testBundleSeed)
|
|
respEnroll, err := client.Do(reqEnroll)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respEnroll.Body.Close() }()
|
|
if respEnroll.StatusCode != http.StatusCreated {
|
|
b, _ := io.ReadAll(respEnroll.Body)
|
|
t.Fatalf("enroll status=%d body=%s", respEnroll.StatusCode, b)
|
|
}
|
|
var enroll map[string]any
|
|
if err := json.NewDecoder(respEnroll.Body).Decode(&enroll); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
clientID, _ := enroll["client_id"].(string)
|
|
if clientID == "" {
|
|
t.Fatal("missing client_id")
|
|
}
|
|
|
|
reqBlock, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/blocklist", nil)
|
|
reqBlock.Header.Set("Authorization", "Bearer "+tok)
|
|
respBlock, err := client.Do(reqBlock)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respBlock.Body.Close() }()
|
|
if respBlock.StatusCode != http.StatusForbidden {
|
|
t.Fatalf("pending blocklist want 403 got %d", respBlock.StatusCode)
|
|
}
|
|
|
|
reqApprove, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/firewall/clients/"+clientID+"/approve", nil)
|
|
reqApprove.Header.Set("Authorization", "Bearer opkey")
|
|
respApprove, err := client.Do(reqApprove)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respApprove.Body.Close() }()
|
|
if respApprove.StatusCode != http.StatusOK {
|
|
b, _ := io.ReadAll(respApprove.Body)
|
|
t.Fatalf("approve status=%d body=%s", respApprove.StatusCode, b)
|
|
}
|
|
|
|
_, err = srv.Store().CreateFirewallRule(tenant, nil, &store.FirewallRuleCreate{Action: "accept", Comment: "default"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
reqBlock2, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/blocklist", nil)
|
|
reqBlock2.Header.Set("Authorization", "Bearer "+tok)
|
|
respBlock2, err := client.Do(reqBlock2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respBlock2.Body.Close() }()
|
|
if respBlock2.StatusCode != http.StatusOK {
|
|
b, _ := io.ReadAll(respBlock2.Body)
|
|
t.Fatalf("blocklist status=%d body=%s", respBlock2.StatusCode, b)
|
|
}
|
|
var bl map[string]any
|
|
if err := json.NewDecoder(respBlock2.Body).Decode(&bl); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if total, _ := bl["total"].(float64); total != 0 {
|
|
t.Fatalf("accept-only want empty blocklist, total=%v", total)
|
|
}
|
|
|
|
reportBody := `{"status":"ok","prefix_count":0,"ip_count":0,"packets_dropped":42,"packets_accepted":1000,"source":"cp","kernel_method":"nft"}`
|
|
reqReport, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/firewall/apply-report", strings.NewReader(reportBody))
|
|
reqReport.Header.Set("Authorization", "Bearer "+tok)
|
|
reqReport.Header.Set("Content-Type", "application/json")
|
|
respReport, err := client.Do(reqReport)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respReport.Body.Close() }()
|
|
if respReport.StatusCode != http.StatusOK {
|
|
b, _ := io.ReadAll(respReport.Body)
|
|
t.Fatalf("apply-report status=%d body=%s", respReport.StatusCode, b)
|
|
}
|
|
gotClient, err := srv.Store().GetFirewallClient(tenant, clientID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gotClient.LastApplyPacketsDropped != 42 || gotClient.LastApplyPacketsAccepted != 1000 {
|
|
t.Fatalf("packet stats dropped=%d accepted=%d", gotClient.LastApplyPacketsDropped, gotClient.LastApplyPacketsAccepted)
|
|
}
|
|
}
|
|
|
|
func TestFirewallEnrollBadSeed(t *testing.T) {
|
|
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer srv.Close()
|
|
ts := httptest.NewServer(srv.Handler())
|
|
defer ts.Close()
|
|
|
|
body := `{"name":"x","client_token":"evobgp_fw_` + strings.Repeat("a", 40) + `"}`
|
|
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/firewall/enroll", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-EvoBGP-Seed", "deadbeef")
|
|
resp, err := ts.Client().Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode != http.StatusForbidden {
|
|
t.Fatalf("want 403 got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestFirewallInstallScriptPublic(t *testing.T) {
|
|
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer srv.Close()
|
|
ts := httptest.NewServer(srv.Handler())
|
|
defer ts.Close()
|
|
|
|
for _, path := range []string{"/v1/firewall/install.sh", "/v1/firewall/sync-script"} {
|
|
req, _ := http.NewRequest(http.MethodGet, ts.URL+path, nil)
|
|
resp, err := ts.Client().Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
func() {
|
|
defer func() { _ = resp.Body.Close() }()
|
|
if resp.StatusCode != http.StatusOK {
|
|
b, _ := io.ReadAll(resp.Body)
|
|
t.Fatalf("%s status=%d body=%s", path, resp.StatusCode, b)
|
|
}
|
|
ct := resp.Header.Get("Content-Type")
|
|
if !strings.Contains(ct, "shellscript") {
|
|
t.Fatalf("%s content-type=%q", path, ct)
|
|
}
|
|
b, _ := io.ReadAll(resp.Body)
|
|
if !strings.HasPrefix(string(b), "#!/") {
|
|
t.Fatalf("%s missing shebang", path)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
func TestFirewallInstallContext(t *testing.T) {
|
|
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer srv.Close()
|
|
tenant, _, _, _, _ := srv.Store().DemoIDs()
|
|
mustSetTestAPIKeys(t, srv, "opkey|"+tenant+"|operator,vwkey|"+tenant+"|viewer")
|
|
|
|
ts := httptest.NewServer(srv.Handler())
|
|
defer ts.Close()
|
|
client := ts.Client()
|
|
|
|
reqOp, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/install-context", nil)
|
|
reqOp.Header.Set("Authorization", "Bearer opkey")
|
|
respOp, err := client.Do(reqOp)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respOp.Body.Close() }()
|
|
if respOp.StatusCode != http.StatusOK {
|
|
b, _ := io.ReadAll(respOp.Body)
|
|
t.Fatalf("operator install-context status=%d body=%s", respOp.StatusCode, b)
|
|
}
|
|
var ctx map[string]any
|
|
if err := json.NewDecoder(respOp.Body).Decode(&ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if seed, _ := ctx["bundle_seed"].(string); seed != testBundleSeed {
|
|
t.Fatalf("bundle_seed=%q want %q", seed, testBundleSeed)
|
|
}
|
|
if configured, _ := ctx["bundle_seed_configured"].(bool); !configured {
|
|
t.Fatal("bundle_seed_configured want true")
|
|
}
|
|
if url, _ := ctx["suggested_cp_url"].(string); !strings.HasPrefix(url, "https://") {
|
|
t.Fatalf("suggested_cp_url=%q want https", url)
|
|
}
|
|
if url, _ := ctx["install_sh_url"].(string); !strings.HasPrefix(url, "https://") || !strings.HasSuffix(url, "/v1/firewall/install.sh") {
|
|
t.Fatalf("install_sh_url=%q", url)
|
|
}
|
|
|
|
reqVw, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/install-context", nil)
|
|
reqVw.Header.Set("Authorization", "Bearer vwkey")
|
|
respVw, err := client.Do(reqVw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respVw.Body.Close() }()
|
|
if respVw.StatusCode != http.StatusForbidden {
|
|
t.Fatalf("viewer install-context want 403 got %d", respVw.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestFirewallDeletePendingClient(t *testing.T) {
|
|
srv, err := New(Options{SeedDemo: true, BundleSeedHex: testBundleSeed})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer srv.Close()
|
|
tenant, _, _, _, _ := srv.Store().DemoIDs()
|
|
mustSetTestAPIKeys(t, srv, "opkey|"+tenant+"|operator")
|
|
|
|
ts := httptest.NewServer(srv.Handler())
|
|
defer ts.Close()
|
|
client := ts.Client()
|
|
|
|
tok := "evobgp_fw_revoketest123456789012345678901"
|
|
enrollBody := `{"name":"reject-me","hostname":"test.local","client_token":"` + tok + `","client_version":"test/1"}`
|
|
reqEnroll, _ := http.NewRequest(http.MethodPost, ts.URL+"/v1/firewall/enroll", strings.NewReader(enrollBody))
|
|
reqEnroll.Header.Set("Content-Type", "application/json")
|
|
reqEnroll.Header.Set("X-EvoBGP-Seed", testBundleSeed)
|
|
respEnroll, err := client.Do(reqEnroll)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respEnroll.Body.Close() }()
|
|
if respEnroll.StatusCode != http.StatusCreated {
|
|
b, _ := io.ReadAll(respEnroll.Body)
|
|
t.Fatalf("enroll status=%d body=%s", respEnroll.StatusCode, b)
|
|
}
|
|
var enroll map[string]any
|
|
if err := json.NewDecoder(respEnroll.Body).Decode(&enroll); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
clientID, _ := enroll["client_id"].(string)
|
|
if clientID == "" {
|
|
t.Fatal("missing client_id")
|
|
}
|
|
|
|
reqDelete, _ := http.NewRequest(http.MethodDelete, ts.URL+"/v1/firewall/clients/"+clientID, nil)
|
|
reqDelete.Header.Set("Authorization", "Bearer opkey")
|
|
respDelete, err := client.Do(reqDelete)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respDelete.Body.Close() }()
|
|
if respDelete.StatusCode != http.StatusNoContent {
|
|
b, _ := io.ReadAll(respDelete.Body)
|
|
t.Fatalf("delete status=%d body=%s", respDelete.StatusCode, b)
|
|
}
|
|
|
|
_, err = srv.Store().GetFirewallClient(tenant, clientID)
|
|
if err == nil {
|
|
t.Fatal("client should be deleted")
|
|
}
|
|
if !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("delete err=%v", err)
|
|
}
|
|
|
|
reqBlock, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/firewall/blocklist", nil)
|
|
reqBlock.Header.Set("Authorization", "Bearer "+tok)
|
|
respBlock, err := client.Do(reqBlock)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = respBlock.Body.Close() }()
|
|
if respBlock.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("deleted blocklist want 401 got %d", respBlock.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestFirewallTokenHashMatchesAuthkey(t *testing.T) {
|
|
tok := "evobgp_fw_sample"
|
|
h := authkey.HashToken(tok)
|
|
if len(h) != 32 {
|
|
t.Fatalf("hash len %d", len(h))
|
|
}
|
|
}
|