diff options
Diffstat (limited to 'src')
| -rwxr-xr-x | src/conf_mode/firewall.py | 26 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-wireguard.py | 32 | ||||
| -rwxr-xr-x | src/conf_mode/nat.py | 29 | ||||
| -rwxr-xr-x | src/conf_mode/nat66.py | 11 | ||||
| -rwxr-xr-x | src/conf_mode/policy-route.py | 4 | ||||
| -rwxr-xr-x | src/conf_mode/protocols_isis.py | 22 | ||||
| -rwxr-xr-x | src/conf_mode/service_ipoe-server.py | 289 | ||||
| -rwxr-xr-x | src/conf_mode/service_pppoe-server.py | 30 | ||||
| -rwxr-xr-x | src/conf_mode/system_update_check.py | 93 | ||||
| -rwxr-xr-x | src/conf_mode/vpn_ipsec.py | 11 | ||||
| -rwxr-xr-x | src/conf_mode/vpn_openconnect.py | 22 | ||||
| -rwxr-xr-x | src/migration-scripts/ipoe-server/0-to-1 | 133 | ||||
| -rwxr-xr-x | src/migration-scripts/ipsec/9-to-10 | 134 | ||||
| -rwxr-xr-x | src/migration-scripts/pppoe-server/5-to-6 | 52 | ||||
| -rwxr-xr-x | src/op_mode/nat.py | 2 | ||||
| -rwxr-xr-x | src/op_mode/show_nat66_statistics.py | 2 | ||||
| -rwxr-xr-x | src/op_mode/show_nat_statistics.py | 2 | ||||
| -rwxr-xr-x | src/op_mode/system.py | 92 | ||||
| -rwxr-xr-x | src/system/vyos-system-update-check.py | 70 | ||||
| -rw-r--r-- | src/systemd/vyos-system-update.service | 11 | ||||
| -rwxr-xr-x | src/validators/range | 56 | 
21 files changed, 628 insertions, 495 deletions
| diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index eeb57bd30..cbd9cbe90 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -179,6 +179,20 @@ def verify_rule(firewall, rule_conf, ipv6):      if 'action' not in rule_conf:          raise ConfigError('Rule action must be defined') +    if 'jump' in rule_conf['action'] and 'jump_target' not in rule_conf: +        raise ConfigError('Action set to jump, but no jump-target specified') + +    if 'jump_target' in rule_conf: +        if 'jump' not in rule_conf['action']: +            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'): +                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 'fragment' in rule_conf:          if {'match_frag', 'match_non_frag'} <= set(rule_conf['fragment']):              raise ConfigError('Cannot specify both "match-frag" and "match-non-frag"') @@ -287,6 +301,18 @@ def verify(firewall):      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') diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 61bab2feb..8d738f55e 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -14,16 +14,12 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -import os -  from sys import exit -from copy import deepcopy  from vyos.config import Config  from vyos.configdict import dict_merge  from vyos.configdict import get_interface_dict -from vyos.configdict import node_changed -from vyos.configdict import leaf_node_changed +from vyos.configdict import is_node_changed  from vyos.configverify import verify_vrf  from vyos.configverify import verify_address  from vyos.configverify import verify_bridge_delete @@ -50,17 +46,20 @@ def get_config(config=None):      ifname, wireguard = get_interface_dict(conf, base)      # Check if a port was changed -    wireguard['port_changed'] = leaf_node_changed(conf, base + [ifname, 'port']) +    tmp = is_node_changed(conf, base + [ifname, 'port']) +    if tmp: wireguard['port_changed'] = {}      # Determine which Wireguard peer has been removed.      # Peers can only be removed with their public key! -    dict = {} -    tmp = node_changed(conf, base + [ifname, 'peer'], key_mangling=('-', '_')) -    for peer in (tmp or []): -        public_key = leaf_node_changed(conf, base + [ifname, 'peer', peer, 'public_key']) -        if public_key: -            dict = dict_merge({'peer_remove' : {peer : {'public_key' : public_key[0]}}}, dict) -            wireguard.update(dict) +    if 'peer' in wireguard: +        peer_remove = {} +        for peer, peer_config in wireguard['peer'].items(): +            # T4702: If anything on a peer changes we remove the peer first and re-add it +            if is_node_changed(conf, base + [ifname, 'peer', peer]): +                if 'public_key' in peer_config: +                    peer_remove = dict_merge({'peer_remove' : {peer : peer_config['public_key']}}, peer_remove) +        if peer_remove: +           wireguard.update(peer_remove)      return wireguard @@ -81,12 +80,11 @@ def verify(wireguard):      if 'peer' not in wireguard:          raise ConfigError('At least one Wireguard peer is required!') -    if 'port' in wireguard and wireguard['port_changed']: +    if 'port' in wireguard and 'port_changed' in wireguard:          listen_port = int(wireguard['port'])          if check_port_availability('0.0.0.0', listen_port, 'udp') is not True: -            raise ConfigError( -                f'The UDP port {listen_port} is busy or unavailable and cannot be used for the interface' -            ) +            raise ConfigError(f'UDP port {listen_port} is busy or unavailable and ' +                               'cannot be used for the interface!')      # run checks on individual configured WireGuard peer      for tmp in wireguard['peer']: diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index e75418ba5..8b1a5a720 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -147,14 +147,10 @@ def verify(nat):                  Warning(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system')              addr = dict_search('translation.address', config) -            if addr != None: -                if addr != 'masquerade' and not is_ip_network(addr): -                    for ip in addr.split('-'): -                        if not is_addr_assigned(ip): -                            Warning(f'IP address {ip} does not exist on the system!') -            elif 'exclude' not in config: -                raise ConfigError(f'{err_msg}\n' \ -                                  'translation address not specified') +            if addr != None and addr != 'masquerade' and not is_ip_network(addr): +                for ip in addr.split('-'): +                    if not is_addr_assigned(ip): +                        Warning(f'IP address {ip} does not exist on the system!')              # common rule verification              verify_rule(config, err_msg) @@ -167,14 +163,8 @@ def verify(nat):              if 'inbound_interface' not in config:                  raise ConfigError(f'{err_msg}\n' \                                    'inbound-interface not specified') -            else: -                if config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces(): -                    Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') - - -            if dict_search('translation.address', config) == None and 'exclude' not in config: -                raise ConfigError(f'{err_msg}\n' \ -                                  'translation address not specified') +            elif config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces(): +                Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system')              # common rule verification              verify_rule(config, err_msg) @@ -193,6 +183,9 @@ def verify(nat):      return None  def generate(nat): +    if not os.path.exists(nftables_nat_config): +        nat['first_install'] = True +      render(nftables_nat_config, 'firewall/nftables-nat.j2', nat)      render(nftables_static_nat_conf, 'firewall/nftables-static-nat.j2', nat) @@ -201,7 +194,9 @@ def generate(nat):      if tmp > 0:          raise ConfigError('Configuration file errors encountered!') -    tmp = run(f'nft -c -f {nftables_nat_config}') +    tmp = run(f'nft -c -f {nftables_static_nat_conf}') +    if tmp > 0: +        raise ConfigError('Configuration file errors encountered!')      return None diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index f64102d88..d8f913b0c 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -36,7 +36,7 @@ airbag.enable()  k_mod = ['nft_nat', 'nft_chain_nat'] -nftables_nat66_config = '/tmp/vyos-nat66-rules.nft' +nftables_nat66_config = '/run/nftables_nat66.nft'  ndppd_config = '/run/ndppd/ndppd.conf'  def get_handler(json, chain, target): @@ -147,6 +147,9 @@ def verify(nat):      return None  def generate(nat): +    if not os.path.exists(nftables_nat66_config): +        nat['first_install'] = True +      render(nftables_nat66_config, 'firewall/nftables-nat66.j2', nat, permission=0o755)      render(ndppd_config, 'ndppd/ndppd.conf.j2', nat, permission=0o755)      return None @@ -154,15 +157,15 @@ def generate(nat):  def apply(nat):      if not nat:          return None -    cmd(f'{nftables_nat66_config}') + +    cmd(f'nft -f {nftables_nat66_config}') +      if 'deleted' in nat or not dict_search('source.rule', nat):          cmd('systemctl stop ndppd')          if os.path.isfile(ndppd_config):              os.unlink(ndppd_config)      else:          cmd('systemctl restart ndppd') -    if os.path.isfile(nftables_nat66_config): -        os.unlink(nftables_nat66_config)      return None diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py index 9fddbd2c6..00539b9c7 100755 --- a/src/conf_mode/policy-route.py +++ b/src/conf_mode/policy-route.py @@ -92,7 +92,7 @@ def get_config(config=None):      return policy -def verify_rule(policy, name, rule_conf, ipv6): +def verify_rule(policy, name, rule_conf, ipv6, rule_id):      icmp = 'icmp' if not ipv6 else 'icmpv6'      if icmp in rule_conf:          icmp_defined = False @@ -166,7 +166,7 @@ def verify(policy):              for name, pol_conf in policy[route].items():                  if 'rule' in pol_conf:                      for rule_id, rule_conf in pol_conf['rule'].items(): -                        verify_rule(policy, name, rule_conf, ipv6) +                        verify_rule(policy, name, rule_conf, ipv6, rule_id)      for ifname, if_policy in policy['interfaces'].items():          name = dict_search_args(if_policy, 'route') diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index 5dafd26d0..cb8ea3be4 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -203,6 +203,28 @@ def verify(isis):          if list(set(global_range) & set(local_range)):              raise ConfigError(f'Segment-Routing Global Block ({g_low_label_value}/{g_high_label_value}) '\                                f'conflicts with Local Block ({l_low_label_value}/{l_high_label_value})!') +         +    # Check for a blank or invalid value per prefix +    if dict_search('segment_routing.prefix', isis): +        for prefix, prefix_config in isis['segment_routing']['prefix'].items(): +            if 'absolute' in prefix_config: +                if prefix_config['absolute'].get('value') is None: +                    raise ConfigError(f'Segment routing prefix {prefix} absolute value cannot be blank.') +            elif 'index' in prefix_config: +                if prefix_config['index'].get('value') is None: +                    raise ConfigError(f'Segment routing prefix {prefix} index value cannot be blank.') + +    # Check for explicit-null and no-php-flag configured at the same time per prefix +    if dict_search('segment_routing.prefix', isis): +        for prefix, prefix_config in isis['segment_routing']['prefix'].items(): +            if 'absolute' in prefix_config: +                if ("explicit_null" in prefix_config['absolute']) and ("no_php_flag" in prefix_config['absolute']):  +                    raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ +                                      f'and no-php-flag configured at the same time.') +            elif 'index' in prefix_config: +                if ("explicit_null" in prefix_config['index']) and ("no_php_flag" in prefix_config['index']): +                    raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ +                                      f'and no-php-flag configured at the same time.')      return None diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 61f484129..e9afd6a55 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -15,266 +15,34 @@  # along with this program.  If not, see <http://www.gnu.org/licenses/>.  import os -import re -from copy import deepcopy -from stat import S_IRUSR, S_IWUSR, S_IRGRP  from sys import exit  from vyos.config import Config +from vyos.configdict import get_accel_dict +from vyos.configverify import verify_accel_ppp_base_service +from vyos.configverify import verify_interface_exists  from vyos.template import render -from vyos.template import is_ipv4 -from vyos.template import is_ipv6 -from vyos.util import call, get_half_cpus +from vyos.util import call +from vyos.util import dict_search  from vyos import ConfigError -  from vyos import airbag  airbag.enable()  ipoe_conf = '/run/accel-pppd/ipoe.conf'  ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets' -default_config_data = { -    'auth_mode': 'local', -    'auth_interfaces': [], -    'chap_secrets_file': ipoe_chap_secrets, # used in Jinja2 template -    'interfaces': [], -    'dnsv4': [], -    'dnsv6': [], -    'client_named_ip_pool': [], -    'client_ipv6_pool': [], -    'client_ipv6_delegate_prefix': [], -    'radius_server': [], -    'radius_acct_inter_jitter': '', -    'radius_acct_tmo': '3', -    'radius_max_try': '3', -    'radius_timeout': '3', -    'radius_nas_id': '', -    'radius_nas_ip': '', -    'radius_source_address': '', -    'radius_shaper_attr': '', -    'radius_shaper_enable': False, -    'radius_shaper_multiplier': '', -    'radius_shaper_vendor': '', -    'radius_dynamic_author': '', -    'thread_cnt': get_half_cpus() -} -  def get_config(config=None):      if config:          conf = config      else:          conf = Config() -    base_path = ['service', 'ipoe-server'] -    if not conf.exists(base_path): +    base = ['service', 'ipoe-server'] +    if not conf.exists(base):          return None -    conf.set_level(base_path) -    ipoe = deepcopy(default_config_data) - -    for interface in conf.list_nodes(['interface']): -        tmp  = { -            'mode': 'L2', -            'name': interface, -            'shared': '1', -            # may need a config option, can be dhcpv4 or up for unclassified pkts -            'sess_start': 'dhcpv4', -            'range': None, -            'ifcfg': '1', -            'vlan_mon': [] -        } - -        conf.set_level(base_path + ['interface', interface]) - -        if conf.exists(['network-mode']): -            tmp['mode'] = conf.return_value(['network-mode']) - -        if conf.exists(['network']): -            mode = conf.return_value(['network']) -            if mode == 'vlan': -                tmp['shared'] = '0' - -                if conf.exists(['vlan-id']): -                    tmp['vlan_mon'] += conf.return_values(['vlan-id']) - -                if conf.exists(['vlan-range']): -                    tmp['vlan_mon'] += conf.return_values(['vlan-range']) - -        if conf.exists(['client-subnet']): -            tmp['range'] = conf.return_value(['client-subnet']) - -        ipoe['interfaces'].append(tmp) - -    conf.set_level(base_path) - -    if conf.exists(['name-server']): -        for name_server in conf.return_values(['name-server']): -            if is_ipv4(name_server): -                ipoe['dnsv4'].append(name_server) -            else: -                ipoe['dnsv6'].append(name_server) - -    if conf.exists(['authentication', 'mode']): -        ipoe['auth_mode'] = conf.return_value(['authentication', 'mode']) - -    if conf.exists(['authentication', 'interface']): -        for interface in conf.list_nodes(['authentication', 'interface']): -            tmp = { -                'name': interface, -                'mac': [] -            } -            for mac in conf.list_nodes(['authentication', 'interface', interface, 'mac-address']): -                client = { -                    'address': mac, -                    'rate_download': '', -                    'rate_upload': '', -                    'vlan_id': '' -                } -                conf.set_level(base_path + ['authentication', 'interface', interface, 'mac-address', mac]) - -                if conf.exists(['rate-limit', 'download']): -                    client['rate_download'] = conf.return_value(['rate-limit', 'download']) - -                if conf.exists(['rate-limit', 'upload']): -                    client['rate_upload'] = conf.return_value(['rate-limit', 'upload']) - -                if conf.exists(['vlan-id']): -                    client['vlan'] = conf.return_value(['vlan-id']) - -                tmp['mac'].append(client) - -            ipoe['auth_interfaces'].append(tmp) - -    conf.set_level(base_path) - -    # -    # authentication mode radius servers and settings -    if conf.exists(['authentication', 'mode', 'radius']): -        for server in conf.list_nodes(['authentication', 'radius', 'server']): -            radius = { -                'server' : server, -                'key' : '', -                'fail_time' : 0, -                'port' : '1812', -                'acct_port' : '1813' -            } - -            conf.set_level(base_path + ['authentication', 'radius', 'server', server]) - -            if conf.exists(['fail-time']): -                radius['fail_time'] = conf.return_value(['fail-time']) - -            if conf.exists(['port']): -                radius['port'] = conf.return_value(['port']) - -            if conf.exists(['acct-port']): -                radius['acct_port'] = conf.return_value(['acct-port']) - -            if conf.exists(['key']): -                radius['key'] = conf.return_value(['key']) - -            if not conf.exists(['disable']): -                ipoe['radius_server'].append(radius) - -    # -    # advanced radius-setting -    conf.set_level(base_path + ['authentication', 'radius']) - -    if conf.exists(['acct-interim-jitter']): -        ipoe['radius_acct_inter_jitter'] = conf.return_value(['acct-interim-jitter']) - -    if conf.exists(['acct-timeout']): -        ipoe['radius_acct_tmo'] = conf.return_value(['acct-timeout']) - -    if conf.exists(['max-try']): -        ipoe['radius_max_try'] = conf.return_value(['max-try']) - -    if conf.exists(['timeout']): -        ipoe['radius_timeout'] = conf.return_value(['timeout']) - -    if conf.exists(['nas-identifier']): -        ipoe['radius_nas_id'] = conf.return_value(['nas-identifier']) - -    if conf.exists(['nas-ip-address']): -        ipoe['radius_nas_ip'] = conf.return_value(['nas-ip-address']) - -    if conf.exists(['rate-limit', 'attribute']): -        ipoe['radius_shaper_attr'] = conf.return_value(['rate-limit', 'attribute']) - -    if conf.exists(['rate-limit', 'enable']): -        ipoe['radius_shaper_enable'] = True - -    if conf.exists(['rate-limit', 'multiplier']): -        ipoe['radius_shaper_multiplier'] = conf.return_value(['rate-limit', 'multiplier']) - -    if conf.exists(['rate-limit', 'vendor']): -        ipoe['radius_shaper_vendor'] = conf.return_value(['rate-limit', 'vendor']) - -    if conf.exists(['source-address']): -        ipoe['radius_source_address'] = conf.return_value(['source-address']) - -    # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) -    if conf.exists(['dynamic-author']): -        dae = { -            'port' : '', -            'server' : '', -            'key' : '' -        } - -        if conf.exists(['dynamic-author', 'server']): -            dae['server'] = conf.return_value(['dynamic-author', 'server']) - -        if conf.exists(['dynamic-author', 'port']): -            dae['port'] = conf.return_value(['dynamic-author', 'port']) - -        if conf.exists(['dynamic-author', 'key']): -            dae['key'] = conf.return_value(['dynamic-author', 'key']) - -        ipoe['radius_dynamic_author'] = dae - - -    conf.set_level(base_path) -    # Named client-ip-pool -    if conf.exists(['client-ip-pool', 'name']): -        for name in conf.list_nodes(['client-ip-pool', 'name']): -            tmp = { -                'name': name, -                'gateway_address': '', -                'subnet': '' -            } - -            if conf.exists(['client-ip-pool', 'name', name, 'gateway-address']): -                tmp['gateway_address'] += conf.return_value(['client-ip-pool', 'name', name, 'gateway-address']) -            if conf.exists(['client-ip-pool', 'name', name, 'subnet']): -                tmp['subnet'] += conf.return_value(['client-ip-pool', 'name', name, 'subnet']) - -            ipoe['client_named_ip_pool'].append(tmp) - -    if conf.exists(['client-ipv6-pool', 'prefix']): -        for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']): -            tmp = { -                'prefix': prefix, -                'mask': '64' -            } - -            if conf.exists(['client-ipv6-pool', 'prefix', prefix, 'mask']): -                tmp['mask'] = conf.return_value(['client-ipv6-pool', 'prefix', prefix, 'mask']) - -            ipoe['client_ipv6_pool'].append(tmp) - - -    if conf.exists(['client-ipv6-pool', 'delegate']): -        for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']): -            tmp = { -                'prefix': prefix, -                'mask': '' -            } - -            if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']): -                tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']) - -            ipoe['client_ipv6_delegate_prefix'].append(tmp) - +    # retrieve common dictionary keys +    ipoe = get_accel_dict(conf, base, ipoe_chap_secrets)      return ipoe @@ -282,26 +50,17 @@ def verify(ipoe):      if not ipoe:          return None -    if not ipoe['interfaces']: +    if 'interface' not in ipoe:          raise ConfigError('No IPoE interface configured') -    if len(ipoe['dnsv4']) > 2: -        raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') - -    if len(ipoe['dnsv6']) > 3: -        raise ConfigError('Not more then three IPv6 DNS name-servers can be configured') - -    if ipoe['auth_mode'] == 'radius': -        if len(ipoe['radius_server']) == 0: -            raise ConfigError('RADIUS authentication requires at least one server') +    for interface in ipoe['interface']: +        verify_interface_exists(interface) -        for radius in ipoe['radius_server']: -            if not radius['key']: -                server = radius['server'] -                raise ConfigError(f'Missing RADIUS secret key for server "{ server }"') +    #verify_accel_ppp_base_service(ipoe, local_users=False) -    if ipoe['client_ipv6_delegate_prefix'] and not ipoe['client_ipv6_pool']: -        raise ConfigError('IPoE IPv6 deletate-prefix requires IPv6 prefix to be configured!') +    if 'client_ipv6_pool' in ipoe: +        if 'delegate' in ipoe['client_ipv6_pool'] and 'prefix' not in ipoe['client_ipv6_pool']: +            raise ConfigError('IPoE IPv6 deletate-prefix requires IPv6 prefix to be configured!')      return None @@ -312,27 +71,23 @@ def generate(ipoe):      render(ipoe_conf, 'accel-ppp/ipoe.config.j2', ipoe) -    if ipoe['auth_mode'] == 'local': -        render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.j2', ipoe) -        os.chmod(ipoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) - -    else: -        if os.path.exists(ipoe_chap_secrets): -             os.unlink(ipoe_chap_secrets) - +    if dict_search('authentication.mode', ipoe) == 'local': +        render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.j2', +               ipoe, permission=0o640)      return None  def apply(ipoe): +    systemd_service = 'accel-ppp@ipoe.service'      if ipoe == None: -        call('systemctl stop accel-ppp@ipoe.service') +        call(f'systemctl stop {systemd_service}')          for file in [ipoe_conf, ipoe_chap_secrets]:              if os.path.exists(file):                  os.unlink(file)          return None -    call('systemctl restart accel-ppp@ipoe.service') +    call(f'systemctl reload-or-restart {systemd_service}')  if __name__ == '__main__':      try: diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index 6086ef859..ba0249efd 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -21,14 +21,12 @@ from sys import exit  from vyos.config import Config  from vyos.configdict import get_accel_dict  from vyos.configverify import verify_accel_ppp_base_service +from vyos.configverify import verify_interface_exists  from vyos.template import render  from vyos.util import call  from vyos.util import dict_search -from vyos.util import get_interface_config  from vyos import ConfigError  from vyos import airbag -from vyos.range_regex import range_to_regex -  airbag.enable()  pppoe_conf = r'/run/accel-pppd/pppoe.conf' @@ -54,15 +52,14 @@ def verify(pppoe):      verify_accel_ppp_base_service(pppoe)      if 'wins_server' in pppoe and len(pppoe['wins_server']) > 2: -        raise ConfigError('Not more then two IPv4 WINS name-servers can be configured') +        raise ConfigError('Not more then two WINS name-servers can be configured')      if 'interface' not in pppoe:          raise ConfigError('At least one listen interface must be defined!')      # Check is interface exists in the system -    for iface in pppoe['interface']: -        if not get_interface_config(iface): -            raise ConfigError(f'Interface {iface} does not exist!') +    for interface in pppoe['interface']: +        verify_interface_exists(interface)      # local ippool and gateway settings config checks      if not (dict_search('client_ip_pool.subnet', pppoe) or @@ -81,35 +78,24 @@ def generate(pppoe):      if not pppoe:          return None -    # Generate special regex for dynamic interfaces -    for iface in pppoe['interface']: -        if 'vlan_range' in pppoe['interface'][iface]: -            pppoe['interface'][iface]['regex'] = [] -            for vlan_range in pppoe['interface'][iface]['vlan_range']: -                pppoe['interface'][iface]['regex'].append(range_to_regex(vlan_range)) -      render(pppoe_conf, 'accel-ppp/pppoe.config.j2', pppoe)      if dict_search('authentication.mode', pppoe) == 'local':          render(pppoe_chap_secrets, 'accel-ppp/chap-secrets.config_dict.j2',                 pppoe, permission=0o640) -    else: -        if os.path.exists(pppoe_chap_secrets): -            os.unlink(pppoe_chap_secrets) -      return None  def apply(pppoe): +    systemd_service = 'accel-ppp@pppoe.service'      if not pppoe: -        call('systemctl stop accel-ppp@pppoe.service') +        call(f'systemctl stop {systemd_service}')          for file in [pppoe_conf, pppoe_chap_secrets]:              if os.path.exists(file):                  os.unlink(file) -          return None -    call('systemctl restart accel-ppp@pppoe.service') +    call(f'systemctl reload-or-restart {systemd_service}')  if __name__ == '__main__':      try: diff --git a/src/conf_mode/system_update_check.py b/src/conf_mode/system_update_check.py new file mode 100755 index 000000000..08ecfcb81 --- /dev/null +++ b/src/conf_mode/system_update_check.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import os +import json +import jmespath + +from pathlib import Path +from sys import exit + +from vyos.config import Config +from vyos.util import call +from vyos import ConfigError +from vyos import airbag +airbag.enable() + + +base = ['system', 'update-check'] +service_name = 'vyos-system-update' +service_conf = Path(f'/run/{service_name}.conf') +motd_file = Path('/run/motd.d/10-vyos-update') + + +def get_config(config=None): +    if config: +        conf = config +    else: +        conf = Config() + +    if not conf.exists(base): +        return None + +    config = conf.get_config_dict(base, key_mangling=('-', '_'), +                                  get_first_key=True, no_tag_node_value_mangle=True) + +    return config + + +def verify(config): +    # bail out early - looks like removal from running config +    if config is None: +        return + +    if 'url' not in config: +        raise ConfigError('URL is required!') + + +def generate(config): +    # bail out early - looks like removal from running config +    if config is None: +        # Remove old config and return +        service_conf.unlink(missing_ok=True) +        # MOTD used in /run/motd.d/10-update +        motd_file.unlink(missing_ok=True) +        return None + +    # Write configuration file +    conf_json = json.dumps(config, indent=4) +    service_conf.write_text(conf_json) + +    return None + + +def apply(config): +    if config: +        if 'auto_check' in config: +            call(f'systemctl restart {service_name}.service') +    else: +        call(f'systemctl stop {service_name}.service') + + +if __name__ == '__main__': +    try: +        c = get_config() +        verify(c) +        generate(c) +        apply(c) +    except ConfigError as e: +        print(e) +        exit(1) diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 5ca32d23e..c9061366d 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -16,6 +16,7 @@  import ipaddress  import os +import re  from sys import exit  from time import sleep @@ -348,6 +349,14 @@ def verify(ipsec):      if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']:          for peer, peer_conf in ipsec['site_to_site']['peer'].items():              has_default_esp = False +            # Peer name it is swanctl connection name and shouldn't contain dots or colons, T4118 +            if bool(re.search(':|\.', peer)): +                raise ConfigError(f'Incorrect peer name "{peer}" ' +                                  f'Peer name can contain alpha-numeric letters, hyphen and underscore') + +            if 'remote_address' not in peer_conf: +                print(f'You should set correct remote-address "peer {peer} remote-address x.x.x.x"\n') +              if 'default_esp_group' in peer_conf:                  has_default_esp = True                  if 'esp_group' not in ipsec or peer_conf['default_esp_group'] not in ipsec['esp_group']: diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 240546817..c050b796b 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -58,15 +58,16 @@ def get_config():      default_values = defaults(base)      ocserv = dict_merge(default_values, ocserv) -    # workaround a "know limitation" - https://phabricator.vyos.net/T2665 -    del ocserv['authentication']['local_users']['username']['otp'] -    if not ocserv["authentication"]["local_users"]["username"]: -        raise ConfigError('openconnect mode local required at least one user') -    default_ocserv_usr_values = default_values['authentication']['local_users']['username']['otp'] -    for user, params in ocserv['authentication']['local_users']['username'].items(): -        # Not every configuration requires OTP settings -        if ocserv['authentication']['local_users']['username'][user].get('otp'): -            ocserv['authentication']['local_users']['username'][user]['otp'] = dict_merge(default_ocserv_usr_values, ocserv['authentication']['local_users']['username'][user]['otp']) +    if "local" in ocserv["authentication"]["mode"]: +        # workaround a "know limitation" - https://phabricator.vyos.net/T2665 +        del ocserv['authentication']['local_users']['username']['otp'] +        if not ocserv["authentication"]["local_users"]["username"]: +            raise ConfigError('openconnect mode local required at least one user') +        default_ocserv_usr_values = default_values['authentication']['local_users']['username']['otp'] +        for user, params in ocserv['authentication']['local_users']['username'].items(): +            # Not every configuration requires OTP settings +            if ocserv['authentication']['local_users']['username'][user].get('otp'): +                ocserv['authentication']['local_users']['username'][user]['otp'] = dict_merge(default_ocserv_usr_values, ocserv['authentication']['local_users']['username'][user]['otp'])      if ocserv:          ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), @@ -80,9 +81,10 @@ def verify(ocserv):      # Check if listen-ports not binded other services      # It can be only listen by 'ocserv-main'      for proto, port in ocserv.get('listen_ports').items(): -        if check_port_availability('0.0.0.0', int(port), proto) is not True and \ +        if check_port_availability(ocserv['listen_address'], int(port), proto) is not True and \                  not is_listen_port_bind_service(int(port), 'ocserv-main'):              raise ConfigError(f'"{proto}" port "{port}" is used by another service') +      # Check authentication      if "authentication" in ocserv:          if "mode" in ocserv["authentication"]: diff --git a/src/migration-scripts/ipoe-server/0-to-1 b/src/migration-scripts/ipoe-server/0-to-1 index f328ebced..d768758ba 100755 --- a/src/migration-scripts/ipoe-server/0-to-1 +++ b/src/migration-scripts/ipoe-server/0-to-1 @@ -1,6 +1,6 @@  #!/usr/bin/env python3  # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2022 VyOS maintainers and contributors  #  # This program is free software; you can redistribute it and/or modify  # it under the terms of the GNU General Public License version 2 or later as @@ -14,8 +14,11 @@  # You should have received a copy of the GNU General Public License  # along with this program.  If not, see <http://www.gnu.org/licenses/>. -# - remove primary/secondary identifier from nameserver -# - Unifi RADIUS configuration by placing it all under "authentication radius" node +# - T4703: merge vlan-id and vlan-range to vlan CLI node + +# L2|L3 -> l2|l3 +# mac-address -> mac +# network-mode -> mode  import os  import sys @@ -37,97 +40,35 @@ base = ['service', 'ipoe-server']  if not config.exists(base):      # Nothing to do      exit(0) -else: - -    # Migrate IPv4 DNS servers -    dns_base = base + ['dns-server'] -    if config.exists(dns_base): -        for server in ['server-1', 'server-2']: -          if config.exists(dns_base + [server]): -            dns = config.return_value(dns_base + [server]) -            config.set(base + ['name-server'], value=dns, replace=False) - -        config.delete(dns_base) - -    # Migrate IPv6 DNS servers -    dns_base = base + ['dnsv6-server'] -    if config.exists(dns_base): -        for server in ['server-1', 'server-2', 'server-3']: -          if config.exists(dns_base + [server]): -            dns = config.return_value(dns_base + [server]) -            config.set(base + ['name-server'], value=dns, replace=False) - -        config.delete(dns_base) - -    # Migrate radius-settings node to RADIUS and use this as base for the -    # later migration of the RADIUS servers - this will save a lot of code -    radius_settings = base + ['authentication', 'radius-settings'] -    if config.exists(radius_settings): -        config.rename(radius_settings, 'radius') - -    # Migrate RADIUS dynamic author / change of authorisation server -    dae_old = base + ['authentication', 'radius', 'dae-server'] -    if config.exists(dae_old): -        config.rename(dae_old, 'dynamic-author') -        dae_new = base + ['authentication', 'radius', 'dynamic-author'] - -        if config.exists(dae_new + ['ip-address']): -            config.rename(dae_new + ['ip-address'], 'server') - -        if config.exists(dae_new + ['secret']): -            config.rename(dae_new + ['secret'], 'key') -    # Migrate RADIUS server -    radius_server = base + ['authentication', 'radius-server'] -    if config.exists(radius_server): -        new_base = base + ['authentication', 'radius', 'server'] -        config.set(new_base) -        config.set_tag(new_base) -        for server in config.list_nodes(radius_server): -            old_base = radius_server + [server] -            config.copy(old_base, new_base + [server]) - -            # migrate key -            if config.exists(new_base + [server, 'secret']): -                config.rename(new_base + [server, 'secret'], 'key') - -            # remove old req-limit node -            if config.exists(new_base + [server, 'req-limit']): -                config.delete(new_base + [server, 'req-limit']) - -        config.delete(radius_server) - -    # Migrate IPv6 prefixes -    ipv6_base = base + ['client-ipv6-pool'] -    if config.exists(ipv6_base + ['prefix']): -        prefix_old = config.return_values(ipv6_base + ['prefix']) -        # delete old prefix CLI nodes -        config.delete(ipv6_base + ['prefix']) -        # create ned prefix tag node -        config.set(ipv6_base + ['prefix']) -        config.set_tag(ipv6_base + ['prefix']) - -        for p in prefix_old: -            prefix = p.split(',')[0] -            mask = p.split(',')[1] -            config.set(ipv6_base + ['prefix', prefix, 'mask'], value=mask) - -    if config.exists(ipv6_base + ['delegate-prefix']): -        prefix_old = config.return_values(ipv6_base + ['delegate-prefix']) -        # delete old delegate prefix CLI nodes -        config.delete(ipv6_base + ['delegate-prefix']) -        # create ned delegation tag node -        config.set(ipv6_base + ['delegate']) -        config.set_tag(ipv6_base + ['delegate']) - -        for p in prefix_old: -            prefix = p.split(',')[0] -            mask = p.split(',')[1] -            config.set(ipv6_base + ['delegate', prefix, 'delegation-prefix'], value=mask) - -    try: -        with open(file_name, 'w') as f: -            f.write(config.to_string()) -    except OSError as e: -        print("Failed to save the modified config: {}".format(e)) -        exit(1) +if config.exists(base + ['authentication', 'interface']): +    for interface in config.list_nodes(base + ['authentication', 'interface']): +        config.rename(base + ['authentication', 'interface', interface, 'mac-address'], 'mac') + +        mac_base = base + ['authentication', 'interface', interface, 'mac'] +        for mac in config.list_nodes(mac_base): +            vlan_config = mac_base + [mac, 'vlan-id'] +            if config.exists(vlan_config): +                config.rename(vlan_config, 'vlan') + +for interface in config.list_nodes(base + ['interface']): +    base_path = base + ['interface', interface] +    for vlan in ['vlan-id', 'vlan-range']: +        if config.exists(base_path + [vlan]): +            print(interface, vlan) +            for tmp in config.return_values(base_path + [vlan]): +                config.set(base_path + ['vlan'], value=tmp, replace=False) +            config.delete(base_path + [vlan]) + +    if config.exists(base_path + ['network-mode']): +        tmp = config.return_value(base_path + ['network-mode']) +        config.delete(base_path + ['network-mode']) +        # Change L2|L3 to lower case l2|l3 +        config.set(base_path + ['mode'], value=tmp.lower()) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print("Failed to save the modified config: {}".format(e)) +    exit(1) diff --git a/src/migration-scripts/ipsec/9-to-10 b/src/migration-scripts/ipsec/9-to-10 new file mode 100755 index 000000000..1254104cb --- /dev/null +++ b/src/migration-scripts/ipsec/9-to-10 @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import re + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 + + +if (len(argv) < 1): +    print("Must specify file name!") +    exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +base = ['vpn', 'ipsec'] +config = ConfigTree(config_file) + +if not config.exists(base): +    # Nothing to do +    exit(0) + +# IKE changes, T4118: +if config.exists(base + ['ike-group']): +    for ike_group in config.list_nodes(base + ['ike-group']): +        # replace 'ipsec ike-group <tag> mobike disable' +        #      => 'ipsec ike-group <tag> disable-mobike' +        mobike = base + ['ike-group', ike_group, 'mobike'] +        if config.exists(mobike): +            if config.return_value(mobike) == 'disable': +                config.set(base + ['ike-group', ike_group, 'disable-mobike']) +            config.delete(mobike) + +        # replace 'ipsec ike-group <tag> ikev2-reauth yes' +        #      => 'ipsec ike-group <tag> ikev2-reauth' +        reauth = base + ['ike-group', ike_group, 'ikev2-reauth'] +        if config.exists(reauth): +            if config.return_value(reauth) == 'yes': +                config.delete(reauth) +                config.set(reauth) +            else: +                config.delete(reauth) + +# ESP changes +# replace 'ipsec esp-group <tag> compression enable' +#      => 'ipsec esp-group <tag> compression' +if config.exists(base + ['esp-group']): +    for esp_group in config.list_nodes(base + ['esp-group']): +        compression = base + ['esp-group', esp_group, 'compression'] +        if config.exists(compression): +            if config.return_value(compression) == 'enable': +                config.delete(compression) +                config.set(compression) +            else: +                config.delete(compression) + +# PEER changes +if config.exists(base + ['site-to-site', 'peer']): +    for peer in config.list_nodes(base + ['site-to-site', 'peer']): +        peer_base = base + ['site-to-site', 'peer', peer] + +        # replace: 'peer <tag> id x' +        #       => 'peer <tag> local-id x' +        if config.exists(peer_base + ['authentication', 'id']): +            config.rename(peer_base + ['authentication', 'id'], 'local-id') + +        # For the peer '@foo' set remote-id 'foo' if remote-id is not defined +        if peer.startswith('@'): +            if not config.exists(peer_base + ['authentication', 'remote-id']): +                tmp = peer.replace('@', '') +                config.set(peer_base + ['authentication', 'remote-id'], value=tmp) + +        # replace: 'peer <tag> force-encapsulation enable' +        #       => 'peer <tag> force-udp-encapsulation' +        force_enc = peer_base + ['force-encapsulation'] +        if config.exists(force_enc): +            if config.return_value(force_enc) == 'enable': +                config.delete(force_enc) +                config.set(peer_base + ['force-udp-encapsulation']) +            else: +                config.delete(force_enc) + +        # add option: 'peer <tag> remote-address x.x.x.x' +        remote_address = peer +        if peer.startswith('@'): +            remote_address = 'any' +        config.set(peer_base + ['remote-address'], value=remote_address) +        # Peer name it is swanctl connection name and shouldn't contain dots or colons +        # rename peer: +        #   peer 192.0.2.1   => peer peer_192-0-2-1 +        #   peer 2001:db8::2 => peer peer_2001-db8--2 +        #   peer @foo        => peer peer_foo +        re_peer_name = re.sub(':|\.', '-', peer) +        if re_peer_name.startswith('@'): +            re_peer_name = re.sub('@', '', re_peer_name) +        new_peer_name = f'peer_{re_peer_name}' + +        config.rename(peer_base, new_peer_name) + +# remote-access/road-warrior changes +if config.exists(base + ['remote-access', 'connection']): +    for connection in config.list_nodes(base + ['remote-access', 'connection']): +        ra_base = base + ['remote-access', 'connection', connection] +        # replace: 'remote-access connection <tag> authentication id x' +        #       => 'remote-access connection <tag> authentication local-id x' +        if config.exists(ra_base + ['authentication', 'id']): +            config.rename(ra_base + ['authentication', 'id'], 'local-id') + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print(f'Failed to save the modified config: {e}') +    exit(1) diff --git a/src/migration-scripts/pppoe-server/5-to-6 b/src/migration-scripts/pppoe-server/5-to-6 new file mode 100755 index 000000000..e4888f4db --- /dev/null +++ b/src/migration-scripts/pppoe-server/5-to-6 @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +# - T4703: merge vlan-id and vlan-range to vlan CLI node + +from vyos.configtree import ConfigTree +from sys import argv +from sys import exit + +if (len(argv) < 1): +    print("Must specify file name!") +    exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +config = ConfigTree(config_file) +base_path = ['service', 'pppoe-server', 'interface'] +if not config.exists(base_path): +    # Nothing to do +    exit(0) + +for interface in config.list_nodes(base_path): +    for vlan in ['vlan-id', 'vlan-range']: +        if config.exists(base_path + [interface, vlan]): +            print(interface, vlan) +            for tmp in config.return_values(base_path + [interface, vlan]): +                config.set(base_path + [interface, 'vlan'], value=tmp, replace=False) +            config.delete(base_path + [interface, vlan]) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print(f'Failed to save the modified config: {e}') +    exit(1) + diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py index 1339d5b92..a0496dedb 100755 --- a/src/op_mode/nat.py +++ b/src/op_mode/nat.py @@ -60,7 +60,7 @@ def _get_json_data(direction, family):      if direction == 'destination':          chain = 'PREROUTING'      family = 'ip6' if family == 'inet6' else 'ip' -    return cmd(f'sudo nft --json list chain {family} nat {chain}') +    return cmd(f'sudo nft --json list chain {family} vyos_nat {chain}')  def _get_raw_data_rules(direction, family): diff --git a/src/op_mode/show_nat66_statistics.py b/src/op_mode/show_nat66_statistics.py index bc81692ae..cb10aed9f 100755 --- a/src/op_mode/show_nat66_statistics.py +++ b/src/op_mode/show_nat66_statistics.py @@ -44,7 +44,7 @@ group.add_argument("--destination", help="Show statistics for configured destina  args = parser.parse_args()  if args.source or args.destination: -    tmp = cmd('sudo nft -j list table ip6 nat') +    tmp = cmd('sudo nft -j list table ip6 vyos_nat')      tmp = json.loads(tmp)      source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }" diff --git a/src/op_mode/show_nat_statistics.py b/src/op_mode/show_nat_statistics.py index c568c8305..be41e083b 100755 --- a/src/op_mode/show_nat_statistics.py +++ b/src/op_mode/show_nat_statistics.py @@ -44,7 +44,7 @@ group.add_argument("--destination", help="Show statistics for configured destina  args = parser.parse_args()  if args.source or args.destination: -    tmp = cmd('sudo nft -j list table ip nat') +    tmp = cmd('sudo nft -j list table ip vyos_nat')      tmp = json.loads(tmp)      source = r"nftables[?rule.chain=='POSTROUTING'].rule.{chain: chain, handle: handle, comment: comment, counter: expr[].counter | [0], interface: expr[].match.right | [0] }" diff --git a/src/op_mode/system.py b/src/op_mode/system.py new file mode 100755 index 000000000..11a3a8730 --- /dev/null +++ b/src/op_mode/system.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import jmespath +import json +import sys +import requests +import typing + +from sys import exit + +from vyos.configquery import ConfigTreeQuery + +import vyos.opmode +import vyos.version + +config = ConfigTreeQuery() +base = ['system', 'update-check'] + + +def _compare_version_raw(): +    url = config.value(base + ['url']) +    local_data = vyos.version.get_full_version_data() +    remote_data = vyos.version.get_remote_version(url) +    if not remote_data: +        return {"error": True, +                "reason": "Unable to get remote version"} +    if local_data.get('version') and remote_data: +        local_version = local_data.get('version') +        remote_version = jmespath.search('[0].version', remote_data) +        image_url = jmespath.search('[0].url', remote_data) +        if local_data.get('version') != remote_version: +            return {"error": False, +                    "update_available": True, +                    "local_version": local_version, +                    "remote_version": remote_version, +                    "url": image_url} +        return {"update_available": False, +                "local_version": local_version, +                "remote_version": remote_version} + + +def _formatted_compare_version(data): +    local_version = data.get('local_version') +    remote_version = data.get('remote_version') +    url = data.get('url') +    if {'update_available','local_version', 'remote_version', 'url'} <= set(data): +        return f'Current version: {local_version}\n\nUpdate available: {remote_version}\nUpdate URL: {url}' +    elif local_version == remote_version and remote_version is not None: +        return f'No available updates for your system \n' \ +               f'current version: {local_version}\nremote version: {remote_version}' +    else: +        return 'Update not found' + + +def _verify(): +    if not config.exists(base): +        return False +    return True + + +def show_update(raw: bool): +    if not _verify(): +        raise vyos.opmode.UnconfiguredSubsystem("system update-check not configured") +    data = _compare_version_raw() +    if raw: +        return data +    else: +        return _formatted_compare_version(data) + + +if __name__ == '__main__': +    try: +        res = vyos.opmode.run(sys.modules[__name__]) +        if res: +            print(res) +    except (ValueError, vyos.opmode.Error) as e: +        print(e) +        sys.exit(1) diff --git a/src/system/vyos-system-update-check.py b/src/system/vyos-system-update-check.py new file mode 100755 index 000000000..c9597721b --- /dev/null +++ b/src/system/vyos-system-update-check.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +import argparse +import json +import jmespath + +from pathlib import Path +from sys import exit +from time import sleep + +from vyos.util import call + +import vyos.version + +motd_file = Path('/run/motd.d/10-vyos-update') + + +if __name__ == '__main__': +    # Parse command arguments and get config +    parser = argparse.ArgumentParser() +    parser.add_argument('-c', +                        '--config', +                        action='store', +                        help='Path to system-update-check configuration', +                        required=True, +                        type=Path) + +    args = parser.parse_args() +    try: +        config_path = Path(args.config) +        config = json.loads(config_path.read_text()) +    except Exception as err: +        print( +            f'Configuration file "{config_path}" does not exist or malformed: {err}' +        ) +        exit(1) + +    url_json = config.get('url') +    local_data = vyos.version.get_full_version_data() +    local_version = local_data.get('version') + +    while True: +        remote_data = vyos.version.get_remote_version(url_json) +        if remote_data: +            url = jmespath.search('[0].url', remote_data) +            remote_version = jmespath.search('[0].version', remote_data) +            if local_version != remote_version and remote_version: +                call(f'wall -n "Update available: {remote_version} \nUpdate URL: {url}"') +                # MOTD used in /run/motd.d/10-update +                motd_file.parent.mkdir(exist_ok=True) +                motd_file.write_text(f'---\n' +                                     f'Current version: {local_version}\n' +                                     f'Update available: \033[1;34m{remote_version}\033[0m\n' +                                     f'---\n') +        # Check every 12 hours +        sleep(43200) diff --git a/src/systemd/vyos-system-update.service b/src/systemd/vyos-system-update.service new file mode 100644 index 000000000..032e5a14c --- /dev/null +++ b/src/systemd/vyos-system-update.service @@ -0,0 +1,11 @@ +[Unit] +Description=VyOS system udpate-check service +After=network.target vyos-router.service + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/python3 /usr/libexec/vyos/system/vyos-system-update-check.py --config /run/vyos-system-update.conf + +[Install] +WantedBy=multi-user.target diff --git a/src/validators/range b/src/validators/range deleted file mode 100755 index d4c25f3c4..000000000 --- a/src/validators/range +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2021 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program.  If not, see <http://www.gnu.org/licenses/>. - -import re -import sys -import argparse - -class MalformedRange(Exception): -    pass - -def validate_range(value, min=None, max=None): -    try: -        lower, upper = re.match(r'^(\d+)-(\d+)$', value).groups() - -        lower, upper = int(lower), int(upper) - -        if int(lower) > int(upper): -            raise MalformedRange("the lower bound exceeds the upper bound".format(value)) - -        if min is not None: -            if lower < min: -                raise MalformedRange("the lower bound must not be less than {}".format(min)) - -        if max is not None: -            if upper > max: -                raise MalformedRange("the upper bound must not be greater than {}".format(max)) - -    except (AttributeError, ValueError): -        raise MalformedRange("range syntax error") - -parser = argparse.ArgumentParser(description='Range validator.') -parser.add_argument('--min', type=int, action='store') -parser.add_argument('--max', type=int, action='store') -parser.add_argument('value', action='store') - -if __name__ == '__main__': -    args = parser.parse_args() - -    try: -        validate_range(args.value, min=args.min, max=args.max) -    except MalformedRange as e: -        print("Incorrect range '{}': {}".format(args.value, e)) -        sys.exit(1) | 
