diff options
Diffstat (limited to 'src')
-rwxr-xr-x | src/conf_mode/interfaces_openvpn.py | 37 | ||||
-rwxr-xr-x | src/conf_mode/system_option.py | 2 | ||||
-rw-r--r-- | src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper | 18 | ||||
-rw-r--r-- | src/op_mode/ntp.py | 45 | ||||
-rwxr-xr-x | src/op_mode/pki.py | 41 | ||||
-rwxr-xr-x | src/services/vyos-configd | 19 | ||||
-rwxr-xr-x | src/services/vyos-http-api-server | 62 |
7 files changed, 192 insertions, 32 deletions
diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py index 9105ce1f8..8c1213e2b 100755 --- a/src/conf_mode/interfaces_openvpn.py +++ b/src/conf_mode/interfaces_openvpn.py @@ -123,6 +123,18 @@ def get_config(config=None): openvpn['module_load_dco'] = {} break + # Calculate the protocol modifier. This is concatenated to the protocol string to direct + # OpenVPN to use a specific IP protocol version. If unspecified, the kernel decides which + # type of socket to open. In server mode, an additional "ipv6-dual-stack" option forces + # binding the socket in IPv6 mode, which can also receive IPv4 traffic (when using the + # default "ipv6" mode, we specify "bind ipv6only" to disable kernel dual-stack behaviors). + if openvpn['ip_version'] == 'ipv4': + openvpn['protocol_modifier'] = '4' + elif openvpn['ip_version'] in ['ipv6', 'dual-stack']: + openvpn['protocol_modifier'] = '6' + else: + openvpn['protocol_modifier'] = '' + return openvpn def is_ec_private_key(pki, cert_name): @@ -257,6 +269,9 @@ def verify(openvpn): if openvpn['protocol'] == 'tcp-passive': raise ConfigError('Protocol "tcp-passive" is not valid in client mode') + if 'ip_version' in openvpn and openvpn['ip_version'] == 'dual-stack': + raise ConfigError('"ip-version dual-stack" is not supported in client mode') + if dict_search('tls.dh_params', openvpn): raise ConfigError('Cannot specify "tls dh-params" in client mode') @@ -264,6 +279,9 @@ def verify(openvpn): # OpenVPN site-to-site - VERIFY # elif openvpn['mode'] == 'site-to-site': + if 'ip_version' in openvpn and openvpn['ip_version'] == 'dual-stack': + raise ConfigError('"ip-version dual-stack" is not supported in site-to-site mode') + if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn: raise ConfigError('Must specify "local-address" or add interface to bridge') @@ -487,6 +505,25 @@ def verify(openvpn): # not depending on any operation mode # + # verify that local_host/remote_host match with any ip_version override + # specified (if a dns name is specified for remote_host, no attempt is made + # to verify that record resolves to an address of the configured family) + if 'local_host' in openvpn: + if openvpn['ip_version'] == 'ipv4' and is_ipv6(openvpn['local_host']): + raise ConfigError('Cannot use an IPv6 "local-host" with "ip-version ipv4"') + elif openvpn['ip_version'] == 'ipv6' and is_ipv4(openvpn['local_host']): + raise ConfigError('Cannot use an IPv4 "local-host" with "ip-version ipv6"') + elif openvpn['ip_version'] == 'dual-stack': + raise ConfigError('Cannot use "local-host" with "ip-version dual-stack". "dual-stack" is only supported when OpenVPN binds to all available interfaces.') + + if 'remote_host' in openvpn: + remote_hosts = dict_search('remote_host', openvpn) + for remote_host in remote_hosts: + if openvpn['ip_version'] == 'ipv4' and is_ipv6(remote_host): + raise ConfigError('Cannot use an IPv6 "remote-host" with "ip-version ipv4"') + elif openvpn['ip_version'] == 'ipv6' and is_ipv4(remote_host): + raise ConfigError('Cannot use an IPv4 "remote-host" with "ip-version ipv6"') + # verify specified IP address is present on any interface on this system if 'local_host' in openvpn: if not is_addr_assigned(openvpn['local_host']): diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py index d1647e3a1..52d0b7cda 100755 --- a/src/conf_mode/system_option.py +++ b/src/conf_mode/system_option.py @@ -85,6 +85,8 @@ def verify(options): raise ConfigError('No interface with address "{address}" configured!') if 'source_interface' in config: + # verify_source_interface reuires key 'ifname' + config['ifname'] = config['source_interface'] verify_source_interface(config) if 'source_address' in config: address = config['source_address'] diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper index 5d879471d..2a1c5a7b2 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper +++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper @@ -72,6 +72,22 @@ function delroute () { fi } +# try to communicate with vtysh +function vtysh_conf () { + # perform 10 attempts with 1 second delay for retries + for i in {1..10} ; do + if vtysh -c "conf t" -c "$1" ; then + logmsg info "Command was executed successfully via vtysh: \"$1\"" + return 0 + else + logmsg info "Failed to send command to vtysh, retrying in 1 second" + sleep 1 + fi + done + logmsg error "Failed to execute command via vtysh after 10 attempts: \"$1\"" + return 1 +} + # replace ip command with this wrapper function ip () { # pass comand to system `ip` if this is not related to routes change @@ -84,7 +100,7 @@ function ip () { delroute ${@:4} iptovtysh $@ logmsg info "Sending command to vtysh" - vtysh -c "conf t" -c "$VTYSH_CMD" + vtysh_conf "$VTYSH_CMD" else # add ip route to kernel logmsg info "Modifying routes in kernel: \"/usr/sbin/ip $@\"" diff --git a/src/op_mode/ntp.py b/src/op_mode/ntp.py index e14cc46d0..6ec0fedcb 100644 --- a/src/op_mode/ntp.py +++ b/src/op_mode/ntp.py @@ -110,49 +110,62 @@ def _is_configured(): if not config.exists("service ntp"): raise vyos.opmode.UnconfiguredSubsystem("NTP service is not enabled.") +def _extend_command_vrf(): + config = ConfigTreeQuery() + if config.exists('service ntp vrf'): + vrf = config.value('service ntp vrf') + return f'ip vrf exec {vrf} ' + return '' + + def show_activity(raw: bool): _is_configured() command = f'chronyc' if raw: - command += f" -c activity" - return _get_raw_data(command) + command += f" -c activity" + return _get_raw_data(command) else: - command += f" activity" - return cmd(command) + command = _extend_command_vrf() + command + command += f" activity" + return cmd(command) def show_sources(raw: bool): _is_configured() command = f'chronyc' if raw: - command += f" -c sources" - return _get_raw_data(command) + command += f" -c sources" + return _get_raw_data(command) else: - command += f" sources -v" - return cmd(command) + command = _extend_command_vrf() + command + command += f" sources -v" + return cmd(command) def show_tracking(raw: bool): _is_configured() command = f'chronyc' if raw: - command += f" -c tracking" - return _get_raw_data(command) + command += f" -c tracking" + return _get_raw_data(command) else: - command += f" tracking" - return cmd(command) + command = _extend_command_vrf() + command + command += f" tracking" + return cmd(command) def show_sourcestats(raw: bool): _is_configured() command = f'chronyc' if raw: - command += f" -c sourcestats" - return _get_raw_data(command) + command += f" -c sourcestats" + return _get_raw_data(command) else: - command += f" sourcestats -v" - return cmd(command) + command = _extend_command_vrf() + command + command += f" sourcestats -v" + return cmd(command) + if __name__ == '__main__': try: diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py index 84b080023..ab613e5c4 100755 --- a/src/op_mode/pki.py +++ b/src/op_mode/pki.py @@ -316,7 +316,13 @@ def generate_certificate_request(private_key=None, key_type=None, return_request default_values = get_default_values() subject = {} - subject['country'] = ask_input('Enter country code:', default=default_values['country']) + while True: + country = ask_input('Enter country code:', default=default_values['country']) + if len(country) != 2: + print("Country name must be a 2 character country code") + continue + subject['country'] = country + break subject['state'] = ask_input('Enter state:', default=default_values['state']) subject['locality'] = ask_input('Enter locality:', default=default_values['locality']) subject['organization'] = ask_input('Enter organization name:', default=default_values['organization']) @@ -693,7 +699,7 @@ def generate_wireguard_psk(interface=None, peer=None, install=False): print(f'Pre-shared key: {psk}') # Import functions -def import_ca_certificate(name, path=None, key_path=None): +def import_ca_certificate(name, path=None, key_path=None, no_prompt=False, passphrase=None): if path: if not os.path.exists(path): print(f'File not found: {path}') @@ -717,19 +723,20 @@ def import_ca_certificate(name, path=None, key_path=None): return key = None - passphrase = ask_input('Enter private key passphrase: ') or None + if not no_prompt: + passphrase = ask_input('Enter private key passphrase: ') or None with open(key_path) as f: key_data = f.read() key = load_private_key(key_data, passphrase=passphrase, wrap_tags=False) if not key: - print(f'Invalid private key or passphrase: {path}') + print(f'Invalid private key or passphrase: {key_path}') return install_certificate(name, private_key=key, is_ca=True) -def import_certificate(name, path=None, key_path=None): +def import_certificate(name, path=None, key_path=None, no_prompt=False, passphrase=None): if path: if not os.path.exists(path): print(f'File not found: {path}') @@ -753,14 +760,15 @@ def import_certificate(name, path=None, key_path=None): return key = None - passphrase = ask_input('Enter private key passphrase: ') or None + if not no_prompt: + passphrase = ask_input('Enter private key passphrase: ') or None with open(key_path) as f: key_data = f.read() key = load_private_key(key_data, passphrase=passphrase, wrap_tags=False) if not key: - print(f'Invalid private key or passphrase: {path}') + print(f'Invalid private key or passphrase: {key_path}') return install_certificate(name, private_key=key, is_ca=False) @@ -799,7 +807,7 @@ def import_dh_parameters(name, path): install_dh_parameters(name, dh) -def import_keypair(name, path=None, key_path=None): +def import_keypair(name, path=None, key_path=None, no_prompt=False, passphrase=None): if path: if not os.path.exists(path): print(f'File not found: {path}') @@ -823,14 +831,15 @@ def import_keypair(name, path=None, key_path=None): return key = None - passphrase = ask_input('Enter private key passphrase: ') or None + if not no_prompt: + passphrase = ask_input('Enter private key passphrase: ') or None with open(key_path) as f: key_data = f.read() key = load_private_key(key_data, passphrase=passphrase, wrap_tags=False) if not key: - print(f'Invalid private key or passphrase: {path}') + print(f'Invalid private key or passphrase: {key_path}') return install_keypair(name, None, private_key=key, prompt=False) @@ -1011,6 +1020,9 @@ if __name__ == '__main__': parser.add_argument('--filename', help='Write certificate into specified filename', action='store') parser.add_argument('--key-filename', help='Write key into specified filename', action='store') + parser.add_argument('--no-prompt', action='store_true', help='Perform action non-interactively') + parser.add_argument('--passphrase', help='A passphrase to decrypt the private key') + args = parser.parse_args() try: @@ -1054,15 +1066,18 @@ if __name__ == '__main__': generate_wireguard_psk(args.interface, peer=args.peer, install=args.install) elif args.action == 'import': if args.ca: - import_ca_certificate(args.ca, path=args.filename, key_path=args.key_filename) + import_ca_certificate(args.ca, path=args.filename, key_path=args.key_filename, + no_prompt=args.no_prompt, passphrase=args.passphrase) elif args.certificate: - import_certificate(args.certificate, path=args.filename, key_path=args.key_filename) + import_certificate(args.certificate, path=args.filename, key_path=args.key_filename, + no_prompt=args.no_prompt, passphrase=args.passphrase) elif args.crl: import_crl(args.crl, args.filename) elif args.dh: import_dh_parameters(args.dh, args.filename) elif args.keypair: - import_keypair(args.keypair, path=args.filename, key_path=args.key_filename) + import_keypair(args.keypair, path=args.filename, key_path=args.key_filename, + no_prompt=args.no_prompt, passphrase=args.passphrase) elif args.openvpn: import_openvpn_secret(args.openvpn, args.filename) elif args.action == 'show': diff --git a/src/services/vyos-configd b/src/services/vyos-configd index d797e90cf..3674d9627 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -30,6 +30,7 @@ from vyos.defaults import directories from vyos.utils.boot import boot_configuration_complete from vyos.configsource import ConfigSourceString from vyos.configsource import ConfigSourceError +from vyos.configdiff import get_commit_scripts from vyos.config import Config from vyos import ConfigError @@ -220,6 +221,12 @@ def initialization(socket): dependent_func: dict[str, list[typing.Callable]] = {} setattr(config, 'dependent_func', dependent_func) + commit_scripts = get_commit_scripts(config) + logger.debug(f'commit_scripts: {commit_scripts}') + + scripts_called = [] + setattr(config, 'scripts_called', scripts_called) + return config def process_node_data(config, data, last: bool = False) -> int: @@ -228,6 +235,7 @@ def process_node_data(config, data, last: bool = False) -> int: return R_ERROR_DAEMON script_name = None + os.environ['VYOS_TAGNODE_VALUE'] = '' args = [] config.dependency_list.clear() @@ -244,6 +252,12 @@ def process_node_data(config, data, last: bool = False) -> int: args = res.group(3).split() args.insert(0, f'{script_name}.py') + tag_value = os.getenv('VYOS_TAGNODE_VALUE', '') + tag_ext = f'_{tag_value}' if tag_value else '' + script_record = f'{script_name}{tag_ext}' + scripts_called = getattr(config, 'scripts_called', []) + scripts_called.append(script_record) + if script_name not in include_set: return R_PASS @@ -302,11 +316,12 @@ if __name__ == '__main__': socket.send(resp.encode()) config = initialization(socket) elif message["type"] == "node": - if message["last"]: - logger.debug(f'final element of priority queue') res = process_node_data(config, message["data"], message["last"]) response = res.to_bytes(1, byteorder=sys.byteorder) logger.debug(f"Sending response {res}") socket.send(response) + if message["last"] and config: + scripts_called = getattr(config, 'scripts_called', []) + logger.debug(f'scripts_called: {scripts_called}') else: logger.critical(f"Unexpected message: {message}") diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 7f5233c6b..97633577d 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -212,6 +212,22 @@ class ImageModel(ApiModel): } } +class ImportPkiModel(ApiModel): + op: StrictStr + path: List[StrictStr] + passphrase: StrictStr = None + + class Config: + schema_extra = { + "example": { + "key": "id_key", + "op": "import_pki", + "path": ["op", "mode", "path"], + "passphrase": "passphrase", + } + } + + class ContainerImageModel(ApiModel): op: StrictStr name: StrictStr = None @@ -585,6 +601,14 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel, return success(msg) +def create_path_import_pki_no_prompt(path): + correct_paths = ['ca', 'certificate', 'key-pair'] + if path[1] not in correct_paths: + return False + path[1] = '--' + path[1].replace('-', '') + path[3] = '--key-filename' + return path[1:] + @app.post('/configure') def configure_op(data: Union[ConfigureModel, ConfigureListModel], @@ -814,6 +838,44 @@ def reset_op(data: ResetModel): return success(res) +@app.post('/import-pki') +def import_pki(data: ImportPkiModel): + session = app.state.vyos_session + + op = data.op + path = data.path + + lock.acquire() + + try: + if op == 'import-pki': + # need to get rid or interactive mode for private key + if len(path) == 5 and path[3] in ['key-file', 'private-key']: + path_no_prompt = create_path_import_pki_no_prompt(path) + if not path_no_prompt: + return error(400, f"Invalid command: {' '.join(path)}") + if data.passphrase: + path_no_prompt += ['--passphrase', data.passphrase] + res = session.import_pki_no_prompt(path_no_prompt) + else: + res = session.import_pki(path) + if not res[0].isdigit(): + return error(400, res) + # commit changes + session.commit() + res = res.split('. ')[0] + else: + return error(400, f"'{op}' is not a valid operation") + except ConfigSessionError as e: + return error(400, str(e)) + except Exception as e: + logger.critical(traceback.format_exc()) + return error(500, "An internal error occured. Check the logs for details.") + finally: + lock.release() + + return success(res) + @app.post('/poweroff') def poweroff_op(data: PoweroffModel): session = app.state.vyos_session |