mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-04 04:34:07 +09:00
policy/v2: recapture tester rows with failure detail
Re-runs keep SaaS's error data and validate answer. Tester compat checks verdict, message, failure breakdown and node labels.
This commit is contained in:
1 parent
1915feafd5
commit
c69c47409a
96 files changed
+4764
-1110190
No files matched your search
@@ -1,56 +1,66 @@
|
||||
// Compatibility tests for the policy `tests` block, replaying captures
|
||||
// recorded against a real Tailscale SaaS tailnet. The runner mirrors the
|
||||
// pattern in tailscale_grants_compat_test.go: a single Glob over a
|
||||
// testdata directory, one t.Run per file. Each capture is one of:
|
||||
// Compatibility tests for the policy `tests` and `sshTests` blocks, replaying
|
||||
// captures recorded against a real Tailscale SaaS tailnet. Each capture is
|
||||
// checked in steps, see [runTesterCompat]:
|
||||
//
|
||||
// - APIResponseCode != 200 — the policy was rejected by the SaaS, the
|
||||
// captured Message is the byte-exact body the user saw, and headscale
|
||||
// must reject the same input with an error string that contains the
|
||||
// same body (substring match, allowing wrapping like "test(s)
|
||||
// failed:\n…").
|
||||
// - APIResponseCode == 200 — the SaaS accepted the policy (its `tests`
|
||||
// block passed); headscale's RunTests must also pass.
|
||||
// - verdict: SaaS accepted the policy (200) exactly when headscale does;
|
||||
// - message: a refusal's message is contained in headscale's error;
|
||||
// - breakdown: the failures SaaS lists in its error data are the ones
|
||||
// headscale's evaluators report, see [requireFailureBreakdown];
|
||||
// - label: no node hostname appears in one report and not the other.
|
||||
//
|
||||
// Captures live in testdata/policytest_results/*.hujson. Scenarios in
|
||||
// knownPolicyTesterDivergences are skipped with their tracking note —
|
||||
// these are real Tailscale ↔ headscale divergences uncovered by the
|
||||
// captures that need engine-level fixes in follow-up PRs.
|
||||
// Captures live in testdata/policytest_results/*.hujson; the sshTests ones
|
||||
// are replayed by sshtester_compat_test.go.
|
||||
//
|
||||
// Source format: github.com/juanfont/headscale/hscontrol/types/testcapture
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/juanfont/headscale/hscontrol/types"
|
||||
"github.com/juanfont/headscale/hscontrol/types/testcapture"
|
||||
"github.com/stretchr/testify/require"
|
||||
tsapi "tailscale.com/client/tailscale/v2"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// knownPolicyTesterDivergences lists scenarios where headscale's evaluator
|
||||
// disagrees with Tailscale SaaS on whether the policy should be accepted.
|
||||
// Each entry is a real bug to fix in a follow-up; documenting them here
|
||||
// keeps the compat suite green and the divergence list visible.
|
||||
var knownPolicyTesterDivergences = map[string]string{} //nolint:gosec // strings here are human-readable notes, not credentials
|
||||
// knownPolicyTesterDivergences lists checks where headscale's evaluator
|
||||
// differs from Tailscale SaaS, keyed <test_id>/<check>.
|
||||
var knownPolicyTesterDivergences = map[string]string{ //nolint:gosec // strings here are human-readable notes, not credentials
|
||||
"policytest-accept-fail-autogroup-tagged-src-tag-dst/breakdown": "TEST_SRC_SHAPE: SaaS reports autogroup:tagged as an unknown tests source; headscale evaluates it",
|
||||
|
||||
"policytest-accept-fail-proto-icmp-not-allowed/breakdown": testEntryErrors,
|
||||
"policytest-proto-icmp/breakdown": testEntryErrors,
|
||||
|
||||
"policytest-dst-unknown-group/breakdown": testUnknownAlias,
|
||||
"policytest-src-unknown-group/breakdown": testUnknownAlias,
|
||||
"policytest-src-unknown-user-email/breakdown": testUnknownAlias,
|
||||
}
|
||||
|
||||
const (
|
||||
testEntryErrors = "TEST_ENTRY_ERRORS: SaaS reports the first shape error of a tests entry, headscale every one"
|
||||
testUnknownAlias = "TEST_UNKNOWN_ALIAS: SaaS reports a test naming an unknown user or group as not reached, headscale as an evaluation error"
|
||||
)
|
||||
|
||||
// TestPolicyTesterCompat replays every capture under
|
||||
// testdata/policytest_results/ against the engine. With no captures the
|
||||
// test is a no-op — committed early so the layout/wiring lands before
|
||||
// the bulk import.
|
||||
// testdata/policytest_results/ against the engine.
|
||||
func TestPolicyTesterCompat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files, err := filepath.Glob(filepath.Join("testdata", "policytest_results", "*.hujson"))
|
||||
require.NoError(t, err, "failed to glob test files")
|
||||
|
||||
if len(files) == 0 {
|
||||
t.Skip("no policytest captures yet")
|
||||
}
|
||||
|
||||
users := setupSSHDataCompatUsers()
|
||||
require.NotEmpty(t, files, "no policytest captures")
|
||||
|
||||
for _, file := range files {
|
||||
c, err := testcapture.Read(file)
|
||||
@@ -59,57 +69,250 @@ func TestPolicyTesterCompat(t *testing.T) {
|
||||
t.Run(c.TestID, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if reason, skip := knownPolicyTesterDivergences[c.TestID]; skip {
|
||||
t.Skip(reason)
|
||||
}
|
||||
|
||||
// Each capture pins its own topology IPs; range and
|
||||
// address tests mean nothing against other nodes.
|
||||
nodes := buildGrantsNodesFromCapture(users, c)
|
||||
|
||||
policyJSON := []byte(c.Input.FullPolicy)
|
||||
|
||||
pm, parseErr := NewPolicyManager(policyJSON, users, nodes.ViewSlice())
|
||||
|
||||
// Tailscale validates and runs tests as one POST step:
|
||||
// either failure mode produces the same 400. Headscale
|
||||
// splits structural validation (parse) from test
|
||||
// evaluation (SetPolicy). For the compat assertion, the
|
||||
// two are equivalent — whichever surfaces first carries
|
||||
// the captured body.
|
||||
if c.Input.APIResponseCode == 200 {
|
||||
require.NoError(t, parseErr, "tailscale accepted this policy; headscale must parse it")
|
||||
|
||||
_, setErr := pm.SetPolicy(policyJSON)
|
||||
require.NoError(t, setErr, "tailscale accepted this policy; headscale tests should pass")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var got error
|
||||
|
||||
switch {
|
||||
case parseErr != nil:
|
||||
got = parseErr
|
||||
default:
|
||||
_, setErr := pm.SetPolicy(policyJSON)
|
||||
got = setErr
|
||||
}
|
||||
|
||||
require.Error(t, got, "tailscale rejected; headscale must reject too")
|
||||
|
||||
if c.Input.APIResponseBody == nil || c.Input.APIResponseBody.Message == "" {
|
||||
return
|
||||
}
|
||||
|
||||
want := c.Input.APIResponseBody.Message
|
||||
if !strings.Contains(got.Error(), want) {
|
||||
t.Errorf("error body mismatch\n tailscale wants: %q\n headscale got: %q", want, got.Error())
|
||||
}
|
||||
runTesterCompat(t, c, knownPolicyTesterDivergences)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// runTesterCompat replays one tests or sshTests capture. SaaS validates the
|
||||
// policy and runs both blocks in one POST; headscale parses first and runs
|
||||
// the blocks in SetPolicy, so an error from either step is its refusal.
|
||||
func runTesterCompat(t *testing.T, c *testcapture.Capture, skips map[string]string) {
|
||||
t.Helper()
|
||||
|
||||
users := setupSSHDataCompatUsers()
|
||||
|
||||
// Each capture pins its own topology IPs; range and address tests mean
|
||||
// nothing against other nodes.
|
||||
nodes := buildGrantsNodesFromCapture(users, c)
|
||||
policyJSON := []byte(c.Input.FullPolicy)
|
||||
|
||||
pm, err := NewPolicyManager(policyJSON, users, nodes.ViewSlice())
|
||||
if err == nil {
|
||||
_, err = pm.SetPolicy(policyJSON)
|
||||
} else {
|
||||
pm = nil
|
||||
}
|
||||
|
||||
var body testcapture.APIResponseBody
|
||||
if c.Input.APIResponseBody != nil {
|
||||
body = *c.Input.APIResponseBody
|
||||
}
|
||||
|
||||
saasRefused := c.Input.APIResponseCode != http.StatusOK
|
||||
key := c.TestID + "/"
|
||||
|
||||
compatCheck(t, skips, key+"verdict", func() string {
|
||||
switch {
|
||||
case saasRefused && err == nil:
|
||||
return fmt.Sprintf("SaaS refused with %q; headscale accepted", body.Message)
|
||||
case !saasRefused && err != nil:
|
||||
return fmt.Sprintf("SaaS accepted; headscale refused: %v", err)
|
||||
}
|
||||
|
||||
return ""
|
||||
})
|
||||
|
||||
if !saasRefused || err == nil {
|
||||
compatChecksNotRun(t, skips, key+"message", key+"breakdown", key+"label")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
compatCheck(t, skips, key+"message", func() string {
|
||||
if strings.Contains(err.Error(), body.Message) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("tailscale wants: %q\nheadscale got: %q", body.Message, err.Error())
|
||||
})
|
||||
|
||||
if len(body.Data) == 0 {
|
||||
compatChecksNotRun(t, skips, key+"breakdown", key+"label")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
requireFailureBreakdown(t, skips, key+"breakdown", body.Data, c.Input.FullPolicy, pm, err)
|
||||
|
||||
compatCheck(t, skips, key+"label", func() string {
|
||||
return cmp.Diff(nodeLabels(c, fmt.Sprint(body.Data)), nodeLabels(c, err.Error()))
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
// [acl test error]: address "tag:server:22" (protocol "tcp"): want: Accept, got: Drop.
|
||||
saasTestFailRE = regexp.MustCompile(
|
||||
`^\[acl test error\]: address "(.+)" \(protocol "[^"]*"\): want: (Accept|Drop), got: (?:Accept|Drop)$`)
|
||||
|
||||
// [ssh test error]: src "a" at "ip", dst "b" at "ip", ssh user "root": want: "accept" got: "deny".
|
||||
saasSSHTestFailRE = regexp.MustCompile(
|
||||
`^\[ssh test error\]: src ".*" at ".*", dst ".*" at ".*", ssh user "(.*)": want: "(accept|check|deny)" got: "[a-z]+"$`)
|
||||
)
|
||||
|
||||
// requireFailureBreakdown compares the failures SaaS lists in its error data
|
||||
// with the ones headscale's evaluators report, as sorted lines of
|
||||
// "src -> dst: want" (tests), "src/user: want" (sshTests) and "src: error"
|
||||
// for an entry that could not be evaluated. Repeats are kept. sshTests lines
|
||||
// leave the destination out: SaaS names the alias, headscale each node, and
|
||||
// the label check covers that.
|
||||
func requireFailureBreakdown(
|
||||
t *testing.T,
|
||||
skips map[string]string,
|
||||
key string,
|
||||
data []tsapi.APIErrorData,
|
||||
policy string,
|
||||
pm *PolicyManager,
|
||||
refusal error,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
var want []string
|
||||
|
||||
for _, d := range data {
|
||||
for _, line := range d.Errors {
|
||||
if m := saasTestFailRE.FindStringSubmatch(line); m != nil {
|
||||
want = append(want, fmt.Sprintf("%s -> %s: %s", d.User, m[1], testWant(m[2] == "Accept")))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if m := saasSSHTestFailRE.FindStringSubmatch(line); m != nil {
|
||||
want = append(want, fmt.Sprintf("%s/%s: %s", d.User, m[1], m[2]))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
want = append(want, d.User+": error")
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(want)
|
||||
|
||||
compatCheck(t, skips, key, func() string {
|
||||
if pm != nil {
|
||||
return cmp.Diff(want, testerBreakdown(pm))
|
||||
}
|
||||
|
||||
got, err := parseRefusalBreakdown(policy, refusal)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
return cmp.Diff(want, got)
|
||||
})
|
||||
}
|
||||
|
||||
// parseRefusalEntryRE finds the entries a parse-time refusal names; the
|
||||
// parser reports tests and sshTests entries by index.
|
||||
var parseRefusalEntryRE = regexp.MustCompile(`(?m)^\s*(test|sshTest) (\d+)\b`)
|
||||
|
||||
// parseRefusalBreakdown renders a refusal headscale gave before running the
|
||||
// tests: one "src: error" per entry it names.
|
||||
func parseRefusalBreakdown(policy string, refusal error) ([]string, error) {
|
||||
type entry struct {
|
||||
Src string `json:"src"`
|
||||
}
|
||||
|
||||
var raw struct {
|
||||
Tests []entry `json:"tests"`
|
||||
SSHTests []entry `json:"sshTests"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(policy), &raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading test entries: %w", err)
|
||||
}
|
||||
|
||||
var out []string
|
||||
|
||||
for _, m := range parseRefusalEntryRE.FindAllStringSubmatch(refusal.Error(), -1) {
|
||||
entries := raw.Tests
|
||||
if m[1] == "sshTest" {
|
||||
entries = raw.SSHTests
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(m[2])
|
||||
if err != nil || i >= len(entries) {
|
||||
return nil, fmt.Errorf("refusal names %s %s, which the policy does not have: %w", m[1], m[2], refusal)
|
||||
}
|
||||
|
||||
out = append(out, entries[i].Src+": error")
|
||||
}
|
||||
|
||||
slices.Sort(out)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func testWant(accept bool) string {
|
||||
if accept {
|
||||
return "accept"
|
||||
}
|
||||
|
||||
return "deny"
|
||||
}
|
||||
|
||||
// testerBreakdown runs both blocks as the boot path does and renders the
|
||||
// failures as [requireFailureBreakdown] does.
|
||||
func testerBreakdown(pm *PolicyManager) []string {
|
||||
var out []string
|
||||
|
||||
for _, res := range runPolicyTests(pm.pol, pm.filter, pm.users, pm.nodes).Results {
|
||||
for range res.Errors {
|
||||
out = append(out, res.Src+": error")
|
||||
}
|
||||
|
||||
for _, dst := range res.AcceptFail {
|
||||
out = append(out, fmt.Sprintf("%s -> %s: %s", res.Src, dst, testWant(true)))
|
||||
}
|
||||
|
||||
for _, dst := range res.DenyFail {
|
||||
out = append(out, fmt.Sprintf("%s -> %s: %s", res.Src, dst, testWant(false)))
|
||||
}
|
||||
}
|
||||
|
||||
cache := make(map[types.NodeID]*tailcfg.SSHPolicy)
|
||||
|
||||
for _, res := range runSSHPolicyTests(pm.pol, pm.users, pm.nodes, cache).Results {
|
||||
for range res.Errors {
|
||||
out = append(out, res.Src+": error")
|
||||
}
|
||||
|
||||
for want, fails := range map[string]map[string][]string{
|
||||
"accept": res.AcceptFail,
|
||||
"deny": res.DenyFail,
|
||||
"check": res.CheckFail,
|
||||
} {
|
||||
for user, dsts := range fails {
|
||||
for range dsts {
|
||||
out = append(out, fmt.Sprintf("%s/%s: %s", res.Src, user, want))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(out)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// nodeLabels lists the capture's hostnames that text names while the policy
|
||||
// does not, so they are there only because the report labels nodes.
|
||||
func nodeLabels(c *testcapture.Capture, text string) []string {
|
||||
var out []string
|
||||
|
||||
for _, n := range c.Topology.Nodes {
|
||||
if !strings.Contains(c.Input.FullPolicy, n.Hostname) && strings.Contains(text, n.Hostname) {
|
||||
out = append(out, n.Hostname)
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(out)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// compatCheck runs one check of a capture as a subtest named after the last
|
||||
// element of key; mismatch returns "" when headscale matches SaaS. A key in
|
||||
// skips is a known difference that must still differ, so the fix that makes
|
||||
@@ -131,3 +334,15 @@ func compatCheck(t *testing.T, skips map[string]string, key string, mismatch fun
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// compatChecksNotRun fails for a listed check that cannot run, since its
|
||||
// entry could never go stale.
|
||||
func compatChecksNotRun(t *testing.T, skips map[string]string, keys ...string) {
|
||||
t.Helper()
|
||||
|
||||
for _, key := range keys {
|
||||
if _, listed := skips[key]; listed {
|
||||
t.Errorf("check %q does not run for this capture; delete its skip entry", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,38 @@
|
||||
// Replay golden HuJSON captures under testdata/sshtest_results/*.hujson:
|
||||
// the 200 path requires headscale's evaluateSSHTests to pass; the
|
||||
// non-200 path requires headscale to reject the same input with the
|
||||
// captured error body as a substring. Divergences are listed in
|
||||
// Replay golden HuJSON captures under testdata/sshtest_results/*.hujson with
|
||||
// the checks of [runTesterCompat]. Divergences are listed in
|
||||
// knownSSHTesterDivergences with the engine gap each represents.
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/juanfont/headscale/hscontrol/types/testcapture"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// knownSSHTesterDivergences names the engine gap for each capture where
|
||||
// headscale and upstream disagree.
|
||||
var knownSSHTesterDivergences = map[string]string{}
|
||||
// knownSSHTesterDivergences names the engine gap for each check where
|
||||
// headscale and upstream disagree, keyed <test_id>/<check>.
|
||||
var knownSSHTesterDivergences = map[string]string{
|
||||
"sshtest-accept-fail-no-ssh-rule/label": sshTestNodeLabel,
|
||||
"sshtest-accept-fail-wrong-login-user/label": sshTestNodeLabel,
|
||||
"sshtest-both-tests-pass-sshTests-fail/label": sshTestNodeLabel,
|
||||
"sshtest-deny-fail-policy-allows/label": sshTestNodeLabel,
|
||||
"sshtest-malformed-empty-user/label": sshTestNodeLabel,
|
||||
"sshtest-mixed-acls-tcp22-allow-no-ssh-rule/label": sshTestNodeLabel,
|
||||
"sshtest-user-localpart-domain-mismatch/label": sshTestNodeLabel,
|
||||
"sshtest-user-nonroot-blocks-root/label": sshTestNodeLabel,
|
||||
}
|
||||
|
||||
const sshTestNodeLabel = "SSHTEST_NODE_LABEL: headscale names each failing dst node by hostname; SaaS names the dst as written"
|
||||
|
||||
func TestSSHTesterCompat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files, err := filepath.Glob(filepath.Join("testdata", "sshtest_results", "*.hujson"))
|
||||
require.NoError(t, err, "failed to glob test files")
|
||||
|
||||
if len(files) == 0 {
|
||||
t.Skip("no sshtest captures yet")
|
||||
}
|
||||
|
||||
users := setupSSHDataCompatUsers()
|
||||
require.NotEmpty(t, files, "no sshtest captures")
|
||||
|
||||
for _, file := range files {
|
||||
c, err := testcapture.Read(file)
|
||||
@@ -38,49 +41,7 @@ func TestSSHTesterCompat(t *testing.T) {
|
||||
t.Run(c.TestID, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if reason, skip := knownSSHTesterDivergences[c.TestID]; skip {
|
||||
t.Skip(reason)
|
||||
}
|
||||
|
||||
// Each capture pins its own topology IPs; build nodes
|
||||
// from the capture so host-alias dsts resolve.
|
||||
nodes := buildGrantsNodesFromCapture(users, c)
|
||||
|
||||
policyJSON := []byte(c.Input.FullPolicy)
|
||||
|
||||
pm, parseErr := NewPolicyManager(policyJSON, users, nodes.ViewSlice())
|
||||
|
||||
if c.Input.APIResponseCode == 200 {
|
||||
require.NoError(t, parseErr,
|
||||
"tailscale accepted this policy; headscale must parse it")
|
||||
|
||||
_, setErr := pm.SetPolicy(policyJSON)
|
||||
require.NoError(t, setErr,
|
||||
"tailscale accepted this policy; headscale sshTests must pass")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var got error
|
||||
|
||||
switch {
|
||||
case parseErr != nil:
|
||||
got = parseErr
|
||||
default:
|
||||
_, setErr := pm.SetPolicy(policyJSON)
|
||||
got = setErr
|
||||
}
|
||||
|
||||
require.Error(t, got, "tailscale rejected; headscale must reject too")
|
||||
|
||||
if c.Input.APIResponseBody == nil || c.Input.APIResponseBody.Message == "" {
|
||||
return
|
||||
}
|
||||
|
||||
want := c.Input.APIResponseBody.Message
|
||||
if !strings.Contains(got.Error(), want) {
|
||||
t.Errorf("error body mismatch\n tailscale wants: %q\n headscale got: %q", want, got.Error())
|
||||
}
|
||||
runTesterCompat(t, c, knownSSHTesterDivergences)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,7 @@ func buildGrantsNodesFromCapture(
|
||||
for _, nodeDef := range tf.Topology.Nodes {
|
||||
node := &types.Node{
|
||||
ID: types.NodeID(autoID), //nolint:gosec
|
||||
Hostname: nodeDef.Hostname,
|
||||
GivenName: nodeDef.Hostname,
|
||||
IPv4: ptrAddr(nodeDef.IPv4),
|
||||
IPv6: ptrAddr(nodeDef.IPv6),
|
||||
|
||||
@@ -437,7 +437,9 @@ func TestSSHCheckParamsMatchesCaptures(t *testing.T) {
|
||||
|
||||
for _, file := range files {
|
||||
tf := loadSSHTestFile(t, file)
|
||||
if tf.Input.APIResponseCode != 200 {
|
||||
|
||||
// Validation-only captures hold no node rules to compare.
|
||||
if tf.Input.APIResponseCode != 200 || len(tf.Captures) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
Generated
Vendored
+49
-8727
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+51
-18587
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-8727
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+59
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+57
-8725
File diff suppressed because it is too large.
Load diff
hscontrol/policy/v2/testdata/policytest_results/policytest-accept-fail-proto-icmp-not-allowed.hujson
Generated
Vendored
+60
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+58
-8725
File diff suppressed because it is too large.
Load diff
hscontrol/policy/v2/testdata/policytest_results/policytest-accept-fail-proto-numeric-mismatch.hujson
Generated
Vendored
+58
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-8727
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+56
-7192
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+46
-7178
File diff suppressed because it is too large.
Load diff
hscontrol/policy/v2/testdata/policytest_results/policytest-allpass-acls-only-tag-src-cidr-dst.hujson
Generated
Vendored
+171
@@ -0,0 +1,171 @@
|
||||
// policytest-allpass-acls-only-tag-src-cidr-dst
|
||||
//
|
||||
// tests block all-pass: tag src to cidr dst, acls only, tcp
|
||||
//
|
||||
// Steps: 1
|
||||
// Captured at: 2026-10-17T15:11:37Z
|
||||
// tool version: tscap/4ef410b
|
||||
// schema version: 1
|
||||
{
|
||||
"schema_version": 1,
|
||||
"test_id": "policytest-allpass-acls-only-tag-src-cidr-dst",
|
||||
"description": "tests block all-pass: tag src to cidr dst, acls only, tcp",
|
||||
"category": "policytest",
|
||||
"captured_at": "2026-10-17T15:11:37.637403038Z",
|
||||
"tool_version": "tscap/4ef410b",
|
||||
"tailnet": "odin@example.com",
|
||||
"error": true,
|
||||
"input": {
|
||||
"api_response_code": 400,
|
||||
"api_response_body": {"message": "test(s) failed", "data": [{"user": "tag:client", "errors": [
|
||||
"[acl test error]: user or host is invalid: unknown user or host: \"10.0.0.0/8\""
|
||||
]}]},
|
||||
"tailnet": {
|
||||
"dns": {"magic_dns": false, "nameservers": [], "search_paths": [], "split_dns": {}},
|
||||
"settings": {}
|
||||
},
|
||||
"scenario_hujson": " \n \n \n \n \n \n \n \n \n \n{\n\t\"id\": \"policytest-allpass-acls-only-tag-src-cidr-dst\",\n\t\"category\": \"policytest\",\n\t\"description\": \"tests block all-pass: tag src to cidr dst, acls only, tcp\",\n\n\t\"topology\": \"../_topologies/acl.hujson\",\n\n\t\"tailnet\": {\n\t\t\"dns\": {\"magic_dns\": false, \"nameservers\": [], \"search_paths\": [], \"split_dns\": {}},\n\t\t\"settings\": {}\n\t},\n\n\t\"policy\": {\n\t\t\"groups\": {\n\t\t\t\"group:admins\": [\"odin@example.com\"],\n\t\t\t\"group:developers\": [\"thor@example.org\", \"odin@example.com\"],\n\t\t\t\"group:empty\": [],\n\t\t\t\"group:monitors\": [\"freya@example.com\"]\n\t\t},\n\t\t\"tagOwners\": {\n\t\t\t\"tag:client\": [\"odin@example.com\"],\n\t\t\t\"tag:exit\": [\"odin@example.com\"],\n\t\t\t\"tag:pidgey\": [\"odin@example.com\"],\n\t\t\t\"tag:pidgeotto\": [\"odin@example.com\"],\n\t\t\t\"tag:group-a\": [\"odin@example.com\"],\n\t\t\t\"tag:group-b\": [\"odin@example.com\"],\n\t\t\t\"tag:ha\": [\"odin@example.com\"],\n\t\t\t\"tag:prod\": [\"odin@example.com\"],\n\t\t\t\"tag:router\": [\"odin@example.com\"],\n\t\t\t\"tag:spearow\": [\"odin@example.com\"],\n\t\t\t\"tag:fearow\": [\"odin@example.com\"],\n\t\t\t\"tag:server\": [\"odin@example.com\"]\n\t\t},\n\t\t\"hosts\": {\n\t\t\t\"internal\": \"10.0.0.0/8\",\n\t\t\t\"prodbox\": \"100.103.8.15\",\n\t\t\t\"subnet24\": \"192.168.1.0/24\",\n\t\t\t\"webserver\": \"100.64.0.16\"\n\t\t},\n\t\t\"acls\": [\n\t\t\t{\"action\": \"accept\", \"src\": [\"tag:client\"], \"dst\": [\"10.0.0.0/8:22\"]}\n\t\t],\n\t\t\"tests\": [\n\t\t\t{\"src\": \"tag:client\", \"accept\": [\"10.0.0.0/8:22\"]}\n\t\t]\n\t}\n}\n",
|
||||
"scenario_path": "scenarios/policytest/policytest-allpass-acls-only-tag-src-cidr-dst.hujson",
|
||||
"full_policy": {
|
||||
"acls": [{"action": "accept", "dst": ["10.0.0.0/8:22"], "src": ["tag:client"]}],
|
||||
"groups": {
|
||||
"group:admins": ["odin@example.com"],
|
||||
"group:developers": ["thor@example.org", "odin@example.com"],
|
||||
"group:empty": [],
|
||||
"group:monitors": ["freya@example.com"]
|
||||
},
|
||||
"hosts": {
|
||||
"internal": "10.0.0.0/8",
|
||||
"prodbox": "100.103.8.15",
|
||||
"subnet24": "192.168.1.0/24",
|
||||
"webserver": "100.64.0.16"
|
||||
},
|
||||
"tagOwners": {
|
||||
"tag:client": ["odin@example.com"],
|
||||
"tag:exit": ["odin@example.com"],
|
||||
"tag:pidgey": ["odin@example.com"],
|
||||
"tag:pidgeotto": ["odin@example.com"],
|
||||
"tag:group-a": ["odin@example.com"],
|
||||
"tag:group-b": ["odin@example.com"],
|
||||
"tag:ha": ["odin@example.com"],
|
||||
"tag:prod": ["odin@example.com"],
|
||||
"tag:router": ["odin@example.com"],
|
||||
"tag:spearow": ["odin@example.com"],
|
||||
"tag:fearow": ["odin@example.com"],
|
||||
"tag:server": ["odin@example.com"]
|
||||
},
|
||||
"tests": [{"accept": ["10.0.0.0/8:22"], "src": "tag:client"}]
|
||||
}
|
||||
},
|
||||
"topology": {"users": [
|
||||
{"id": 1, "name": "odin", "email": "odin@example.com"},
|
||||
{"id": 2, "name": "thor", "email": "thor@example.org"},
|
||||
{"id": 3, "name": "freya", "email": "freya@example.com"}
|
||||
], "nodes": {"beedrill": {
|
||||
"hostname": "beedrill",
|
||||
"tags": ["tag:server"],
|
||||
"ipv4": "100.64.0.16",
|
||||
"ipv6": "fd7a:115c:a1e0::10",
|
||||
"routable_ips": [],
|
||||
"approved_routes": []
|
||||
}, "bulbasaur": {
|
||||
"hostname": "bulbasaur",
|
||||
"tags": [],
|
||||
"ipv4": "100.64.0.19",
|
||||
"ipv6": "fd7a:115c:a1e0::13",
|
||||
"user": "odin",
|
||||
"routable_ips": [],
|
||||
"approved_routes": []
|
||||
}, "charmander": {
|
||||
"hostname": "charmander",
|
||||
"tags": ["tag:exit"],
|
||||
"ipv4": "100.64.0.4",
|
||||
"ipv6": "fd7a:115c:a1e0::4",
|
||||
"routable_ips": ["0.0.0.0/0", "::/0"],
|
||||
"approved_routes": []
|
||||
}, "ivysaur": {
|
||||
"hostname": "ivysaur",
|
||||
"tags": [],
|
||||
"ipv4": "100.64.0.17",
|
||||
"ipv6": "fd7a:115c:a1e0::11",
|
||||
"user": "thor",
|
||||
"routable_ips": [],
|
||||
"approved_routes": []
|
||||
}, "kakuna": {
|
||||
"hostname": "kakuna",
|
||||
"tags": ["tag:prod"],
|
||||
"ipv4": "100.64.0.15",
|
||||
"ipv6": "fd7a:115c:a1e0::f",
|
||||
"routable_ips": [],
|
||||
"approved_routes": []
|
||||
}, "squirtle": {
|
||||
"hostname": "squirtle",
|
||||
"tags": ["tag:router"],
|
||||
"ipv4": "100.64.0.13",
|
||||
"ipv6": "fd7a:115c:a1e0::d",
|
||||
"routable_ips": ["10.33.0.0/16"],
|
||||
"approved_routes": []
|
||||
}, "venusaur": {
|
||||
"hostname": "venusaur",
|
||||
"tags": [],
|
||||
"ipv4": "100.64.0.18",
|
||||
"ipv6": "fd7a:115c:a1e0::12",
|
||||
"user": "freya",
|
||||
"routable_ips": [],
|
||||
"approved_routes": []
|
||||
}, "weedle": {
|
||||
"hostname": "weedle",
|
||||
"tags": ["tag:client"],
|
||||
"ipv4": "100.64.0.14",
|
||||
"ipv6": "fd7a:115c:a1e0::e",
|
||||
"routable_ips": [],
|
||||
"approved_routes": []
|
||||
}}},
|
||||
"captures": {},
|
||||
"steps": [{
|
||||
"name": "validate",
|
||||
"at": "2026-10-17T15:11:43.60650233Z",
|
||||
"result": "observed",
|
||||
"actions": [{
|
||||
"kind": "api",
|
||||
"principal": "admin",
|
||||
"method": "POST",
|
||||
"path": "/tailnet/-/acl/validate",
|
||||
"header": {"Content-Type": "application/hujson"},
|
||||
"body": {
|
||||
"acls": [{"action": "accept", "dst": ["10.0.0.0/8:22"], "src": ["tag:client"]}],
|
||||
"groups": {
|
||||
"group:admins": ["odin@example.com"],
|
||||
"group:developers": ["thor@example.org", "odin@example.com"],
|
||||
"group:empty": [],
|
||||
"group:monitors": ["freya@example.com"]
|
||||
},
|
||||
"hosts": {
|
||||
"internal": "10.0.0.0/8",
|
||||
"prodbox": "100.103.8.15",
|
||||
"subnet24": "192.168.1.0/24",
|
||||
"webserver": "100.64.0.16"
|
||||
},
|
||||
"tagOwners": {
|
||||
"tag:client": ["odin@example.com"],
|
||||
"tag:exit": ["odin@example.com"],
|
||||
"tag:pidgey": ["odin@example.com"],
|
||||
"tag:pidgeotto": ["odin@example.com"],
|
||||
"tag:group-a": ["odin@example.com"],
|
||||
"tag:group-b": ["odin@example.com"],
|
||||
"tag:ha": ["odin@example.com"],
|
||||
"tag:prod": ["odin@example.com"],
|
||||
"tag:router": ["odin@example.com"],
|
||||
"tag:spearow": ["odin@example.com"],
|
||||
"tag:fearow": ["odin@example.com"],
|
||||
"tag:server": ["odin@example.com"]
|
||||
},
|
||||
"tests": [{"accept": ["10.0.0.0/8:22"], "src": "tag:client"}]
|
||||
},
|
||||
"status": 200,
|
||||
"response": {"data": [{"errors": [
|
||||
"[acl test error]: user or host is invalid: unknown user or host: \"10.0.0.0/8\""
|
||||
], "user": "tag:client"}], "message": "test(s) failed"}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
Generated
Vendored
+48
-7128
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+171
@@ -0,0 +1,171 @@
|
||||
// policytest-allpass-grants-only-tag-src-cidr-dst-cgnat
|
||||
//
|
||||
// tests block all-pass: tag src to cgnat-cidr dst (contains topology nodes), grants only, tcp
|
||||
//
|
||||
// Steps: 1
|
||||
// Captured at: 2026-10-17T15:11:57Z
|
||||
// tool version: tscap/4ef410b
|
||||
// schema version: 1
|
||||
{
|
||||
"schema_version": 1,
|
||||
"test_id": "policytest-allpass-grants-only-tag-src-cidr-dst-cgnat",
|
||||
"description": "tests block all-pass: tag src to cgnat-cidr dst (contains topology nodes), grants only, tcp",
|
||||
"category": "policytest",
|
||||
"captured_at": "2026-10-17T15:11:57.632457219Z",
|
||||
"tool_version": "tscap/4ef410b",
|
||||
"tailnet": "odin@example.com",
|
||||
"error": true,
|
||||
"input": {
|
||||
"api_response_code": 400,
|
||||
"api_response_body": {"message": "test(s) failed", "data": [{"user": "tag:client", "errors": [
|
||||
"[acl test error]: user or host is invalid: unknown user or host: \"100.64.0.0/24\""
|
||||
]}]},
|
||||
"tailnet": {
|
||||
"dns": {"magic_dns": false, "nameservers": [], "search_paths": [], "split_dns": {}},
|
||||
"settings": {}
|
||||
},
|
||||
"scenario_hujson": " \n \n \n \n \n \n \n \n \n \n{\n\t\"id\": \"policytest-allpass-grants-only-tag-src-cidr-dst-cgnat\",\n\t\"category\": \"policytest\",\n\t\"description\": \"tests block all-pass: tag src to cgnat-cidr dst (contains topology nodes), grants only, tcp\",\n\n\t\"topology\": \"../_topologies/acl.hujson\",\n\n\t\"tailnet\": {\n\t\t\"dns\": {\"magic_dns\": false, \"nameservers\": [], \"search_paths\": [], \"split_dns\": {}},\n\t\t\"settings\": {}\n\t},\n\n\t\"policy\": {\n\t\t\"groups\": {\n\t\t\t\"group:admins\": [\"odin@example.com\"],\n\t\t\t\"group:developers\": [\"thor@example.org\", \"odin@example.com\"],\n\t\t\t\"group:empty\": [],\n\t\t\t\"group:monitors\": [\"freya@example.com\"]\n\t\t},\n\t\t\"tagOwners\": {\n\t\t\t\"tag:client\": [\"odin@example.com\"],\n\t\t\t\"tag:exit\": [\"odin@example.com\"],\n\t\t\t\"tag:pidgey\": [\"odin@example.com\"],\n\t\t\t\"tag:pidgeotto\": [\"odin@example.com\"],\n\t\t\t\"tag:group-a\": [\"odin@example.com\"],\n\t\t\t\"tag:group-b\": [\"odin@example.com\"],\n\t\t\t\"tag:ha\": [\"odin@example.com\"],\n\t\t\t\"tag:prod\": [\"odin@example.com\"],\n\t\t\t\"tag:router\": [\"odin@example.com\"],\n\t\t\t\"tag:spearow\": [\"odin@example.com\"],\n\t\t\t\"tag:fearow\": [\"odin@example.com\"],\n\t\t\t\"tag:server\": [\"odin@example.com\"]\n\t\t},\n\t\t\"hosts\": {\n\t\t\t\"internal\": \"10.0.0.0/8\",\n\t\t\t\"prodbox\": \"100.103.8.15\",\n\t\t\t\"subnet24\": \"192.168.1.0/24\",\n\t\t\t\"webserver\": \"100.64.0.16\"\n\t\t},\n\t\t\"grants\": [\n\t\t\t{\"src\": [\"tag:client\"], \"dst\": [\"100.64.0.0/24\"], \"ip\": [\"tcp:22\"]}\n\t\t],\n\t\t\"tests\": [\n\t\t\t{\"src\": \"tag:client\", \"accept\": [\"100.64.0.0/24:22\"]}\n\t\t]\n\t}\n}\n",
|
||||
"scenario_path": "scenarios/policytest/policytest-allpass-grants-only-tag-src-cidr-dst-cgnat.hujson",
|
||||
"full_policy": {
|
||||
"grants": [{"dst": ["100.64.0.0/24"], "ip": ["tcp:22"], "src": ["tag:client"]}],
|
||||
"groups": {
|
||||
"group:admins": ["odin@example.com"],
|
||||
"group:developers": ["thor@example.org", "odin@example.com"],
|
||||
"group:empty": [],
|
||||
"group:monitors": ["freya@example.com"]
|
||||
},
|
||||
"hosts": {
|
||||
"internal": "10.0.0.0/8",
|
||||
"prodbox": "100.103.8.15",
|
||||
"subnet24": "192.168.1.0/24",
|
||||
"webserver": "100.64.0.16"
|
||||
},
|
||||
"tagOwners": {
|
||||
"tag:client": ["odin@example.com"],
|
||||
"tag:exit": ["odin@example.com"],
|
||||
"tag:pidgey": ["odin@example.com"],
|
||||
"tag:pidgeotto": ["odin@example.com"],
|
||||
"tag:group-a": ["odin@example.com"],
|
||||
"tag:group-b": ["odin@example.com"],
|
||||
"tag:ha": ["odin@example.com"],
|
||||
"tag:prod": ["odin@example.com"],
|
||||
"tag:router": ["odin@example.com"],
|
||||
"tag:spearow": ["odin@example.com"],
|
||||
"tag:fearow": ["odin@example.com"],
|
||||
"tag:server": ["odin@example.com"]
|
||||
},
|
||||
"tests": [{"accept": ["100.64.0.0/24:22"], "src": "tag:client"}]
|
||||
}
|
||||
},
|
||||
"topology": {"users": [
|
||||
{"id": 1, "name": "odin", "email": "odin@example.com"},
|
||||
{"id": 2, "name": "thor", "email": "thor@example.org"},
|
||||
{"id": 3, "name": "freya", "email": "freya@example.com"}
|
||||
], "nodes": {"beedrill": {
|
||||
"hostname": "beedrill",
|
||||
"tags": ["tag:server"],
|
||||
"ipv4": "100.64.0.16",
|
||||
"ipv6": "fd7a:115c:a1e0::10",
|
||||
"routable_ips": [],
|
||||
"approved_routes": []
|
||||
}, "bulbasaur": {
|
||||
"hostname": "bulbasaur",
|
||||
"tags": [],
|
||||
"ipv4": "100.64.0.19",
|
||||
"ipv6": "fd7a:115c:a1e0::13",
|
||||
"user": "odin",
|
||||
"routable_ips": [],
|
||||
"approved_routes": []
|
||||
}, "charmander": {
|
||||
"hostname": "charmander",
|
||||
"tags": ["tag:exit"],
|
||||
"ipv4": "100.64.0.4",
|
||||
"ipv6": "fd7a:115c:a1e0::4",
|
||||
"routable_ips": ["0.0.0.0/0", "::/0"],
|
||||
"approved_routes": []
|
||||
}, "ivysaur": {
|
||||
"hostname": "ivysaur",
|
||||
"tags": [],
|
||||
"ipv4": "100.64.0.17",
|
||||
"ipv6": "fd7a:115c:a1e0::11",
|
||||
"user": "thor",
|
||||
"routable_ips": [],
|
||||
"approved_routes": []
|
||||
}, "kakuna": {
|
||||
"hostname": "kakuna",
|
||||
"tags": ["tag:prod"],
|
||||
"ipv4": "100.64.0.15",
|
||||
"ipv6": "fd7a:115c:a1e0::f",
|
||||
"routable_ips": [],
|
||||
"approved_routes": []
|
||||
}, "squirtle": {
|
||||
"hostname": "squirtle",
|
||||
"tags": ["tag:router"],
|
||||
"ipv4": "100.64.0.13",
|
||||
"ipv6": "fd7a:115c:a1e0::d",
|
||||
"routable_ips": ["10.33.0.0/16"],
|
||||
"approved_routes": []
|
||||
}, "venusaur": {
|
||||
"hostname": "venusaur",
|
||||
"tags": [],
|
||||
"ipv4": "100.64.0.18",
|
||||
"ipv6": "fd7a:115c:a1e0::12",
|
||||
"user": "freya",
|
||||
"routable_ips": [],
|
||||
"approved_routes": []
|
||||
}, "weedle": {
|
||||
"hostname": "weedle",
|
||||
"tags": ["tag:client"],
|
||||
"ipv4": "100.64.0.14",
|
||||
"ipv6": "fd7a:115c:a1e0::e",
|
||||
"routable_ips": [],
|
||||
"approved_routes": []
|
||||
}}},
|
||||
"captures": {},
|
||||
"steps": [{
|
||||
"name": "validate",
|
||||
"at": "2026-10-17T15:12:03.605506581Z",
|
||||
"result": "observed",
|
||||
"actions": [{
|
||||
"kind": "api",
|
||||
"principal": "admin",
|
||||
"method": "POST",
|
||||
"path": "/tailnet/-/acl/validate",
|
||||
"header": {"Content-Type": "application/hujson"},
|
||||
"body": {
|
||||
"grants": [{"dst": ["100.64.0.0/24"], "ip": ["tcp:22"], "src": ["tag:client"]}],
|
||||
"groups": {
|
||||
"group:admins": ["odin@example.com"],
|
||||
"group:developers": ["thor@example.org", "odin@example.com"],
|
||||
"group:empty": [],
|
||||
"group:monitors": ["freya@example.com"]
|
||||
},
|
||||
"hosts": {
|
||||
"internal": "10.0.0.0/8",
|
||||
"prodbox": "100.103.8.15",
|
||||
"subnet24": "192.168.1.0/24",
|
||||
"webserver": "100.64.0.16"
|
||||
},
|
||||
"tagOwners": {
|
||||
"tag:client": ["odin@example.com"],
|
||||
"tag:exit": ["odin@example.com"],
|
||||
"tag:pidgey": ["odin@example.com"],
|
||||
"tag:pidgeotto": ["odin@example.com"],
|
||||
"tag:group-a": ["odin@example.com"],
|
||||
"tag:group-b": ["odin@example.com"],
|
||||
"tag:ha": ["odin@example.com"],
|
||||
"tag:prod": ["odin@example.com"],
|
||||
"tag:router": ["odin@example.com"],
|
||||
"tag:spearow": ["odin@example.com"],
|
||||
"tag:fearow": ["odin@example.com"],
|
||||
"tag:server": ["odin@example.com"]
|
||||
},
|
||||
"tests": [{"accept": ["100.64.0.0/24:22"], "src": "tag:client"}]
|
||||
},
|
||||
"status": 200,
|
||||
"response": {"data": [{"errors": [
|
||||
"[acl test error]: user or host is invalid: unknown user or host: \"100.64.0.0/24\""
|
||||
], "user": "tag:client"}], "message": "test(s) failed"}
|
||||
}]
|
||||
}]
|
||||
}
|
||||
Generated
Vendored
+53
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-8087
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+50
-6268
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+49
-7219
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+52
-7128
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-9237
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+55
-8727
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+59
-7635
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-7632
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+51
-8727
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-8727
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+57
-8717
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+55
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+57
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+57
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+51
-8719
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+55
-8727
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+57
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+57
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+48
-7117
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+52
-8727
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-8727
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+54
-8719
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-8727
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+57
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+59
-8727
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+54
-8727
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+57
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+59
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+46
-7118
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-8727
File diff suppressed because it is too large.
Load diff
+46
-7118
File diff suppressed because it is too large.
Load diff
+60
-8725
File diff suppressed because it is too large.
Load diff
+54
-7119
File diff suppressed because it is too large.
Load diff
+54
-7119
File diff suppressed because it is too large.
Load diff
+54
-7114
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-8719
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-8727
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+53
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+55
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+60
-8725
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+29
-19936
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+35
-19935
File diff suppressed because it is too large.
Load diff
+29
-19955
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+29
-19947
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+32
-19886
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+28
-19953
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+34
-13425
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+41
-19935
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+29
-19947
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+34
-19936
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+29
-19990
File diff suppressed because it is too large.
Load diff
+28
-19953
File diff suppressed because it is too large.
Load diff
+32
-19955
File diff suppressed because it is too large.
Load diff
+29
-19953
File diff suppressed because it is too large.
Load diff
+28
-19953
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+35
-19935
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+32
-18610
File diff suppressed because it is too large.
Load diff
+30
-19934
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+30
-19934
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+34
-19936
File diff suppressed because it is too large.
Load diff
+34
-19936
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+34
-19936
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+31
-13407
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+31
-13323
File diff suppressed because it is too large.
Load diff
+28
-18603
File diff suppressed because it is too large.
Load diff
+28
-18603
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+37
-18621
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+28
-18658
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+34
-18586
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+30
-18603
File diff suppressed because it is too large.
Load diff
Generated
Vendored
+34
-18586
File diff suppressed because it is too large.
Load diff
+28
-18603
File diff suppressed because it is too large.
Load diff
+31
-18585
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user