gowalk-cicd 1.0.151 → 1.0.153
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/backend-action/.daemux-version +1 -1
- package/backend-action/network_pools.py +25 -11
- package/backend-action/test_network_rule_diagnostics.py +69 -0
- package/docs/BACKEND_NETWORKS.md +6 -0
- package/package.json +1 -1
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.153
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.153
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.153
|
|
@@ -10,7 +10,8 @@ FAILURES = frozenset({
|
|
|
10
10
|
'pool_provenance_unknown', 'pool_inventory_invalid', 'host_routes_unsupported',
|
|
11
11
|
'host_rules_unsupported', 'host_addresses_invalid', 'resolver_inventory_invalid',
|
|
12
12
|
'resolver_upstream_unknown',
|
|
13
|
-
})
|
|
13
|
+
}) | frozenset('host_rules_unsupported_' + part for part in
|
|
14
|
+
('count', 'fields', 'priority', 'source', 'table', 'structure', 'fwmark', 'fwmask', 'action', 'duplicate'))
|
|
14
15
|
PRIVATE = tuple(ipaddress.ip_network(value) for value in ('10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'))
|
|
15
16
|
DEVICE = re.compile(r'[A-Za-z0-9][A-Za-z0-9_.:-]{0,14}')
|
|
16
17
|
|
|
@@ -195,7 +196,7 @@ def validate_rules(rows):
|
|
|
195
196
|
"""Admit ordinary Linux rules or the exact Tailscale v1.88.2 baseIPRules chain."""
|
|
196
197
|
# https://github.com/tailscale/tailscale/blob/v1.88.2/wgengine/router/router_linux.go
|
|
197
198
|
if not isinstance(rows, list) or len(rows) not in (3, 7):
|
|
198
|
-
raise InventoryFailure('
|
|
199
|
+
raise InventoryFailure('host_rules_unsupported_count')
|
|
199
200
|
expected = [{'priority': p, 'src': 'all', 'table': t}
|
|
200
201
|
for p, t in ((0, 'local'), (32766, 'main'), (32767, 'default'))]
|
|
201
202
|
if len(rows) == 7:
|
|
@@ -203,23 +204,36 @@ def validate_rules(rows):
|
|
|
203
204
|
expected += [dict(marked, priority=5210, table='main'), dict(marked, priority=5230, table='default'),
|
|
204
205
|
dict(marked, priority=5250, action='unreachable'), {'priority': 5270, 'src': 'all', 'table': 52}]
|
|
205
206
|
actual = []
|
|
206
|
-
|
|
207
|
+
# iproute2 iprule.c prints table names/numbers as JSON strings, including Tailscale's "52".
|
|
208
|
+
tables = {255: 'local', 254: 'main', 253: 'default',
|
|
209
|
+
'255': 'local', '254': 'main', '253': 'default', '52': 52}
|
|
207
210
|
for row in rows:
|
|
208
|
-
if
|
|
209
|
-
'fwmark', 'fwmask', 'action'}
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
if not isinstance(row, dict) or set(row) - {'priority', 'src', 'table', 'protocol',
|
|
212
|
+
'fwmark', 'fwmask', 'action'}:
|
|
213
|
+
raise InventoryFailure('host_rules_unsupported_fields')
|
|
214
|
+
if type(row.get('priority')) is not int:
|
|
215
|
+
raise InventoryFailure('host_rules_unsupported_priority')
|
|
216
|
+
if row.get('src') != 'all':
|
|
217
|
+
raise InventoryFailure('host_rules_unsupported_source')
|
|
212
218
|
normalized = {key: value for key, value in row.items() if key != 'protocol'}
|
|
213
219
|
table = normalized.get('table')
|
|
214
220
|
if table is not None:
|
|
215
221
|
if type(table) not in (str, int):
|
|
216
|
-
raise InventoryFailure('
|
|
222
|
+
raise InventoryFailure('host_rules_unsupported_table')
|
|
217
223
|
normalized['table'] = tables.get(table, table)
|
|
218
|
-
|
|
219
|
-
|
|
224
|
+
wanted = next((item for item in expected if item['priority'] == normalized['priority']), None)
|
|
225
|
+
if wanted is None:
|
|
226
|
+
raise InventoryFailure('host_rules_unsupported_priority')
|
|
227
|
+
if set(normalized) != set(wanted):
|
|
228
|
+
raise InventoryFailure('host_rules_unsupported_structure')
|
|
229
|
+
for field in ('table', 'fwmark', 'fwmask', 'action'):
|
|
230
|
+
if normalized.get(field) != wanted.get(field):
|
|
231
|
+
raise InventoryFailure('host_rules_unsupported_' + field)
|
|
232
|
+
if normalized in actual:
|
|
233
|
+
raise InventoryFailure('host_rules_unsupported_duplicate')
|
|
220
234
|
actual.append(normalized)
|
|
221
235
|
if len(actual) != len(expected):
|
|
222
|
-
raise InventoryFailure('
|
|
236
|
+
raise InventoryFailure('host_rules_unsupported_count')
|
|
223
237
|
|
|
224
238
|
|
|
225
239
|
def resolver_exclusions(raw, *, resolved=False, records=None):
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Classify refused policy shapes without publishing their private values or relaxing admission."""
|
|
2
|
+
import copy
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import unittest
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
from network_diagnostics import SCHEMA, decode
|
|
9
|
+
from network_inventory import collect
|
|
10
|
+
from network_pools import InventoryFailure, validate_rules
|
|
11
|
+
from test_network_inventory import FakeHost, RULES
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RuleDiagnosticTests(unittest.TestCase):
|
|
15
|
+
def test_iproute_decimal_table_strings_keep_exact_policy_semantics(self):
|
|
16
|
+
marked = {'src': 'all', 'fwmark': '0x80000', 'fwmask': '0xff0000'}
|
|
17
|
+
rules = [dict(row, table=str({'local': 255, 'main': 254, 'default': 253}[row['table']]))
|
|
18
|
+
for row in RULES]
|
|
19
|
+
validate_rules(rules)
|
|
20
|
+
rules += [dict(marked, priority=5210, table='254'), dict(marked, priority=5230, table='253'),
|
|
21
|
+
dict(marked, priority=5250, action='unreachable'),
|
|
22
|
+
{'priority': 5270, 'src': 'all', 'table': '52'}]
|
|
23
|
+
validate_rules(rules)
|
|
24
|
+
for invalid in ('53', '052', '0x34', 'private table alias', 53, True):
|
|
25
|
+
changed = copy.deepcopy(rules)
|
|
26
|
+
changed[-1]['table'] = invalid
|
|
27
|
+
with self.subTest(table=invalid), self.assertRaises(InventoryFailure) as caught:
|
|
28
|
+
validate_rules(changed)
|
|
29
|
+
self.assertEqual(caught.exception.code, 'host_rules_unsupported_table')
|
|
30
|
+
|
|
31
|
+
def test_each_rejected_shape_keeps_a_closed_reason_without_values(self):
|
|
32
|
+
marked = {'src': 'all', 'fwmark': '0x80000', 'fwmask': '0xff0000'}
|
|
33
|
+
valid = RULES + [dict(marked, priority=5210, table='main'), dict(marked, priority=5230, table='default'),
|
|
34
|
+
dict(marked, priority=5250, action='unreachable'),
|
|
35
|
+
{'priority': 5270, 'src': 'all', 'table': 52}]
|
|
36
|
+
validate_rules(valid)
|
|
37
|
+
cases = [('fields', 'PRIVATE_SENTINEL', 'PRIVATE_SENTINEL'), ('priority', 'priority', True),
|
|
38
|
+
('source', 'src', 'PRIVATE_SENTINEL'), ('table', 'table', 'PRIVATE_SENTINEL'),
|
|
39
|
+
('fwmark', 'fwmark', 'PRIVATE_SENTINEL'), ('fwmask', 'fwmask', 'PRIVATE_SENTINEL')]
|
|
40
|
+
for reason, key, value in cases:
|
|
41
|
+
rows = copy.deepcopy(valid)
|
|
42
|
+
rows[3][key] = value
|
|
43
|
+
with self.subTest(reason=reason), self.assertRaises(InventoryFailure) as caught:
|
|
44
|
+
validate_rules(rows)
|
|
45
|
+
self.assertEqual(caught.exception.code, 'host_rules_unsupported_' + reason)
|
|
46
|
+
self.assertNotIn('PRIVATE_SENTINEL', str(caught.exception))
|
|
47
|
+
for rows, reason in ((valid[:-1], 'count'), (valid[:-1] + [valid[0]], 'duplicate'),
|
|
48
|
+
([dict(row, fwmark='0x80000') for row in RULES], 'structure')):
|
|
49
|
+
with self.subTest(reason=reason), self.assertRaises(InventoryFailure) as caught:
|
|
50
|
+
validate_rules(rows)
|
|
51
|
+
self.assertEqual(caught.exception.code, 'host_rules_unsupported_' + reason)
|
|
52
|
+
|
|
53
|
+
def test_scoped_diagnostic_retains_reason_and_partial_metadata_only(self):
|
|
54
|
+
host = FakeHost()
|
|
55
|
+
host.answers[('ip', '-j', '-4', 'rule', 'show')][0]['src'] = 'PRIVATE_SENTINEL'
|
|
56
|
+
with patch.dict(os.environ, {}, clear=True), self.assertRaises(InventoryFailure) as caught:
|
|
57
|
+
collect(host, require_pools=False)
|
|
58
|
+
failure = caught.exception
|
|
59
|
+
result = dict(failure.details, schema=SCHEMA, ok=False, project='fixture', failure=failure.code)
|
|
60
|
+
raw = json.dumps(result).encode()
|
|
61
|
+
self.assertEqual(decode(1, raw, 'fixture')['failure'], 'host_rules_unsupported_source')
|
|
62
|
+
self.assertFalse(result['complete'])
|
|
63
|
+
self.assertNotIn(b'PRIVATE_SENTINEL', raw)
|
|
64
|
+
self.assertNotIn('networks', result)
|
|
65
|
+
self.assertFalse(any('create' in call or 'rm' in call for call in host.calls))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == '__main__':
|
|
69
|
+
unittest.main()
|
package/docs/BACKEND_NETWORKS.md
CHANGED
|
@@ -79,6 +79,12 @@ A routing refusal can retain safe engine/pool metadata with `complete:false` and
|
|
|
79
79
|
partial evidence never admits allocation. A successful diagnostic is a readback, not a deployment,
|
|
80
80
|
capacity reservation, proof of application health or proof that another project cannot reach the app.
|
|
81
81
|
Raw container data, labels, environment, daemon configuration and command errors are not exported.
|
|
82
|
+
Policy-rule refusals use `host_rules_unsupported_<reason>` to distinguish count, fields, priority,
|
|
83
|
+
source, table, structure, fwmark, fwmask, action and duplicate mismatches. These fixed classifications
|
|
84
|
+
retain the strict supported-rule comparison and publish no rejected keys or values.
|
|
85
|
+
The exact decimal-string spellings of supported table IDs are normalized because
|
|
86
|
+
[iproute2 prints rule tables as JSON strings](https://github.com/iproute2/iproute2/blob/v6.8.0/ip/iprule.c#L370).
|
|
87
|
+
This does not admit additional routing tables, aliases, priorities or forwarding policies.
|
|
82
88
|
|
|
83
89
|
The deployment emits its own `gowalk-cicd/backend-networks.v1` receipt. The existing
|
|
84
90
|
`backend_deploy_failed` phase remains `compose_up` on a preparation refusal. Normal public health,
|