summaryrefslogtreecommitdiff
path: root/src/conf_mode/firewall.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode/firewall.py')
-rwxr-xr-xsrc/conf_mode/firewall.py418
1 files changed, 158 insertions, 260 deletions
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index cbd9cbe90..769cc598f 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -23,29 +23,26 @@ from sys import exit
from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.configdict import node_changed
from vyos.configdiff import get_config_diff, Diff
-# from vyos.configverify import verify_interface_exists
+from vyos.configdep import set_dependents, call_dependents
+from vyos.configverify import verify_interface_exists
+from vyos.firewall import fqdn_config_parse
from vyos.firewall import geoip_update
-from vyos.firewall import get_ips_domains_dict
-from vyos.firewall import nft_add_set_elements
-from vyos.firewall import nft_flush_set
-from vyos.firewall import nft_init_set
-from vyos.firewall import nft_update_set_elements
from vyos.template import render
-from vyos.util import call
-from vyos.util import cmd
-from vyos.util import dict_search_args
-from vyos.util import dict_search_recursive
-from vyos.util import process_named_running
-from vyos.util import rc_cmd
-from vyos.xml import defaults
+from vyos.utils.process import call
+from vyos.utils.process import cmd
+from vyos.utils.dict import dict_search_args
+from vyos.utils.dict import dict_search_recursive
+from vyos.utils.process import process_named_running
+from vyos.utils.process import rc_cmd
from vyos import ConfigError
from vyos import airbag
+
airbag.enable()
-policy_route_conf_script = '/usr/libexec/vyos/conf_mode/policy-route.py'
+nat_conf_script = 'nat.py'
+policy_route_conf_script = 'policy-route.py'
nftables_conf = '/run/nftables.conf'
@@ -58,7 +55,6 @@ sysfs_config = {
'log_martians': {'sysfs': '/proc/sys/net/ipv4/conf/all/log_martians'},
'receive_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_redirects'},
'send_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/send_redirects'},
- 'source_validation': {'sysfs': '/proc/sys/net/ipv4/conf/*/rp_filter', 'disable': '0', 'strict': '1', 'loose': '2'},
'syn_cookies': {'sysfs': '/proc/sys/net/ipv4/tcp_syncookies'},
'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337'}
}
@@ -67,7 +63,8 @@ valid_groups = [
'address_group',
'domain_group',
'network_group',
- 'port_group'
+ 'port_group',
+ 'interface_group'
]
nested_group_types = [
@@ -98,19 +95,22 @@ def geoip_updated(conf, firewall):
updated = False
for key, path in dict_search_recursive(firewall, 'geoip'):
- set_name = f'GEOIP_CC_{path[1]}_{path[3]}'
- if path[0] == 'name':
+ set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}'
+ if (path[0] == 'ipv4'):
out['name'].append(set_name)
- elif path[0] == 'ipv6_name':
+ elif (path[0] == 'ipv6'):
+ set_name = f'GEOIP_CC6_{path[1]}_{path[2]}_{path[4]}'
out['ipv6_name'].append(set_name)
+
updated = True
if 'delete' in node_diff:
for key, path in dict_search_recursive(node_diff['delete'], 'geoip'):
- set_name = f'GEOIP_CC_{path[1]}_{path[3]}'
- if path[0] == 'name':
+ set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}'
+ if (path[0] == 'ipv4'):
out['deleted_name'].append(set_name)
- elif path[0] == 'ipv6-name':
+ elif (path[0] == 'ipv6'):
+ set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}'
out['deleted_ipv6_name'].append(set_name)
updated = True
@@ -126,53 +126,29 @@ def get_config(config=None):
conf = Config()
base = ['firewall']
- firewall = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
- no_tag_node_value_mangle=True)
-
- # We have gathered the dict representation of the CLI, but there are
- # default options which we need to update into the dictionary retrived.
- # XXX: T2665: we currently have no nice way for defaults under tag
- # nodes, thus we load the defaults "by hand"
- default_values = defaults(base)
- for tmp in ['name', 'ipv6_name']:
- if tmp in default_values:
- del default_values[tmp]
-
- if 'zone' in default_values:
- del default_values['zone']
-
- firewall = dict_merge(default_values, firewall)
-
- # Merge in defaults for IPv4 ruleset
- if 'name' in firewall:
- default_values = defaults(base + ['name'])
- for name in firewall['name']:
- firewall['name'][name] = dict_merge(default_values,
- firewall['name'][name])
-
- # Merge in defaults for IPv6 ruleset
- if 'ipv6_name' in firewall:
- default_values = defaults(base + ['ipv6-name'])
- for ipv6_name in firewall['ipv6_name']:
- firewall['ipv6_name'][ipv6_name] = dict_merge(default_values,
- firewall['ipv6_name'][ipv6_name])
-
- if 'zone' in firewall:
- default_values = defaults(base + ['zone'])
- for zone in firewall['zone']:
- firewall['zone'][zone] = dict_merge(default_values, firewall['zone'][zone])
-
- firewall['policy_resync'] = bool('group' in firewall or node_changed(conf, base + ['group']))
-
- if 'config_trap' in firewall and firewall['config_trap'] == 'enable':
- diff = get_config_diff(conf)
- firewall['trap_diff'] = diff.get_child_nodes_diff_str(base)
- firewall['trap_targets'] = conf.get_config_dict(['service', 'snmp', 'trap-target'],
- key_mangling=('-', '_'), get_first_key=True,
- no_tag_node_value_mangle=True)
+ firewall = conf.get_config_dict(base, key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True,
+ with_recursive_defaults=True)
+
+
+ firewall['group_resync'] = bool('group' in firewall or node_changed(conf, base + ['group']))
+ if firewall['group_resync']:
+ # Update nat and policy-route as firewall groups were updated
+ set_dependents('group_resync', conf)
firewall['geoip_updated'] = geoip_updated(conf, firewall)
+ fqdn_config_parse(firewall)
+
+ firewall['flowtable_enabled'] = False
+ flow_offload = dict_search_args(firewall, 'global_options', 'flow_offload')
+ if flow_offload and 'disable' not in flow_offload:
+ for offload_type in ('software', 'hardware'):
+ if dict_search_args(flow_offload, offload_type, 'interface'):
+ firewall['flowtable_enabled'] = True
+ break
+
return firewall
def verify_rule(firewall, rule_conf, ipv6):
@@ -187,11 +163,20 @@ def verify_rule(firewall, rule_conf, ipv6):
raise ConfigError('jump-target defined, but action jump needed and it is not defined')
target = rule_conf['jump_target']
if not ipv6:
- if target not in dict_search_args(firewall, 'name'):
+ if target not in dict_search_args(firewall, 'ipv4', 'name'):
raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
else:
- if target not in dict_search_args(firewall, 'ipv6_name'):
- raise ConfigError(f'Invalid jump-target. Firewall ipv6-name {target} does not exist on the system')
+ if target not in dict_search_args(firewall, 'ipv6', 'name'):
+ raise ConfigError(f'Invalid jump-target. Firewall ipv6 name {target} does not exist on the system')
+
+ if 'queue_options' in rule_conf:
+ if 'queue' not in rule_conf['action']:
+ raise ConfigError('queue-options defined, but action queue needed and it is not defined')
+ if 'fanout' in rule_conf['queue_options'] and ('queue' not in rule_conf or '-' not in rule_conf['queue']):
+ raise ConfigError('queue-options fanout defined, then queue needs to be defined as a range')
+
+ if 'queue' in rule_conf and 'queue' not in rule_conf['action']:
+ raise ConfigError('queue defined, but action queue needed and it is not defined')
if 'fragment' in rule_conf:
if {'match_frag', 'match_non_frag'} <= set(rule_conf['fragment']):
@@ -232,29 +217,28 @@ def verify_rule(firewall, rule_conf, ipv6):
if side in rule_conf:
side_conf = rule_conf[side]
- if dict_search_args(side_conf, 'geoip', 'country_code'):
- if 'address' in side_conf:
- raise ConfigError('Address and GeoIP cannot both be defined')
-
- if dict_search_args(side_conf, 'group', 'address_group'):
- raise ConfigError('Address-group and GeoIP cannot both be defined')
-
- if dict_search_args(side_conf, 'group', 'network_group'):
- raise ConfigError('Network-group and GeoIP cannot both be defined')
+ if len({'address', 'fqdn', 'geoip'} & set(side_conf)) > 1:
+ raise ConfigError('Only one of address, fqdn or geoip can be specified')
if 'group' in side_conf:
- if {'address_group', 'network_group'} <= set(side_conf['group']):
- raise ConfigError('Only one address-group or network-group can be specified')
+ if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1:
+ raise ConfigError('Only one address-group, network-group or domain-group can be specified')
for group in valid_groups:
if group in side_conf['group']:
group_name = side_conf['group'][group]
+ fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group
+ error_group = fw_group.replace("_", "-")
+
+ if group in ['address_group', 'network_group', 'domain_group']:
+ types = [t for t in ['address', 'fqdn', 'geoip'] if t in side_conf]
+ if types:
+ raise ConfigError(f'{error_group} and {types[0]} cannot both be defined')
+
if group_name and group_name[0] == '!':
group_name = group_name[1:]
- fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group
- error_group = fw_group.replace("_", "-")
group_obj = dict_search_args(firewall, 'group', fw_group, group_name)
if group_obj is None:
@@ -270,10 +254,30 @@ def verify_rule(firewall, rule_conf, ipv6):
if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group')
+ if 'port' in side_conf and dict_search_args(side_conf, 'group', 'port_group'):
+ raise ConfigError(f'{side} port-group and port cannot both be defined')
+
+ if 'log_options' in rule_conf:
+ if 'log' not in rule_conf or 'enable' not in rule_conf['log']:
+ raise ConfigError('log-options defined, but log is not enable')
+
+ if 'snapshot_length' in rule_conf['log_options'] and 'group' not in rule_conf['log_options']:
+ raise ConfigError('log-options snapshot-length defined, but log group is not define')
+
+ if 'queue_threshold' in rule_conf['log_options'] and 'group' not in rule_conf['log_options']:
+ raise ConfigError('log-options queue-threshold defined, but log group is not define')
+
+ for direction in ['inbound_interface','outbound_interface']:
+ if direction in rule_conf:
+ if 'interface_name' in rule_conf[direction] and 'interface_group' in rule_conf[direction]:
+ raise ConfigError(f'Cannot specify both interface-group and interface-name for {direction}')
+
def verify_nested_group(group_name, group, groups, seen):
if 'include' not in group:
return
+ seen.append(group_name)
+
for g in group['include']:
if g not in groups:
raise ConfigError(f'Nested group "{g}" does not exist')
@@ -281,16 +285,10 @@ def verify_nested_group(group_name, group, groups, seen):
if g in seen:
raise ConfigError(f'Group "{group_name}" has a circular reference')
- seen.append(g)
-
if 'include' in groups[g]:
verify_nested_group(g, groups[g], groups, seen)
def verify(firewall):
- if 'config_trap' in firewall and firewall['config_trap'] == 'enable':
- if not firewall['trap_targets']:
- raise ConfigError(f'Firewall config-trap enabled but "service snmp trap-target" is not defined')
-
if 'group' in firewall:
for group_type in nested_group_types:
if group_type in firewall['group']:
@@ -298,95 +296,53 @@ def verify(firewall):
for group_name, group in groups.items():
verify_nested_group(group_name, group, groups, [])
- for name in ['name', 'ipv6_name']:
- if name in firewall:
- for name_id, name_conf in firewall[name].items():
- if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf:
- raise ConfigError('default-action set to jump, but no default-jump-target specified')
- if 'default_jump_target' in name_conf:
- target = name_conf['default_jump_target']
- if 'jump' not in name_conf['default_action']:
- raise ConfigError('default-jump-target defined,but default-action jump needed and it is not defined')
- if name_conf['default_jump_target'] == name_id:
- raise ConfigError(f'Loop detected on default-jump-target.')
- ## Now need to check that default-jump-target exists (other firewall chain/name)
- if target not in dict_search_args(firewall, name):
- raise ConfigError(f'Invalid jump-target. Firewall {name} {target} does not exist on the system')
-
- if 'rule' in name_conf:
- for rule_id, rule_conf in name_conf['rule'].items():
- verify_rule(firewall, rule_conf, name == 'ipv6_name')
-
- if 'interface' in firewall:
- for ifname, if_firewall in firewall['interface'].items():
- # verify ifname needs to be disabled, dynamic devices come up later
- # verify_interface_exists(ifname)
-
- for direction in ['in', 'out', 'local']:
- name = dict_search_args(if_firewall, direction, 'name')
- ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name')
-
- if name and dict_search_args(firewall, 'name', name) == None:
- raise ConfigError(f'Invalid firewall name "{name}" referenced on interface {ifname}')
-
- if ipv6_name and dict_search_args(firewall, 'ipv6_name', ipv6_name) == None:
- raise ConfigError(f'Invalid firewall ipv6-name "{ipv6_name}" referenced on interface {ifname}')
-
- local_zone = False
- zone_interfaces = []
-
- if 'zone' in firewall:
- for zone, zone_conf in firewall['zone'].items():
- if 'local_zone' not in zone_conf and 'interface' not in zone_conf:
- raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone')
-
- if 'local_zone' in zone_conf:
- if local_zone:
- raise ConfigError('There cannot be multiple local zones')
- if 'interface' in zone_conf:
- raise ConfigError('Local zone cannot have interfaces assigned')
- if 'intra_zone_filtering' in zone_conf:
- raise ConfigError('Local zone cannot use intra-zone-filtering')
- local_zone = True
-
- if 'interface' in zone_conf:
- found_duplicates = [intf for intf in zone_conf['interface'] if intf in zone_interfaces]
-
- if found_duplicates:
- raise ConfigError(f'Interfaces cannot be assigned to multiple zones')
-
- zone_interfaces += zone_conf['interface']
-
- if 'intra_zone_filtering' in zone_conf:
- intra_zone = zone_conf['intra_zone_filtering']
-
- if len(intra_zone) > 1:
- raise ConfigError('Only one intra-zone-filtering action must be specified')
-
- if 'firewall' in intra_zone:
- v4_name = dict_search_args(intra_zone, 'firewall', 'name')
- if v4_name and not dict_search_args(firewall, 'name', v4_name):
- raise ConfigError(f'Firewall name "{v4_name}" does not exist')
-
- v6_name = dict_search_args(intra_zone, 'firewall', 'ipv6_name')
- if v6_name and not dict_search_args(firewall, 'ipv6_name', v6_name):
- raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
-
- if not v4_name and not v6_name:
- raise ConfigError('No firewall names specified for intra-zone-filtering')
-
- if 'from' in zone_conf:
- for from_zone, from_conf in zone_conf['from'].items():
- if from_zone not in firewall['zone']:
- raise ConfigError(f'Zone "{zone}" refers to a non-existent or deleted zone "{from_zone}"')
-
- v4_name = dict_search_args(from_conf, 'firewall', 'name')
- if v4_name and not dict_search_args(firewall, 'name', v4_name):
- raise ConfigError(f'Firewall name "{v4_name}" does not exist')
-
- v6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name')
- if v6_name and not dict_search_args(firewall, 'ipv6_name', v6_name):
- raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
+ if 'ipv4' in firewall:
+ for name in ['name','forward','input','output']:
+ if name in firewall['ipv4']:
+ for name_id, name_conf in firewall['ipv4'][name].items():
+ if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf:
+ raise ConfigError('default-action set to jump, but no default-jump-target specified')
+ if 'default_jump_target' in name_conf:
+ target = name_conf['default_jump_target']
+ if 'jump' not in name_conf['default_action']:
+ raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined')
+ if name_conf['default_jump_target'] == name_id:
+ raise ConfigError(f'Loop detected on default-jump-target.')
+ ## Now need to check that default-jump-target exists (other firewall chain/name)
+ if target not in dict_search_args(firewall['ipv4'], 'name'):
+ raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
+
+ if 'rule' in name_conf:
+ for rule_id, rule_conf in name_conf['rule'].items():
+ verify_rule(firewall, rule_conf, False)
+
+ if 'ipv6' in firewall:
+ for name in ['name','forward','input','output']:
+ if name in firewall['ipv6']:
+ for name_id, name_conf in firewall['ipv6'][name].items():
+ if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf:
+ raise ConfigError('default-action set to jump, but no default-jump-target specified')
+ if 'default_jump_target' in name_conf:
+ target = name_conf['default_jump_target']
+ if 'jump' not in name_conf['default_action']:
+ raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined')
+ if name_conf['default_jump_target'] == name_id:
+ raise ConfigError(f'Loop detected on default-jump-target.')
+ ## Now need to check that default-jump-target exists (other firewall chain/name)
+ if target not in dict_search_args(firewall['ipv6'], 'name'):
+ raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
+
+ if 'rule' in name_conf:
+ for rule_id, rule_conf in name_conf['rule'].items():
+ verify_rule(firewall, rule_conf, True)
+
+ # Verify flow offload options
+ flow_offload = dict_search_args(firewall, 'global_options', 'flow_offload')
+ for offload_type in ('software', 'hardware'):
+ interfaces = dict_search_args(flow_offload, offload_type, 'interface') or []
+ for interface in interfaces:
+ # nft will raise an error when adding a non-existent interface to a flowtable
+ verify_interface_exists(interface)
return None
@@ -394,18 +350,18 @@ def generate(firewall):
if not os.path.exists(nftables_conf):
firewall['first_install'] = True
- if 'zone' in firewall:
- for local_zone, local_zone_conf in firewall['zone'].items():
- if 'local_zone' not in local_zone_conf:
- continue
-
- local_zone_conf['from_local'] = {}
-
- for zone, zone_conf in firewall['zone'].items():
- if zone == local_zone or 'from' not in zone_conf:
- continue
- if local_zone in zone_conf['from']:
- local_zone_conf['from_local'][zone] = zone_conf['from'][local_zone]
+ # Determine if conntrack is needed
+ firewall['ipv4_conntrack_action'] = 'return'
+ firewall['ipv6_conntrack_action'] = 'return'
+ if firewall['flowtable_enabled']: # Netfilter's flowtable offload requires conntrack
+ firewall['ipv4_conntrack_action'] = 'accept'
+ firewall['ipv6_conntrack_action'] = 'accept'
+ else: # Check if conntrack is needed by firewall rules
+ for proto in ('ipv4', 'ipv6'):
+ for rules, _ in dict_search_recursive(firewall.get(proto, {}), 'rule'):
+ if any(('state' in rule_conf or 'connection_status' in rule_conf) for rule_conf in rules.values()):
+ firewall[f'{proto}_conntrack_action'] = 'accept'
+ break
render(nftables_conf, 'firewall/nftables.j2', firewall)
return None
@@ -415,9 +371,8 @@ def apply_sysfs(firewall):
paths = glob(conf['sysfs'])
value = None
- if name in firewall:
- conf_value = firewall[name]
-
+ if name in firewall['global_options']:
+ conf_value = firewall['global_options'][name]
if conf_value in conf:
value = conf[conf_value]
elif conf_value == 'enable':
@@ -430,78 +385,23 @@ def apply_sysfs(firewall):
with open(path, 'w') as f:
f.write(value)
-def post_apply_trap(firewall):
- if 'first_install' in firewall:
- return None
-
- if 'config_trap' not in firewall or firewall['config_trap'] != 'enable':
- return None
-
- if not process_named_running('snmpd'):
- return None
-
- trap_username = os.getlogin()
-
- for host, target_conf in firewall['trap_targets'].items():
- community = target_conf['community'] if 'community' in target_conf else 'public'
- port = int(target_conf['port']) if 'port' in target_conf else 162
-
- base_cmd = f'snmptrap -v2c -c {community} {host}:{port} 0 {snmp_trap_mib}::{snmp_trap_name} '
-
- for change_type, changes in firewall['trap_diff'].items():
- for path_str, value in changes.items():
- objects = [
- f'mgmtEventUser s "{trap_username}"',
- f'mgmtEventSource i {snmp_event_source}',
- f'mgmtEventType i {snmp_change_type[change_type]}'
- ]
-
- if change_type == 'add':
- objects.append(f'mgmtEventCurrCfg s "{path_str} {value}"')
- elif change_type == 'delete':
- objects.append(f'mgmtEventPrevCfg s "{path_str} {value}"')
- elif change_type == 'change':
- objects.append(f'mgmtEventPrevCfg s "{path_str} {value[0]}"')
- objects.append(f'mgmtEventCurrCfg s "{path_str} {value[1]}"')
-
- cmd(base_cmd + ' '.join(objects))
-
-def resync_policy_route():
- # Update policy route as firewall groups were updated
- tmp, out = rc_cmd(policy_route_conf_script)
- if tmp > 0:
- Warning(f'Failed to re-apply policy route configuration! {out}')
-
def apply(firewall):
install_result, output = rc_cmd(f'nft -f {nftables_conf}')
if install_result == 1:
raise ConfigError(f'Failed to apply firewall: {output}')
- # set firewall group domain-group xxx
- if 'group' in firewall:
- if 'domain_group' in firewall['group']:
- # T970 Enable a resolver (systemd daemon) that checks
- # domain-group addresses and update entries for domains by timeout
- # If router loaded without internet connection or for synchronization
- call('systemctl restart vyos-domain-group-resolve.service')
- for group, group_config in firewall['group']['domain_group'].items():
- domains = []
- if group_config.get('address') is not None:
- for address in group_config.get('address'):
- domains.append(address)
- # Add elements to domain-group, try to resolve domain => ip
- # and add elements to nft set
- ip_dict = get_ips_domains_dict(domains)
- elements = sum(ip_dict.values(), [])
- nft_init_set(f'D_{group}')
- nft_add_set_elements(f'D_{group}', elements)
- else:
- call('systemctl stop vyos-domain-group-resolve.service')
-
apply_sysfs(firewall)
- if firewall['policy_resync']:
- resync_policy_route()
+ if firewall['group_resync']:
+ call_dependents()
+
+ # T970 Enable a resolver (systemd daemon) that checks
+ # domain-group/fqdn addresses and update entries for domains by timeout
+ # If router loaded without internet connection or for synchronization
+ domain_action = 'stop'
+ if dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'] or firewall['ip6_fqdn']:
+ domain_action = 'restart'
+ call(f'systemctl {domain_action} vyos-domain-resolver.service')
if firewall['geoip_updated']:
# Call helper script to Update set contents
@@ -509,8 +409,6 @@ def apply(firewall):
print('Updating GeoIP. Please wait...')
geoip_update(firewall)
- post_apply_trap(firewall)
-
return None
if __name__ == '__main__':