Files
archy/scripts/c01_network_plan.py

98 lines
6.6 KiB
Python

#!/usr/bin/python3
"""Non-admitting structural preflight. Never opens namespaces or binds sockets."""
import argparse
import hashlib
import ipaddress
import json
from pathlib import Path
import re
import sys
from c01_endpoints import validate_route, source_addresses, source_binding_conflict
from c01_uplink import validate as validate_uplink
import c01_network_mdns
import c01_retained_producer
def exact(pairs):
result={}
for key,value in pairs:
if key in result:raise ValueError('duplicate JSON field')
result[key]=value
return result
def validate(policy,runtime):
if type(policy) is not dict or set(policy) not in ({'version','namespaces','manifests','routes','uplink'},{'version','namespaces','manifests','routes','uplink','mdns'}) or type(policy['version']) is not int or policy['version']!=4:
raise ValueError('version 4 endpoint policy required')
ns=policy['namespaces']
if type(ns) is not dict or set(ns)!={'host','managed','runtime'}:raise ValueError('namespace fields')
for pair in ns.values():
if type(pair) is not list or len(pair)!=2 or any(type(v) is not int or v<=0 for v in pair):raise ValueError('measured namespace identity required')
if len({tuple(pair) for pair in ns.values()})!=3:raise ValueError('namespaces must be distinct')
validate_uplink(policy['uplink'])
if 'mdns' in policy:c01_network_mdns.validate(policy['mdns'],policy['uplink'])
if type(policy['manifests']) is not list or len(policy['manifests'])>128:raise ValueError('projection bound')
manifests={}
base={'app_id','manifest_sha256','loopback_ports','target_cgroup'}
for doc in policy['manifests']:
if type(doc) is not dict or set(doc) not in (base,base|{'source_bindings','target_addresses','producer'}):raise ValueError('projection exact fields')
if not re.fullmatch('[a-z0-9][a-z0-9-]{0,63}',doc['app_id']) or not re.fullmatch('[0-9a-f]{64}',doc['manifest_sha256']):raise ValueError('projection identity')
if type(doc['loopback_ports']) is not list or not 1<=len(doc['loopback_ports'])<=128:raise ValueError('projection ports bound')
for port in doc['loopback_ports']:
if type(port) is not list or len(port)!=2 or port[0] not in ('tcp','udp') or type(port[1]) is not int or not 1<=port[1]<=65535:raise ValueError('projection port')
if 'producer' in doc:
producer=doc['producer']
if type(producer) is dict and producer.get('kind')=='retained-container-v1':
c01_retained_producer.validate(producer,doc['target_cgroup'])
elif producer!={'kind':'exact'}:
fields={'kind','container_name','image_id'}
if type(producer) is not dict or set(producer) not in (fields,fields|{'payload_principal'}) or producer['kind']!='container-payload' or not re.fullmatch('[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}',producer['container_name']) or not re.fullmatch('[0-9a-f]{64}',producer['image_id']):raise ValueError('runtime producer selector')
if 'payload_principal' in producer:
principal=producer['payload_principal']
if type(principal) is not list or len(principal)!=2 or any(type(n) is not int or not 0<n<2**32-1 for n in principal):raise ValueError('mapped payload principal')
if not doc['target_cgroup'].endswith('.service'):raise ValueError('payload requires stable service cgroup')
if type(doc['source_bindings']) is not list or not 1<=len(doc['source_bindings'])<=512:raise ValueError('source binding bound')
for binding in doc['source_bindings']:
if type(binding) is not list or len(binding)!=4 or binding[0] not in ('host','managed') or binding[1] not in ('tcp','udp') or type(binding[2]) is not int or not 1<=binding[2]<=65535:raise ValueError('source binding')
ip=ipaddress.ip_address(binding[3])
if str(ip)!=binding[3] or ip.is_multicast or ip.is_link_local or '%' in binding[3]:raise ValueError('source binding address')
if type(doc['target_addresses']) is not list or not 1<=len(doc['target_addresses'])<=16:raise ValueError('target address bound')
for address in doc['target_addresses']:
ip=ipaddress.ip_address(address)
if str(ip)!=address or ip.is_unspecified or ip.is_multicast or ip.is_link_local or '%' in address:raise ValueError('target address')
raw=json.dumps(doc,sort_keys=True,separators=(',',':'),ensure_ascii=False,allow_nan=False).encode()
digest=hashlib.sha256(raw).hexdigest()
if digest in manifests:raise ValueError('duplicate projection')
manifests[digest]=doc
if type(policy['routes']) is not list or not 1<=len(policy['routes'])<=128:raise ValueError('route bound')
names=set();bindings=set()
for route in policy['routes']:
validate_route(route,manifests,runtime,address_version=4)
if route['name'] in names:raise ValueError('duplicate route')
names.add(route['name'])
for address in source_addresses(route):
key=(route['source'],route['protocol'],route['port'],address)
for prior in bindings:
if source_binding_conflict(key,prior):raise ValueError('overlapping source binding')
bindings.add(key)
if len(bindings)>512:raise ValueError('listener capability bound')
gated_ports={r['target_port'] for r in policy['routes'] if r['name'].startswith('gate/')}
for route in policy['routes']:
if route['target']=='runtime' and route['source']=='host' and route['protocol']=='tcp' and route['target_port'] in gated_ports and any(not ipaddress.ip_address(address).is_loopback for address in route['source_addresses']):
raise ValueError('public runtime route would bypass an appointed authentication gate')
mdns_names={name for name in names if name.startswith('native/mdns')}
if mdns_names != ({'native/mdns4/udp/5353/host','native/mdns6/udp/5353/host'} if 'mdns' in policy else set()):raise ValueError('complete mDNS scope/routes required')
return {'structural_validation':True,'routes':len(names),'listeners':len(bindings),'live_admission':False}
if __name__=='__main__':
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--policy',required=True);parser.add_argument('--runtime',required=True)
args=parser.parse_args()
def load(path):
with open(path,'rb') as stream:raw=stream.read(65537)
if len(raw)>65536:raise ValueError('plan exceeds size bound')
return json.loads(raw,object_pairs_hook=exact)
print(json.dumps(validate(load(args.policy),load(args.runtime))))